BIND 10 trac2945, updated. bd923e85fd2f9bd6ad4dbfbbadae4fb43ff79210 [2945]Merge branch 'master' into trac2945
BIND 10 source code commits
bind10-changes at lists.isc.org
Tue Jan 21 21:23:08 UTC 2014
The branch, trac2945 has been updated
via bd923e85fd2f9bd6ad4dbfbbadae4fb43ff79210 (commit)
via 904601920fec509d9d28759fa03e1f8108ee4079 (commit)
via acc4ba201116d19be4f9d6389461088e6c89b3ea (commit)
via c27219ff78ee2927a4ab44e4f2a0020f7310dec1 (commit)
via afea612c23143f81a4201e39ba793bc837c5c9f1 (commit)
via 312e27c04145572a62815627ed63d9dc4ef866cd (commit)
via e3038321406be5d3548edb666de79b63b818dba2 (commit)
via 1d1e8b67f60e66804663fc9c3e8da4721f08074e (commit)
via 3acb0511190ab3c0b2711f712adfef35c172af05 (commit)
via 5a2b69ed6fcb4f4ff2589f679716cb9436116232 (commit)
via a66dc23ea5d6c6ecd34d9598d7e111d1c0ce696d (commit)
via 0ba859834503f2b9b908cd7bc572e0286ca9201f (commit)
via 1e8fea4a8987ece65543c5f30f09cdc4d5dc5746 (commit)
via 8edba8799ee54f28e6ab8ee9abe0b60e033ca954 (commit)
via e50f125ad256d2622c73c2df4448489e818a86e0 (commit)
via 432acd7ecbdbe741d07c20f8b43be107d9f761b2 (commit)
via 661e1f133e6c3a3f40c2af4ee8be358e114d641e (commit)
via 2fad937d0a224f49c926fd70747c5ccf34c404fe (commit)
via 805d2b269c6bf3e7be68c13f1da1709d8150a666 (commit)
via aad427b882473bdfe968db775858f47b6829d61b (commit)
via cb0080e355e2a8848e275b206354a4e516b9d661 (commit)
via af5eada1bba906697ee92df3fcc25cc0e3979221 (commit)
via 60ee84646638336b46c253ba1a91589094eac28a (commit)
via 65afca2b2488bc18e2d5235255a4b01beac536b3 (commit)
via 62d2898da8e632219f0dc45e5cb282ffbe526ca3 (commit)
via eeb1050f1d652969488cda690104f38f554e3019 (commit)
via 6d5522801eb392e991a4ecfc67be59e0ea7763b6 (commit)
via 66251995752ead4f93b51c2a05bae16a413cb302 (commit)
via e52fc6f0961bb5c54548760b7363b6ccb2bc1020 (commit)
via 68b6c2b5001d19eb88ea10bb1c40744cbe1d33e5 (commit)
via e64974ccce21ce592789ca36ba887ffddf2a1486 (commit)
via ec856ca7f468f0c23b19b6ed42282fabf1549de8 (commit)
via c918027a387da8514acf7e125fd52c8378113662 (commit)
via 381aca0b35bf693238462fb8e13fdccaa9d711c0 (commit)
via c716f4335d644744d16fa624e2b14dcf7bf217a4 (commit)
via d45c447d6c595b6ce5c15c17436c854ea512525d (commit)
via 7286499d5206c6d2aa8a59a5247c3841a772a43e (commit)
via faf86ad3a04fc7fca0af2b9d19abba0fa1d94387 (commit)
via c243f8423af55c2a58860e6bc0a72f70ac9a2d42 (commit)
via 87670dec20b372cb0afca087f1c7181de7f7fe59 (commit)
via 9ff948a169e1c1f3ad9e1bad1568375590a3ef42 (commit)
via 9934a46fea9632cd6ea6fd6e1409498316330223 (commit)
via 0d08bdc79a71a209b13a793d95a8ff4b4bea9d51 (commit)
via eb73d9223bbb17aed86eabaf29e62f9e18ac089d (commit)
via 779a230985d1d16af1f30f1948421bbdacb59521 (commit)
via 1736d2d26ee043529888649ecd536bc54f1015fb (commit)
via a42a146ee973d4b0b861458823761bf6a28b486e (commit)
via 17f56de8197a947fa796b3a8765d046c84341d3f (commit)
via c7a229f15089670d2bfde6e9f0530c30ce6f8cf8 (commit)
via d0da7ccdbf880ce20772bf69a985e43e7f083912 (commit)
via 9eb97b9690414e01d4685500aa66a19067bb68c1 (commit)
via fb678e639cd130cc718cff735e351f019d0c9164 (commit)
via 53ca658337ee97c1f8bbe04d8eb8b87621c6a146 (commit)
via 4ff150488bab58782433b1c6db9738470dfe45ac (commit)
via e7a2eaf0dd3a55c06b61ac24d32da592673014e9 (commit)
via 58da9f45c7789225738665f0aa384775498aeb92 (commit)
via e3913c4a5ce52f94fcde221e5c9412970eb5f251 (commit)
via 6625b4d9366bd09afc859e88e41d7197c9b7ba06 (commit)
via 861a9b6fbd5ecdb277345abbac98b8f8893e5db4 (commit)
via 4d83e8f254cb653da6c102b73637b09e1764fe24 (commit)
via c04d82e2d18892b359c6cf2b855de3edd2694d0a (commit)
via 8e2ea0064bf2c96487a1aed89498c7c6b2410f1e (commit)
via 48ce1e8ebd83672af7910cb7f39b14968ecc40ff (commit)
via 0b20f8d3da25bb0374290120a050a1fbce6a3492 (commit)
via 273e917624f994b618d5a499d81e1f8cc96d3dec (commit)
via 0edfc14c92d24bd5b8508913b8843bca2ba09cb3 (commit)
via 57a2ddcad8e466a67ad53511d90bfd00d2584cd6 (commit)
via 6a7967f6f758ac576da29061f5bb40bbeed6c100 (commit)
via 70db20e80cd758862a3978591bd504e9351acbc6 (commit)
via 2b4fea81954b2c34fca03086733051148b61eaf2 (commit)
via 756857b003804fe4a869fd52cf837e4e70bf5f0b (commit)
via 31e416087685a6dadc3047fdbb0927bbf60095aa (commit)
via ac194cf1e1c21f449bd2a378cb5a78ea1c8826ce (commit)
via 21d2f7ec425f8461b545687104cd76a42da61b2e (commit)
via 9d6d5822fe89f485b1f7319e63c7e4572daf0b44 (commit)
via d43dff9a9d08f925375ef6c58c26d4d32c526a9f (commit)
via e1c59f67be33e21c00032d64b6edb0db3882eb7a (commit)
via c994af9f1877b0a62b56cb34ac3d7e0528b2fa07 (commit)
via e3ae30d0122bffa12ef180728b7bdae66ca60e37 (commit)
via 11a9a5e03722ffd0656e60c80cbb82db902ac383 (commit)
via 7607200420438907d7d3e036a967c20c6b9b3f99 (commit)
via df980e49b85b8f04aefbd2bdf3ad1b6da85c3ad9 (commit)
via f45a1ff6b8d569bbaa913cbdd594b1ab6d8f88e5 (commit)
via b9063db0e6dac5389202131438a4b99a396740fc (commit)
via d508d4ac19b7260805fa0838ea627f2f05b960c5 (commit)
via be25b97a4ca9f145303e1028bfb59cff19ff39fc (commit)
via c517e0fa8f17cdd433bc46905bf6e6521a6560ea (commit)
via 5ec35e37dbb46f66ff0f6a9d9a6a87a393b37934 (commit)
via 102f42af2ca75b47457bf57eec1d67166be17542 (commit)
via e6d538fd1e2e2a0669038277c6565a2b06a4956c (commit)
via 2be3eca2864804c37f5eeb7c1fe9d6e562cf20e1 (commit)
via fa95cdf5ca22b4bcce2054a3d627fd4038075f3f (commit)
via 4e3b085d9487815577d6b3319b51e51111321993 (commit)
via ab8c4956534aa2f488cd7bd657e9162fddd444be (commit)
via fc1a10ef5bddba38d929774e298c2b66c57c6f42 (commit)
via 34fddf2e75b80d9e517a8f9c3321aa4878cda795 (commit)
via 94be309810fb7f5964fa52c8348a12e100bd0428 (commit)
via c56b6109a0d9a63be9d1664feec644d383ba91d5 (commit)
via a1beae5fd28331a41e5947213b353fcfb97b2243 (commit)
via 4063c0a8aa70cd0503fbf9d19a8b7f65ecd59064 (commit)
via b425b35008aa702182f722dc34e8fffc27f19270 (commit)
via 2f2d2cfd4de37453e0fd7a2c6e23ffce94784771 (commit)
via daa4b00d29427a0ba98f88bf7eb6cdecd39ada2e (commit)
via 71965bcfe5ce73408bddcef2b29e8ea9b459e5b5 (commit)
via ee836ac5bf89a79dd34065296cd40a1cab069038 (commit)
via 3dade343a9097f9c8c5f2f101b4216575db6234a (commit)
via 51fa695a018b40c1a35363690a352ea03bdc6918 (commit)
via b0c84e76de79f261883524f5e8d811cbc21071b7 (commit)
via 8840cfe8f5be3697f94f03df4e3b2c2d7b4421a1 (commit)
via e1f810f294bae8be142a9101326c066942665a34 (commit)
via 09cfa792bffe8ffa9106c915334ef539577fb62d (commit)
via 7715d34151a84d87702c18ea0717fdb7f63affb0 (commit)
via 0660b7ceb2b998b765e5ab48568f6dfc8111defa (commit)
via 687beb26da60ed2ce8e6eb44cd1725aa53f29f55 (commit)
via 210c5d0939516e7ad8250e4ff056f132e4d8963d (commit)
via cef44a4094cac6272f1a85c485b23fd047107fe0 (commit)
via 35bee74e98a1d769ef7d028ca3ef95f3ac4081b0 (commit)
via 1a6565ad222a830e630100751371575224bdec0d (commit)
via 78eb8a858925785728cf22052c79f09597d96017 (commit)
via 50c35b270c19824d0ec412b0cc1f3402eff287fc (commit)
via 9139b19b6220b270e2442731f258ba85c7877526 (commit)
via e7cac9c491fc35f45d4593e309b27ce6a33973dd (commit)
via ca58ac00fce4cb5f46e534d7ffadb2db4e4ffaf3 (commit)
via 13d812dc84b6967638efe30dd1dd1a34efcbfa80 (commit)
via 59bd021c72b3e2d9538b534f3ba00ae4b410da62 (commit)
via 3ff790761f37375de1f9eff59872405fc26e0364 (commit)
via 0e2851da1d2f91263c2f474fecdfe14427cf5c34 (commit)
via 0b4b438f337361eeeb7f5e5dbaf9938c24353812 (commit)
via 88a4858db206dfcd53a227562198f308f7779a72 (commit)
via 2133cf13c8ce75c146c1f66179f9c7d9a9228ef8 (commit)
via d8045b5e1580a1d0b89a232fd61c10d25a95e769 (commit)
via 113fef862040f3c52e11071ae363e9016f30df0a (commit)
via 689bd411d1c25b3bd798a24567cadf4f867475c8 (commit)
via 8f96d784205c46e35f368e39e9233711639918b9 (commit)
via 33003d2af6c0410719a68b7525284735df1ed797 (commit)
via e5ee6e7893e6ecad5760759f85eb0650435f8dbf (commit)
via 26ec9c66db6d73c45d59ce731d05d91c75674890 (commit)
via ddfaf375356cb49e1c784ae66c7f223c60537652 (commit)
via b173ea75b749984a94830c81ad6834f42387f8ba (commit)
via 77059f8a6b32612fab2c862b5dee11c728430327 (commit)
via 4276b45afad091f2022b690effd2711d6a587c79 (commit)
via 0ae10a533c27a489ec4bb434ca54c40e2f92f9dc (commit)
via f2f292a65d52ee73700658543d1587dc4a86a20f (commit)
via 70941f1375063c52a43a10831dc9707c6303f55a (commit)
via 39e902dc727aedf0fe5b949516ee746bad38607e (commit)
via c2222d11fbfc90e09036434c836b419edf6e3675 (commit)
via 265d1dabd2c745fd1884fa2afbeb951068dd9168 (commit)
via fa547a4d7443e7407a8e6cbb02d3263133b24a0d (commit)
via c6158690c389d75686545459618ae0bf16f2cdb8 (commit)
via 03c2a0f0a538aa4f8bebe9678a23473e0a05ee3f (commit)
via 684524bc130080e4fa31b65edfd14d58eec37e50 (commit)
via ac0abc3cf7c512c87122e0f5ead4f0c2fca04890 (commit)
via 82c80862e9c1c75d475adb002205b2f5f8259b35 (commit)
via 39c9499d001a98c8d2f5792563c28a5eb2cc5fcb (commit)
via 840afe672b9002ddecba485aec09ad085e09baeb (commit)
via df4334a7eb130bb0322d271253c4ffe15485a2b7 (commit)
via 96c135a9247ae93f5ba5c8843b72e7c34aa38825 (commit)
via 5862eb4c980b550d5206011d4db3b3d2fc649d05 (commit)
via 12c48d424f606311428eaf1029b6d25b62094fa0 (commit)
via 68d52986f180fa8574dc853febfa4a21fcec3569 (commit)
via 833deb1fced98bc568ab238a8e4f6f876ca97283 (commit)
via 95fd27d60daef2d8ca272ce3fde3911787b1d127 (commit)
via 633b07c7d425adbeba3cf81bf5dced2df9fbc64a (commit)
via 5674a5dc2b08f3c594408fc0b933b9824ab453ab (commit)
via 714ade4bb1e81b5b0dec1a3ef3de556f5fa6f6e3 (commit)
via dc1071d7aa1204e731c0a5959d28dc3425aa7f3b (commit)
via 4895dc78cc15d7b89ab782d7e74ef2ab90255da3 (commit)
via fdc7dd1164fad1d1ccd45b2713a231e0434dda08 (commit)
via 1391bf812cb841eabca6a7e5f67fc0324367eac1 (commit)
via dceca9554cb9410dd8d12371b68198b797cb6cfb (commit)
via 7a96b9bb8c38e0e45b44db09c5c2b015c2609cb6 (commit)
via 08b88147e8368d7f48a2e130deefbcb86cd645af (commit)
via 13cb26e18540a27e77a96736eb7d8029997f1ccf (commit)
via 3c33b14cb434d745187877e56dd3858648343281 (commit)
via d20cfa4f0f766b0c28eeea0be28be42685c1d265 (commit)
via 016bfae00460b4f88adbfd07ed26759eb294ef10 (commit)
via ae4ce59aabd5281822630668956425fa6167eca8 (commit)
via c4af356c438e33f0edcefcb305eb82d11326860d (commit)
via 68ec7bd3331f98e87e5a0f59cc0894d4e6792a84 (commit)
via d0841b4220752c10810f3664b90a05519796aa3e (commit)
via b25d9710abc79dd80179ea256793c1ac5556309a (commit)
via 19a8a4ef198d0b435f8f525cf5eaf215716084ae (commit)
via 4dc858542d2f7d7e4f32d3e0def1d3bb27fcfb85 (commit)
via f49c4b8942cdbafb85414a1925ff6ca1d381f498 (commit)
via 5e857a2109cc56c957fc8949343fbc1fd79100c1 (commit)
via cfd9d626bef43163918be6c5f93e295f4a0889d9 (commit)
via 1a1447425c6acc5601ea59d18c3638a0e65f0aed (commit)
via af2ab70ac7327f38f0e75f37175d190e0f77fa4d (commit)
via 5414d2bdabfc5d440848014901c7744979a92486 (commit)
via ae4c0644431f0da68e498b3334a9ea2e55825bf6 (commit)
via 9aad3742c573ad2f16a9b8bf07400e9c0b247e7e (commit)
via 13f795defaa2a330f4ccc637735f2f3a7d43a0cc (commit)
via 3ed35605cc7f7aeaec15d04e8c5f54a1b54369b6 (commit)
via 439951434df8bb361eaf9453a65f386a74700215 (commit)
via 8f99da735a9f39d514c40d0a295f751dc8edfbcd (commit)
via 33a7569f381acf79c29b57a20329a8ed94319aa1 (commit)
via 608638ff330a5d144516d8d556779aad5e6e8cb3 (commit)
via 3677658ea00a12e19eed9904480c807ba0e5f7a2 (commit)
via 68aae7c97b56de73918721193ec1ee29bee638e6 (commit)
via a52c2327de24151a8a9f1d93f4bb6c3ee3962ac0 (commit)
via ddf389fa6171e8b6b57bf8b359d5b2db45727dc1 (commit)
via 4c74dff22bd0cd130120d98847c16152ddf39bc6 (commit)
via cf3e0e1c486079543ad11af9a3ec48f0a70b4b57 (commit)
via d186a7f908e4796311fbfb97fc5e534b2bdb681d (commit)
via 6782bcbb2c658aecf68902af6f50f07cf04cf2f2 (commit)
via 9fdc1f5af7844126fdc5b578110ab91879666132 (commit)
via 93e9a85ea91a3ee3b25304a7049921061084c4ea (commit)
via fe717f9cfeebf48714de35b1c8f4b1ce323ca67f (commit)
via 65238e01cb0390c23e4937ed7710e968514fb858 (commit)
via 00d6ca046beb9f2e494043d4bc1397859d554f6f (commit)
via fe99fae754429d42ff01dc01d79fe7b5495beaad (commit)
via 331b10dc0682b41939e9a184e3c4a2cc9b132803 (commit)
via 5b9c261d8b66d61f1df4a088e9a283855332c392 (commit)
via 3ca6b9a20ba4d9931a686fd5b684166d413d700f (commit)
via b52a45ce15d894e540d0c8011786a913630dbffc (commit)
via c62586a2c2f8aecddbb2c1cfd0c00fb7cf41816a (commit)
via 8cd003e66527e9224b951bebd0c4c4e675e5827d (commit)
via ad0866f984b4a223234c4ba5ea1df55c5cf9bff1 (commit)
via 183693547ce28671d56bc272859b401c47db8f33 (commit)
via e4d1f908e14c82062aab9a391299eb0b2692bf9a (commit)
via d0506dcfcf7798859931851c3eb90956dd4bb03c (commit)
via 09811212553870d80e7958fb1161146a99e2d216 (commit)
via 4507742b539d4b992b184d3aefdc005264871973 (commit)
via 67e3af3c8a57868712f85d86d47e1936ee834628 (commit)
via 721815f9c773352c738b5bcf3a7e88bb9ad4cbf8 (commit)
via 4e225668a62a8fde0edb28a2938d968989f3cbc4 (commit)
via 0f7b304c340173ab49759f82ff7cbdf49aef5d7a (commit)
via 8017a0f12131114498ee21875ff0843310650612 (commit)
via 0f1ed4205a46eb42ef728ba6b0955c9af384e0be (commit)
via d4e570f097fe0eb9009b177a4af285cde0c636cc (commit)
via 0feb5b9c77c1677b51ea258f1839fdb2efd24ff5 (commit)
via a232f3d7d92ebcfb7793dc6b67914299c45c715b (commit)
via 927511744e90baff4b1d7b4c92797f0c9b1dbea6 (commit)
via 9bd776e36b7f53a6ee2e4d5a2ea79722ba5fe13b (commit)
via ae4a470fa56723eeb41df8145823144031a5039b (commit)
via f617e6af8cdf068320d14626ecbe14a73a6da221 (commit)
via 904161f5137d01126cb076ebebbcc7854fd182d6 (commit)
via 138fa31d02d9276f1f193b67d9b342357228e35b (commit)
via 48b7ddcc06665e8b13be3da3e418c31696461484 (commit)
via c587763b64ff0e38bab72961ead737b5462ebcce (commit)
via d77a5642cc9696757b2559f53b103218b89e9bba (commit)
via 1c9cf817f494416bfe7dba7896c4e23f331239bc (commit)
via 9ad9193ea3565d71a2c7a5fb7eff225be8c8e2ab (commit)
via e12b9bda527f26fff635e14b28cf60dda8f1cabb (commit)
via ee3c393ea080a07dd4e7589ce3195aa43d0080ea (commit)
via 9ea3131be861042b883033530ffaec46d190dfa2 (commit)
via b7dd9c0a496dfc8cc5779f0130a5375af1e84d14 (commit)
via 6443d1198b26c6fec9461301ff887258651fe423 (commit)
via 4921d7de6b5623c7e85d2baf8bc978686877345b (commit)
via 2fb33211cae9d8e7d43edc266279bf4a46fc4658 (commit)
via 41643199f9742a7c9afd96a2f20138c3f0033c37 (commit)
via 338d7d58b4d4f0f0cb19d14d8be87b03964e7227 (commit)
via 3328fb3879e398494bf86d8e74c36188426178aa (commit)
via 672d49074ea1a9a39e0cd53ef92db33980ceffc0 (commit)
via d57c3ae58c721dccc44a84e2eb30cbb60361bed7 (commit)
via 5d2952c08dfedf586da40857c2434b211b42d183 (commit)
via 89e903003964299fe321bad57e707b925aaf3a91 (commit)
via da025438b6d36873aea91193987e5b9ecbb1352c (commit)
via 12bb02d70471d46b8e1c7352289f60dc30734ce5 (commit)
via 9c0b16629926d904e0273d401862fb0939378782 (commit)
via 887d1e728e9d7bacd5e9daffc28db83d76103549 (commit)
via f7ee4ceaddef21ec63d85126611778fc31580957 (commit)
via c3cf34189e240dfd2a7006e17f405e2c67c0e0b0 (commit)
via f2f02dba84b5c12314f6018294423ebc45c4127e (commit)
via 7c4d8dae1b063b67e5382c070a900be1e3c9b0a5 (commit)
via e1771af6cabe0bad2f87b7c79405a92ce46e38c6 (commit)
via 0119b50f66d9c1ae139d663d08c99cbe895a53e7 (commit)
via ad2132e568f5db4b7926d2bf56214d195061cce1 (commit)
via 54910e664a3697b1b486b84d08aba0c382a5ecc7 (commit)
via 91cb5a837770be0fe38b64441ede9bd461443018 (commit)
via e03adbe6fe2a91adb5065396bb00aa16fa5fc45f (commit)
via e03cb3e3bc2bebf0924288afc7c62ab3382ec8d1 (commit)
via e144ab7b4fa2cbaa258849b8f2d31746326a43ea (commit)
via 070f123aef2d20da364f3f12d57e9be4e070d854 (commit)
via 7e75d22f278f460b7b61470c62063b345a1f436c (commit)
via ad5750db8c708d8849695dec43ff79e64a256078 (commit)
via e6df9bca72feedd92ec08dc43b53dfec632f228c (commit)
via 606b06cef9bae2038a10dad2ac89db8f93708e84 (commit)
via 32e3681b8ed317fbf9e3e195a854a0830fccbb08 (commit)
via 578392d0e3f5fd8abbce315321d3ec270d283eb1 (commit)
via 4c16336d81f8af698f6791c3f2a637106211e72d (commit)
via 4d07784b1a5773e5773e1ccb683171e0225a098e (commit)
via d6d7f7d201eb0dcfbcfda4e8589b556b596e826b (commit)
via 61f52a04ef4df1c8ba17ebf75baaa25e88b90342 (commit)
via 4655c110afa0ec6f5669bf53245bffe6b30ece4b (commit)
via e4475a2cd90aa4247482b1a4b96ec3dac21b802b (commit)
via 2696320ef2eda1e405748b89b854ac517fa3506c (commit)
via dfd3f41f975a491ae9f49df3c1ff8d276e99df7e (commit)
via d2f1cc6e6d1d29b33f8885cc2e7b2c1e46d62456 (commit)
via 66518462614dd93c7a0ca3111940952d171fbef9 (commit)
via da04b4807344089a3f6e008cf7a448cb4b4ad1d9 (commit)
via ccb253d801db7496dfc6a93a90db4a23cf2300ae (commit)
via f8fac8823dd928e2b5d6f4bf7bb31ca992dc0562 (commit)
via c18a49b0435c656669e6f87ef65d44dc98e0e726 (commit)
via 495195f672876310d4d10a997b0c2ac6c3c23117 (commit)
via b756633d52f492b02efce5303432f9ec97646389 (commit)
via 75867fdfdc6e53045e934d597bc601fca2caf204 (commit)
via 66f2939830926f4337623b159210103b5a8e2434 (commit)
via 3b1b5032e2afd07bf7d947e185b59a04d9805e08 (commit)
via 445a1ea248aee12202c7e076f7b85192384f57fd (commit)
via 9d3cf130a0c92c715b8ff1b7ce8f1743ab9c2bc7 (commit)
via 64ae9936d165c0e070bdf17809971918c29ee838 (commit)
via fe031382e774b530a89ad19aa4c6d894a2c2444a (commit)
via 081cc5311fd9e8cc52b0610b77bb72997eeba835 (commit)
via ab5d8f7f2ce5d95cd09ee0d7ad6999ed7b2c09b4 (commit)
via ed43618a2c7b2387d76f99a5a4b1a3e05ac70f5e (commit)
via fab2fd65c02fe2e215a004b094916c449f6d994c (commit)
via c9c132e4cb353b7fbe16e5c98ef72f8c7e498f81 (commit)
via 66748b4b5ecd06dc1af0793c8470e4b5e2ee7bd9 (commit)
via 2dbe0c8aa52634f064b285c8133255abd28cf324 (commit)
via d130cf2cb4c0d294a43d47d989732e466a8be24a (commit)
via 243ded15bbed0d35e230d00f4e3ee42c3609616c (commit)
via 5bb29d3c8d9f318fd4242e3c5a34a59b70964311 (commit)
via 276e655555df9302ba56ea5520a5713d1915bd7f (commit)
via db31405ea79f587bb7a1ec9b94c02edacb758271 (commit)
via 476e1924b7fe11c3a46408df4aede27e84f2c225 (commit)
via 1d28cc4d6ad1cc69ba00cc4e1d6822092a259e63 (commit)
via 6f6251bbd761809634aa470f36480d046b4d2a20 (commit)
via ff2b7624b7d1bba8ce38c85018b625aecca937ab (commit)
via bc5486ec4a8bffd32dd161d7238177c4a50fc94e (commit)
via e065bd5759ab046d5dc4ce071a8424db8bf975e7 (commit)
via 925a6f65ff995b19c877f064bdfdfb846d4c5b06 (commit)
via 83950286387c7e41661c810dc173171e8b9e1fd9 (commit)
via 37f108c1fb43466d8ca55ee3175a938281eb99ce (commit)
via 2b15e02a8b27e4a03ef6bdd2826acc44a50a5f0d (commit)
via d67f893b43f1c293d7342dbd02a6cbf044d3a201 (commit)
via 327d9ff442bd25fa742c3ea03aa4630440c00661 (commit)
via faea98cbf4274229b207b20adbb97524a5d3e748 (commit)
via 7f26bb20177605c9826f85ebecda30cb1ceaa0e8 (commit)
via 4a08e3662e7720ac6c96b7d585084bbe4033c29b (commit)
via 2e283ec70fa4c751835e1afcba65c4554fd78d15 (commit)
via 96fdb92a291b0f8897027e3b1c8b79918ae32483 (commit)
via 9fbb30d633080d5e6a65cef793b1a990f4f1d5e8 (commit)
via 9dc05967b079038d66d4a79ba607ac553500e23b (commit)
via 4cad8855373838b302370a00adee1501ee080d04 (commit)
via 95fb9fb9e6a8a45b74169ece6c0ab8b092b85b4a (commit)
via 748d5e585d6c4bcd5bb18498f768ad70f7e3bf0f (commit)
via 7fe44b1d9400284a0bd22cac9d197bf0776cce1d (commit)
via ed672a898d28d6249ff0c96df12384b0aee403c8 (commit)
via 3cbdec67c78ea16cf26742a3865046332774d2d7 (commit)
via f3f429b0e05eac677146e0419b50bc7704d1eb73 (commit)
via ea8ba75f55dceb13773d98a5836d075e231b2b6f (commit)
via 4b0a0a96a7ac23db5d1c07c3e9511574e5d49c57 (commit)
via 4ba8271b4050f3133dd6ca53721c0ce32ec715a3 (commit)
via 639db0b57ac5c0d24c1cd021d42385f03d7ce818 (commit)
via f36aab92c85498f8511fbbe19fad5e3f787aef68 (commit)
via 2c28d84f3657c10e6520c413705f532c0b42b1c8 (commit)
via 99db31bc3494aece5861df6c64b821564cbb9348 (commit)
via 541922b5300904a5de2eaeddc3666fc4b654ffba (commit)
via ea193899c17695b2a626cd5fe733506518eedcc0 (commit)
via d7460a8c4d81e973a45b70c27676d87b1a0523d3 (commit)
via 3d320e51a5518a6bb3578ff8f804ac1479aee9b6 (commit)
via 50d91e4c069c6de13680bfaaee3c56b68d6e4ab1 (commit)
via 92740b11ed7dc3a757e2082608596e0a3d173b51 (commit)
via 1a0dd0c77ad728edfd20bd329db0019f87fb64a4 (commit)
via c1ac5b336c1b1be2aa41e4202e997d7a57532935 (commit)
via 72e601f2a57ab70b25d50877c8e49242739d1c9f (commit)
via 45eb1980e4238717e3aee354587eb857759d74e8 (commit)
via c936de6a262f1509d14c3ce457c5c0b27e86a4e2 (commit)
via e135eb799810f5671b622a2e5d5e4bedf545c049 (commit)
via 6ff2610c28228d3a91b428b178517ff2bb7fbe69 (commit)
via d8d20c9781e6a644c85339b966c5c6a31ad24a59 (commit)
via 1bbab8c3c1d12e1a7aaa2e2fc36ab4fc5653c92e (commit)
via 0764a74e461d71c8d3dcb25dc2399a4d18d553f8 (commit)
via e7fff7124061a6fb64dfb03d3e652de8b883305c (commit)
via d1397da715c4c2fd9039923250f380e3f1172fb3 (commit)
via 78cb0f4ff0cfba61cf775c178632131f21432304 (commit)
via 6e171110c4dd9ae3b1be828b9516efc65c33460b (commit)
via 7f5a921d7005d7eb4d955a63321ffba7cb8cd995 (commit)
via 4707a2dbce19333a1c6cb1bc9020ef2051eca1fe (commit)
via 5baa1aeb9435170663bcce936e53fbac6d55eef8 (commit)
via 9a9b9d8e466efb4a3a2e363a65c84ac0a37f2226 (commit)
via e9f4d7e510ae5392cfa25c40c5aebe1a49222c5b (commit)
via a1b91fbe2702b3fb60ac639f3ce7286c525c739a (commit)
via beac4c1be1f4063d1b8420d0241f5f384564bba3 (commit)
via 5b11136c2a87ccac0c1ef100868dc647ea1feedb (commit)
via 33026699dd29fd15841fb860d35b50a8c7114c26 (commit)
via 732d98acba64fee4421a440b349069f10856451b (commit)
via 28c612e504efd4f340fa1d9929ea17cd75565a67 (commit)
via 7abead307c37c5d1f2a3ef3540e0648985997297 (commit)
via e76a6be3b3ffbc619ddb36b56e2a30d998ac9c36 (commit)
via 358735b6bc9fe4caf2b12466e54a90eb69a4e673 (commit)
via 07a72f62c2e93585de92d032b5af56c9a2ca562b (commit)
via ebeb291e7a086a17a6fcddb1abcab583dc59b46c (commit)
via a27c2a6a18ec14e5c79eae56beb238044ecb87d2 (commit)
via bd647c2a9820ecd3fc62852c42797265eca26ab1 (commit)
via e3683e45ff2288d6b1307a0c9d15271df9d0c722 (commit)
via 4d71b071d1feafe88cc3756fd4e678301e5f0b70 (commit)
via 770f4dfb5ac6abd620a01215ed85c1719b946f11 (commit)
via 426203928242be40458c4e2ea861257e32424b14 (commit)
via f8487b5689486f3563f6b7546ecd867f473d74b1 (commit)
via 11ec18030d858c037a862cc806a0329d3271d53a (commit)
via c95421cd2f4719b166700bac51361254483787ef (commit)
via 6291862d54c72d64178fe3c5f5b489120e297e12 (commit)
via 14918661a10e2f9c3367f668261656db1ee4bfed (commit)
via e98ccfe63db14be0d9a027797ffd75885b296969 (commit)
via f296e302ed073dadc5b03bc4edaff5a7da07376a (commit)
via f0edd52c244689cd370853d520bc6e97d23a3bb2 (commit)
via a9ec411b9522c9571f87afc39009bb7201be735f (commit)
via bf773a093fd8e7da0c93816fe3a9a744b689dd8c (commit)
via 5eede0e0f040d932d1a4f4e93d46afcd39b91cd7 (commit)
via 287389c049518bff66bdf6a5a49bb8768be02d8e (commit)
via 09695b7ff9631f67e247fa43f89a6653635dd8fa (commit)
via b080f05c590ce7e24c38db62194c85889a321ae1 (commit)
via f73fba3cde9421acbeb9486c615900b0af58fa25 (commit)
via e01ba2041d11147012551c1434deb753c75a9b04 (commit)
via 77c44b84587d190af775c8ab1d06e77857544d75 (commit)
via 62e07f67a8f462d7e7a1f446376cea3650827973 (commit)
via 4c0ef14158d57433edb9ae8aa1e2b0237773673b (commit)
via de9b4b42461156523d406375064d5c34d3d68b0d (commit)
via 87b3de01a1d6d49443831f1fd77f1d5e8ef91f7b (commit)
via 3207932815f58045acea84ae092e0a5aa7c4bfd7 (commit)
via 6489d09af85dddff538944f6efab14783a46fe9c (commit)
via 2838c0769e219dbd2c8a479efe87f9bfdb4da8e9 (commit)
via f1094390dc096d25df6465d737974a5d6f82e6ea (commit)
via d130980f515cc5cfab65df8dd0944b56d5fbcdea (commit)
via 2d83a3a525c255b565aa57e3bd5efce5ca38a269 (commit)
via 1644a516022739e5213b5c400ca240d354afda82 (commit)
via 48f0f8bb242ebcb0f7f4402c6bcf90bfb2650a12 (commit)
via 36eabc93eddd0eaf89ba2a5c29cf802bab4ad9e0 (commit)
via 708a8dd0257f40c076f3a3db58502dacf26ff020 (commit)
via 8e5d94583bc9da8374341509fbf0260d7a53edc2 (commit)
via 33a9204a11501ee3289161543e5abcacf042a504 (commit)
via 2115ed0befc5e58bc17960bfb89f5d209c2f9d71 (commit)
via dab092748d4083472373516cd4d576b0158aceeb (commit)
via c71753a27a64d409f28629f308dfe9f7afb7e0ae (commit)
via 79569d47bdbece10f1ee4532f33ec23d4c8e00b4 (commit)
via 4386caec66f57272c1239a3134dc8a0add870506 (commit)
via 0d62790c36094e5789cdb7ff713e8e58ba9326fd (commit)
via 235246a1c1aaf52a976896ab1bfc0df551abdb5a (commit)
via f42fbaf960614930dc0c23591e430411a9417624 (commit)
via 4527401f4afbd8f7a9dafb7296863d01da0d8c09 (commit)
via 2c20227ed4c9f1ccb5184e8c9698dcf7a7e79c4d (commit)
via a9cd7cf196ad37ab7a31bc2a26722d660078676e (commit)
via be15d120159f1b7380f88707efb05c4b11b121d4 (commit)
via 6b33de4bea92eecb64b6c673bf1b8ae51f8edcf1 (commit)
via a0e73dd74658f2deb22fad2c7a1f56d122aa9021 (commit)
via e43c23bc2d1006c2abb5dd7df7ed575cfce00451 (commit)
via ff3ddd8332fbaab5ff76f140ae9f185a2d2d6e34 (commit)
via 9cb427b4940ed7531facf3420f3bdd94afc37f64 (commit)
via 0fcc98b098d285dddcbdc73a06c38aeb826cb16a (commit)
via b054ce924f26fba021c4ab86bd663d9acaa6ae34 (commit)
via 988ee2ace62b93b41fd5261e9b27ed39335192ea (commit)
via d06b0d68f9d91b363183fe86a3d8ce9e46eb37f7 (commit)
via 4da14c5ca0e659263f2abd8bcee8a79ad79e3722 (commit)
via daf53e75f5ff4b9019b3859f7989ea37c2c39053 (commit)
via bec7f21950fde94b6c845a23bc5202b6bc2232c1 (commit)
via a715f62a23e400828ce9ef9072e7690fac016551 (commit)
via 361505f1c009d75af0c6a189d9433fda12d2a157 (commit)
via 7f464b31a95cefc4cad54aad92b8a94d5f557dc7 (commit)
via 9b7cc4afda911d762394c8e432fc429d027a106c (commit)
via b107f99d0ae8379ad635e7017d6bccb7d1c50882 (commit)
via 7d1431b4c887f0c7ee1b26b9b82d3d3b8464b34f (commit)
via 15c8b6ac63fd8f81b085fec8a0fd6d22085ea898 (commit)
via 7ae935a62c0f2fb0638cfee0d53c6b1fd63dfceb (commit)
via b825c117ad54e0503e5629d74dd86d8f41102d60 (commit)
via 4b8f75a145fbdd8006363c36ee32a140fd33ead9 (commit)
via a019cd162f1ad9584dcc67732ee545c6395bebb4 (commit)
via b48106123d817b95de69c2d58aeb24ef26a186e7 (commit)
via 0d5c7903522b79906cb0335006d678911af1eee8 (commit)
via 23c944640d1b697af40582f7999b40b05b17a44a (commit)
via 70d44604584934a651d1b3d975a072513aa010ed (commit)
via eee878e7817bcdc270ff56867dc0671f1d223e31 (commit)
via acbe4afb25b5f7eb6da8e259d9d8a78526bccd80 (commit)
via 582bafeae092fe5a1e67b03a0fcd74eba34dfe7f (commit)
via 3d19eee4dbfabc7cf7ae528351ee9e3a334cae92 (commit)
via bb0ac07b4621308695146918e37d4f2a68e2a9bd (commit)
via d3dbe8e1643358d4f88cdbb7a16a32fd384b85b1 (commit)
via 6f1227c27cded2a283ea3ec78e7497a825fdc005 (commit)
via e3f9c480c13dda60f83105377ced9f3255f9df22 (commit)
via ca691626a2be16f08754177bb27983a9f4984702 (commit)
via 698682948e932cfacb1c8a7f3c068dd5f9ffd050 (commit)
via b17b792e4a1015e301327296b1c9f466d2cb793f (commit)
via 79a22be33825bafa1a0cdfa24d5cb751ab1ae2d3 (commit)
via aa37f6861c45ad16577f327441f1dc2b546e258f (commit)
via 4a4a6098ba06773512cb4e20033b58c8353c8201 (commit)
via 1ef8a393e8257105a66a028c7dbc4141309139dc (commit)
via b5f8586097eb7bb1961fce2c767a71f534923d2b (commit)
via 66158f78882b882fb8fcc1be29e2969b0dbae75b (commit)
via 2e425ff153d59bf8f551c0aec716dce8e2d53996 (commit)
via 4cc844f7cc82c8bd749296a2709ef67af8d9ba87 (commit)
via 6660c4b27472285c5876eabf25ecc5b093861371 (commit)
via 57d3634c3d37f204f7cce1c62313036cd023faf0 (commit)
via d84999478ba92ffc94741716d3b6ae8cea91f3fc (commit)
via 6e9227b1b15448e834d1f60dd655e5633ff9745c (commit)
via a617211d81b31c5e72f73ed6043d183be61679cf (commit)
via e09660860573d1b76c65a200d3682d3592406b67 (commit)
via c83724e334548f586781ee77813348c75c111836 (commit)
via f67708b12cc011dcaa6a6352792b42f154527bec (commit)
via b2686c2da77de241fbc4bc0f2d642daaf3c9d402 (commit)
via e898091a969a7f4d6481be297d1af3676480069a (commit)
via 82a00e6ef79987b9de80c0b63950a8389592b3dc (commit)
via 4e7c1fb3533a53bd6cd21b563553f1e40f9bdf30 (commit)
via 97c7bb8690967c253151462c72af68ec64da3bea (commit)
via 33b1dda3335b747ca6fa0fb3fa421b50de867ce3 (commit)
via ef4ae0b32f759f25ba8fc20f1213406115fc6502 (commit)
via 646763a3166e60b9a8920100b5ee1d02d10217c5 (commit)
via dcaf9bfe96a40471b82b2f97455c57828460fdd2 (commit)
via b6c658d2fd1bc056f66beca05ae11162a14afe09 (commit)
via 7509a8d1bc64d4f688f6746d195987e5e22d4933 (commit)
via cbda6551031656b6d03f7d5578d0389c562a4238 (commit)
via d3f0e486be9ee295d1556ccb2afe11e843e16009 (commit)
via a282ac7153171ac4445c6b6eb2220937d097fd74 (commit)
via 8ed4c169acd688c7d9389991272c2e699a4dd8fc (commit)
via 242628300d9d7879e1dd90b2020f20274ee3bcf3 (commit)
via 4e9470edf1e15d65d8291fa6e33118cc92f70086 (commit)
via 08f0151628c90ba7c70412414dd0dd1019b708b8 (commit)
via 3e08c03f168623554f4dac564cbfd4bc5160bf76 (commit)
via 4b7664ccfdf1640cbdd914ddd032989149a3f0ef (commit)
via e295438305b351d4959001113670c6455f4a0abb (commit)
via 86050220a664a736659cce5d32b44a8d58914082 (commit)
via 7759a99d23a5d3246a160ad4363c33edfda2ef74 (commit)
via f8bbd7fa7def41d09c458282aadbbd2ca352f3a6 (commit)
via e33d14843af79aa49e86fe4f13b8e253fcbe7fb2 (commit)
via 511e9c6eca07d842e90f080663d9a3ecadb430c2 (commit)
via 7aeeab642dd16c978464a944fdc732246431a579 (commit)
via 8842945c12181ee427f7dfc1e7c1e1032f9cc25f (commit)
via d7d74b349336faa877191ed4cc9f157858325596 (commit)
via b190045d32b46b927e40146ecb3b0af56d21e586 (commit)
via a26a75c9487a45b3d6d73abf41590ae379c811df (commit)
via 7c0211c80bdf3cea4aad09a113e3afa513c55b90 (commit)
via 1702164569ed176fdd638ddbd47f0d1f4a8f7932 (commit)
via 65b6372b783cb1361fd56efe2b3247bfdbdc47ea (commit)
via ab67242d4cf7ae3bbdb7d0a57f55b44350cfc2fc (commit)
via e05189ee6561d06753b3bb2c8c6e4fad7c005625 (commit)
via 08bc7db55cafdd916ae227ff7ca524542de280f4 (commit)
via eaff454676513de3bf85d76c9ab724cf8191c649 (commit)
via a0aaef8644a889594af32cd5a3898a63a225b7cb (commit)
via 923a40c23023361591077e0560719a55f8e697e6 (commit)
via bd9f2d3c9e45d945b8e6ad9db79f93954ee0dcf9 (commit)
via 52d44bc5004ae501ca039896db099b734781f00b (commit)
via 74e7cfcd290d32ca6cfe2eccf959c796639a22cb (commit)
via f69dc1f6a5f9f350b8183f3969e43462182d0f92 (commit)
via b1217e691027f310f47bb7b23b621d5b8656ea6c (commit)
via 7430591b4ae4c7052cab86ed17d0221db3b524a8 (commit)
via 4bee63eec6164380cc042de3405102a6e4a74b67 (commit)
via 807d78bb19ec4df52474e91831c4b670c76db043 (commit)
via 43c722563552989ee5f7adfe99b0ba75bb56bb87 (commit)
via e6f0e89162bac0adae3ce3141437a282d5183162 (commit)
via dd4f121644d51c4bf1b943dda702bd72482eb090 (commit)
via 9b98a0122bc1cb39afc90fdece0cd63616ae4cc5 (commit)
via e4fe1a798cb7d56561c992662d84a2be46af8ae3 (commit)
via c543008573eba65567e9c189824322954c6dd43b (commit)
via cc480a64481dbce14348038cfa254578fc51623b (commit)
via ecc58c5e3a9e10f06357fcffaf169568b507901f (commit)
via eeacc3ac637e400c5f1512fc434ab08523521e2e (commit)
via c37f3b910e63df55b46516943a7e2ffd51e14e5c (commit)
via 22b8b335e30ab84bdbac60244d74553e5bf3b719 (commit)
via 68fc48d1f7b01e65aeaadeeffb08785899b023af (commit)
via f6619e644f62317afb4259e2af7732caa9d10c25 (commit)
via 6d99f3d904bb5a9688e6d95bedc9b0bc4c3a0e27 (commit)
via 37069dfbb56bd41717c439339668807a28ca9be0 (commit)
via 04eb5cebe1ab646507b4dab3964a87e730a79646 (commit)
via 8d746f00a3e7014d78502b3744a426d54fc0e3ef (commit)
via 18cf54ed89dee1dd1847053c5210f0ca220590c2 (commit)
via 720c9d6f11290c69b8dfc6e99b3de3938286ceb4 (commit)
via 2c152e0bf5106716d3360dcc549b3484945872b9 (commit)
via 91c0c0605d92d36bf5e51a3b41548455542a26e3 (commit)
via 928a9b6e56930f055d45f14f6f8d199e3470558d (commit)
via 339f201e9a6c58980f8cc9e6577e80254f04e951 (commit)
via 8b88f186238c9dc920a3a4dac668c8e5bf68927b (commit)
via b1e65b902324572544555903b45bc63ffcefcd2b (commit)
via c5c85a0d56fc30c9c0fc3b7040306360c961350b (commit)
via 733a15db3855503efe1b5e0b89d5558015f67a86 (commit)
via 384b365bc6895e251876cfbd440bee03076a87de (commit)
via fa1c3b6a4edfc169a86410ccf3b35263c6cb8d4a (commit)
via 6b8e42d064b94f40f2c050705e0724093ab00c9f (commit)
via ad88197cef83ed46a3255fe5948a233efacb7402 (commit)
via faa4cc45bb9721a6ede085bdbaaeef7d3c69b3f9 (commit)
via 4cfee008f5fff875b90416b3318f4b8e63b8d7e3 (commit)
via b1274061b9157ca486c9406422de7b913b904856 (commit)
via 223d4a422bf123539160029f40e0a0fdfeaac8b0 (commit)
via 7a9adddb3ba72d3b9e294d99b304d506b5a247d3 (commit)
via 342c56224c215ca1cb245df0875878e4f8fad0e6 (commit)
via 273b3105e21cfa457381983bf0c98f2590c55ebd (commit)
via e2dbd7090ae49267e0ff14f593de49629afbf710 (commit)
via f83c6c44a65b4cb04d3e2691e5bf0a3bf82d59c0 (commit)
via 3b9c90d00e5b0c2790a9d7c94b8765b61a6b64a0 (commit)
via 1e278e019742e8f13b93d6f670d50eb14e675217 (commit)
via 677e0bd0a52306ea81f3a7bab51257ffc466b856 (commit)
via 2f3324cbeeada97f94105d9c8ce902f496e34dd3 (commit)
via 37bb1f516ee852871f61b92d7bb5f50ba79a4b0f (commit)
via 19cb9064139e3b2adcf26bef7d7fdd38db356010 (commit)
via 3f5af01217a8e67822a9a25744b06579390a57f5 (commit)
via 8bf74aab112eeac3471cc9d9ebdfb74cac970176 (commit)
via 7d35d92626c35bd28ba4cf195fb679f44091bf6b (commit)
via d640e2711d36ff59c9adf84b092ccba7f17ef46a (commit)
via 0e24ff0516f95c8f0ae4a4b82892d97e40891530 (commit)
via 14aecfe2eb61e067200a91684eec7b04b10e0634 (commit)
via 08d4a94ef93b4a1fb0ba0fcbadc3793793c8c73a (commit)
via e674965bfb4fefd24fd44b70e80429ddea92e6ca (commit)
via e18f0aeceb9095198c3081cbd08e772a5715baf8 (commit)
via 2f326350f8f49fd6db247d38d6c3575e9e82a0a5 (commit)
via d2b3d82dc02b9e6fe5fab81b78f6dfc532888d3b (commit)
via 46e6d05e9f1c6990c17afff7890607db8c5acc04 (commit)
via cfd7033c2710b19d92d719f54ceb50fe866261bc (commit)
via cca46d0df7a4c3008e83d84e863a9be877f42b12 (commit)
via d8a6c3dd5baed22bb4b1b8729ffc9ddcece48924 (commit)
via 05a05d810be754e7a4d8ca181550867febf6dcc6 (commit)
via 90b595cca8c6b9041e9680db1d00f41f0d80423b (commit)
via 0e4c259dde279eafa1e3677a161ac9b67095c3d7 (commit)
via 27e895a6ee41d4588f6822fc7aa9dd5eac3c5738 (commit)
via 63b99b7f1c5d80b78da7d526f883d3b53fb116c4 (commit)
via bf6f8ee24cd9fee7c642114d3f1ca083625d6d9d (commit)
via 154fbc8f1d2a8de45de50799228a52585726d823 (commit)
via 48b5e32efb89d2c07b40c0d3e74200b56d984f75 (commit)
via 7edd26567ac96016c1caad9b7229d75b404f95be (commit)
via e3454067b2f7f83723befe5728b984e510593d0d (commit)
via 534a84a7d3f35a11204966317724bb8327a36282 (commit)
via 3a844e85ecc3067ccd1c01841f4a61366cb278f4 (commit)
via 9d3952fc9739348ccc34694c36036d7d46a5e273 (commit)
via 537e404c74470a717a4044260d7076b4b1a805ce (commit)
via 660f725b6fb6b326668d7c3c4395ec90b018535d (commit)
via 8f3591204bb394d7b2aa00508e99ec71a5720a8b (commit)
via 494ec81f8f76dc62a1ea51f1719ab0fb8fdc008c (commit)
via 42d8288e577b331401c4c785c8272a9a5a133c09 (commit)
via ee550617afb58aa9ae58f1cee64fb75496d3cfba (commit)
via 44d1f2b6eda3eb818cfb0d15f618fbaf6d8a9bb7 (commit)
via b5fe9ef4194ec0b3c2cec527834e27e3377d903e (commit)
via d21b3e92d6825acb41512747991b94f145d66199 (commit)
via 9e3e043fd5b34eaf866bd590fae3a3a252ac2bff (commit)
via 390b118ab527c2671062d52100137da674ced5c5 (commit)
via 9dc963b3f128ec1ec68ac3b3e778b80718cef973 (commit)
via 6a7aa9cb237b629b548bbf2d8c4041c8ec2b9a02 (commit)
via ea2cd98c25c7d51b4116203038105bc88a10ecab (commit)
via ee888857ee0e9d9d571f8087ff5edbd1ddbd637b (commit)
via fc2b2f8fa482e56c2c6f7e1e49db4129e9c5e7e6 (commit)
via 1d58f76d64c79770956797c41f91e261633f812e (commit)
via c8a1da135c0a6e9e82bbbec0fb5ed062a88ee37b (commit)
via 9273dca334ac61f89578b1b318b794325ced97d6 (commit)
via 346892cca6784b947e11efb446f53676b143e4d6 (commit)
via 0b0bb4a2785056f705dd7de8e5e9e30bc0eff80a (commit)
via abd29c04e21cac8115834506c2d9e44f69a4d716 (commit)
via 681def2806a3296aacab813882712da9109ba72d (commit)
via bc9c1208f3fdb1e0bedb850be56940048cf0b915 (commit)
via 079b862c9eb21056fdf957e560b8fe7b218441b6 (commit)
via 351b73dcc7a084622b91868cbab14ad2b2d93469 (commit)
via 95729b096ebac05beac6b42752b4734f4ca73028 (commit)
via 2093c5f82308d58537cb01130a2e54109cfbd6d7 (commit)
via d5d77acc2f46862e365fb42ae983158bd47eee9b (commit)
via d79db92249d8e529bd8346cffbaf5c19680923f5 (commit)
via 3984b23014333a678d3c78f8253312bede64a213 (commit)
via 0edf1c3dd2e12a28ecb74e47bdc969d6dc15fcd4 (commit)
via 9eeab2e6b02fcf316b81bcc72c0dadef84b9a07d (commit)
via 63a7f8bf4e61b69fd29a189f9bc683b5de0ce57b (commit)
via 8090d2c5186a96d80378072761625da2d8d421b0 (commit)
via 05b3886971fb003b0bbd735975bbbf0c38a1970c (commit)
via 151a2b857e77b32b10c007e9a37f37ddcfb03b8b (commit)
via a47c17e54da2b08afb9dae8f46a97ac3a5e5bda0 (commit)
via 612d8e5a1a35b9e359d610654c86b9b6fd1246cc (commit)
via 4924ba4190e5a2df3d7f5fb99c448869ae9133ee (commit)
via 617c1d7c950beb8ddceb2ab03f37dca55959deb6 (commit)
via 8e64776206cf6845c938ddfb1ad76c7d6a61377e (commit)
via 3d4ea5002959fd0510241f2c3550e90d9899fcee (commit)
via 06091ef843f52f38d0d13eb58bd1400f637ee96b (commit)
via ccb473862e3046f9cf0dcc923757a4ada7ba093f (commit)
via 9b97fd86866e26db1da44237352f5c9312d79291 (commit)
via a0f6b0ff5fcf50bc6fef4b12615c59656242c246 (commit)
via b3172918d50077b640796787e22c908f8f6580c4 (commit)
via 44af79cc0dfe2078bd7f85ffd555293935c8d659 (commit)
via df1550729f9ea49d6b0f90e677726fc0dec1f804 (commit)
via 91eca98edcc0af04cbfa27cb684dd3ed680565b9 (commit)
via 782e4e8e612336d637f848c1e2c754906754d79d (commit)
via a50e4235990f2b1b7090092e5fb19c7662480dd3 (commit)
via 663bc38be6ba6371ac5d2c0e4e2db845fd384037 (commit)
via 49c71998445264c8882c3b8389edd4bbd74c22f1 (commit)
via a11683be53db2f9f8f9b71c1d1c163511e0319b3 (commit)
via 4c18226299237919f351e50742032bd4c6b953b6 (commit)
via 92adc1fb42e8966816f96b4e6521bd2a18cead1f (commit)
via b526ece8f040116089977371782a40e7956d643b (commit)
via 8ce7f740b84364774d90f3438634577cc556ae66 (commit)
via 302a01c6707d2f46d9c4077eb9fb726533fad50c (commit)
via cafeb7250bc2c3400c0d3e7736f6cf3971bf741b (commit)
via e91d83ea552f8704e04f099a701a838e14947368 (commit)
via b0131d52efae513ed8d86f6c0b3bb4391d32ad16 (commit)
via 0a5bd2ebb43c45ebab9f466d102a3c9e4c024efa (commit)
via 8380acd2fcbd511f0a72f1854e19baa874e34175 (commit)
via c0e91c529e7e110f68c30c127a8370643a1a9b78 (commit)
via b30ae178084dbfda0da6ccfee9f8d1e27c926b0d (commit)
via e2cb0a39c9819482df8e7cd32ba37ed405d715fd (commit)
via 63e54b17e5b43ee480cb83a4fa0fa6ba08e2ccfa (commit)
via da0e9a81e8a4e4fac77caf257607bf4fe1bb0f0d (commit)
via b403553a567421006bccbf4b4f598b7aca738d54 (commit)
via 066f45d1ddf9f6998904cfa8402ea0d3a3f287f8 (commit)
via c0554a39903141b168944a9c67ef378e5c923e4a (commit)
via ecd5a9bc58e0f15933f5508afec02a0d48ba342e (commit)
via fcedc148b175e78928bf0f46d904e088ef549b00 (commit)
via 49fa0bad8fa7bcc0355acadede93b9f3ce679f14 (commit)
via 7cf392993b65c0b69443f1e749ae067c3d6f142c (commit)
via 0467d75d487b5c7d559ccf9a73e9fe105cd485cd (commit)
via 6024678d9b397613cf8f9b5454b14f9b98e1b176 (commit)
via 967e79e9748de98d874df011184c03bf27c5e8dd (commit)
via ec1cb0b7b54e161404079ca63124aa85d1553104 (commit)
via 1d38fe32f5f891958d302b46044cd961a074ec41 (commit)
via 4ee1f3305b8704b7c5da130e163285bbfcbf36d7 (commit)
via d15a492eb3479310af51cb1a9dde60faa1d57249 (commit)
via 313c564232efbcfc2cc8d34593ce9494ba96d629 (commit)
via 8a4c8c4fec6d869d9ecbbbab9c0374658cfd7a99 (commit)
via c812d223a1a93535f29ede6a082ea96fc1f928fc (commit)
via 128be57f78800c62ca957cd14350bac9627782e0 (commit)
via bb33775113793e5457116033bfafa8f97dd27d18 (commit)
via 091b8584615ed5da2441c579bc37cf1e9681db80 (commit)
via 5420c8bb51bd1a054df1962f86b34b6b0d4f4eb4 (commit)
via ba447c41b65a93c34f12fcb30f07352e12b16793 (commit)
via 79b7d8ee017b57a81cec5099bc028e1494d7e2e9 (commit)
via 2a07912bb6cb95f4df5caffdc6df4b07219b5dc3 (commit)
via 275d63283fe322e93ede93045b6ad870a6eaac68 (commit)
via 23078e9f9718a8dd92180f15a419ef2db1e4c69b (commit)
via 73d7a9538986f78b96584f5f9ce4946f86f05536 (commit)
via 927411d1d221265f82608e62a22683c203103209 (commit)
via b6ca9a816726b573004d1a9140ad38522bec50f8 (commit)
via 38d091beb6348ef3f74175625ffef8348e39c51c (commit)
via e66020fdd4fc764996e892728dcd098a2121cf62 (commit)
via 9da302197f39067d372cee065894e21303ad1034 (commit)
via 7feab5bbe65b961ba416e21a280b442246666a07 (commit)
via 46b7d9d9e329ea3ed9a338924e0cec4e3c83ee04 (commit)
via 5b63f9693efd47c600139b8522c37ea2f01666a0 (commit)
via fb86d9c79d26bbaf2d5f54046b3d8b653f34bef4 (commit)
via 1e6cf587beb55cb81fd4bf3614729cc72bd0c906 (commit)
via d9b6d7856906e61924a292f83dc2acb147386900 (commit)
via ddbff8acbadb104d53aa001deed5ad2777f368d2 (commit)
via a3f0255a3b6696ec8d2f21b78366ec44d13374d7 (commit)
via bd154b0950fd08ea60336c73249bc8b0de57779d (commit)
via 3ae989196066a542911ea9bd3ead0159fa8fa488 (commit)
via d03eba7fae0715c7122f46d283cc9962404e0e6e (commit)
via 4798d92c50c4b6ec09931646f1ba823882b268d2 (commit)
via 8829d940017a15fc738f2ebf6dfb3c8da61c54cc (commit)
via bf298463e636da9989a877145fee8853733f7030 (commit)
via 8a6e0a611829250157c74d6fe8a329629d66b537 (commit)
via 65d3cd24fbee716e9e331a9f4541e34341e5e15b (commit)
via 82fded3b3266bfe0c30dc89572b716898b5aaf8e (commit)
via 4d5859d8e390631a868339c2d435916e75112674 (commit)
via fd0ef5a296cab4d4d7417c3d3ce0110ec21bba06 (commit)
via 99b2d6f764a574071486cadf72e4eee3d3054ced (commit)
via 31e8258bbee3b576531a1fcf762c3a1f61e27785 (commit)
via b86a58b5f172fa9cc9e6e47c647b7acafdb6c0a7 (commit)
via 65052ea1fd8bb6b8a78b10743527ad331f1fa149 (commit)
via 8465110adc375f4605dc0f6b88150306c2ab929c (commit)
via 499e6f2e3f5bd141459ba46f90359d9dbc72c125 (commit)
via dc10e21d3ace93577e654885981ace4cae92ee09 (commit)
via 90b12d797d33c23ec96cc877ecf4c84a42f6214a (commit)
via f7a34944f319ebffaced2e1ea8a7dd1c420a4c99 (commit)
via d9ee65e374924d0f1cde75f4aa9c242218d1ad85 (commit)
via d936a170390f87dca5f73751c7f61891a3f4c4c1 (commit)
via c33aa974e86c6f63e062e562fc0196a9de0ae23a (commit)
via 1d32d636d7f81e1a5385a9107fb7da355aa26c08 (commit)
via b27c26f22c147e9bfe4eaf411ce2247f5a5218e9 (commit)
via 9801de556f82d5c6156d120a2d70a23487524044 (commit)
via c542421c3e40b6417862a07a3e0b5d3fb46f89f9 (commit)
via 847cada3ef9794906046f1b1094e066bed55b581 (commit)
via 41c4d829e66dfce48146a227ed396bf249a41344 (commit)
via 13542119b1625ab23d0b30f36d8b887876b9a818 (commit)
via 715419df2abeba0aeea9664b3a4b832c669f84a2 (commit)
via 8fbdef450e15140f9a9544c2f5ee3630fdf229fe (commit)
via f3c1bcd7bec13a0ff7246a66282bbd96bf032687 (commit)
via 442c5bd67499bbeec768570b5d3188a56da58e30 (commit)
via db1635daea11d94213b8a7cedaa2a621331b5286 (commit)
via f42ab59973d66783958e114de14e49fab63882ae (commit)
via e2f9d2e4c1b36f01eb5bfa2c4f8d55cf139c7e02 (commit)
via 15776db27f4a077e46ef8c30953c8e2e0b2eb674 (commit)
via 37af28303d1cd61f675faea969cd1159df65bf9d (commit)
via 0607a3acafcb7561b95577a56474d83b9c2da2c8 (commit)
via 73fa815876d3f6aa20e4961e51839af26d74d4c0 (commit)
via a2bc0e20d045d1009438c4c1565689931d66b213 (commit)
via 7dfb7839854252b4b6ec9ac626af0e4f2066d919 (commit)
via 39194524eb9a6ce9fa53814516c1ceaffda3b9c3 (commit)
via f09a4f68f8397b150a94b6f9b78da60b117a8fe1 (commit)
via 968bc96429fc12aea18e5e4186fcafcb5f29dc58 (commit)
via 5a860a011511c6977f354f2d74eef46e6d970896 (commit)
via 8ce213f2e0eb1cfd313897288e08be41047cdbd7 (commit)
via 50072746b495d5c14b473cb4466fc3b7fb6d87ea (commit)
via 4b1b5fd99b7b11c8c77b1c36ef06cbe979f9c0f8 (commit)
via 0207a121a00d0e419a6d067d75696a08b1d1e679 (commit)
via 3669c95989ed36429f32a593d74f58dd917663b2 (commit)
via 5207d193067cb884e6aa4ad63d1a96d52ef04f96 (commit)
via 8bda455d6c105e505ed0d11223c708e2bd3b6f7a (commit)
via 8c203f0a835b8b5a4f6e40603ed2c7ff6ed86706 (commit)
via 6be8194bd8c4dc90dc0b702ba1450e59184689a5 (commit)
via 21b9e729efc4ba70e36c69cb223f61d0fb0f81a9 (commit)
via f004912dd236a53ee63ab51e937de7988681a22f (commit)
via 879bf7b43d45f1952473a5bb747f599ddb969bfd (commit)
via 343252ae26f8e68890143fda004008e304dee087 (commit)
via 038082d3a8df1904538a11a6b803bec1804f9c8b (commit)
via 5da41fa159bbf626a3c9acd0d4e49e96282d7589 (commit)
via 918419085f722e43c567d4498e6ca581d925ce26 (commit)
via 4e92be8690b5b439d1d022083d6677a5c1a313d7 (commit)
via 7b722a23d6a85ad0c3d903323e9a36ea2baefa0a (commit)
via 0d893b54513dfba249cf209ac09b0271861437bd (commit)
via 730aac841ed98bd35582d0a7c39a9b00835016a1 (commit)
via a5c4d0ad9dae4bceabac8b85f91ddbddb3ae6871 (commit)
via 5ffd53da7b10bec4ade51ef974a9b9a23e1a0085 (commit)
via 702505336da1317a8cfeeb5a04e8c5d1ac439469 (commit)
via 7f0bb9294f71b325836f3c4fd8989b382627c19a (commit)
via c369c2c9a3bcb252843bd25680627484486b8802 (commit)
via 3674f8287a482a77b9bc2f8a0f161ce2dc4e3f9f (commit)
via 79c66d327a479ff5d6dca45c17e438a404bc9dec (commit)
via 9b06e690a16a338fc943fd039d6cee03e5c9ca45 (commit)
via 0b3041d1b0191e610a5ed3f8b4e93e846823041c (commit)
via 36db9cbd518e50baabd62f645965d656c3cd7531 (commit)
via 4d4b0dcd1dc2571a98b4e60bf846dd704ed7200d (commit)
via 004adb24a8aac2527b2729aee398717b5ff505cd (commit)
via 8ee0490dae879424b7e20fe1994ae4f2b294e3ef (commit)
via 8ab644fc53d264555f4896cf05fdf28ea8753d80 (commit)
via fd47f18f898695b98623a63a0a1c68d2e4b37568 (commit)
via c3473496d36e6509c797fd6f55e9b9acd2ba8638 (commit)
via 96ad742151cbbf6a21da7827f395a9365c18585d (commit)
via a3cb27a3800156227c362565a5778c1f3f9a9557 (commit)
via e40e2e96bee958e49a7bb389435add5d980419f5 (commit)
via 0d22246092ad4822d48f5a52af5f644f5ae2f5e2 (commit)
via 803bd5a3f260b5412a58ee61bf450211d4b4e061 (commit)
via f17c3559e3353f42150d8fa742e1658285811d47 (commit)
via fe33a56cfbcd0f846e09a6151313bbb510f76352 (commit)
via 55235c63e0aaa87d8a6ef8e42ee98f7f99c532bd (commit)
via 5d1ee7b7470fc644b798ac47db1811c829f5ac24 (commit)
via 7fef2d7ffdb3b89a18d6a56799564514bffbd0cb (commit)
via 8abbdb589f6205c51f461541778f40dd355db60b (commit)
via 9a1036278055840ef554c42d398478430c8ba188 (commit)
via 28684bcfe5e54ad0421d75d4445a04b75358ce77 (commit)
via 9fab3d3a0124810b18c32b181b90da2737cae477 (commit)
via 18dc9853c008b26940ea70167daec3a26b382a79 (commit)
via 231fed6c34f32ce87f15be0f79bcc258f900a783 (commit)
via 3b8241154843db71a970ebc30b04fdc3a5e73d33 (commit)
via bf6d6bcddd460b7fa2c77314bccb4e14ab856609 (commit)
via 078965388c8160137cfb8ddbe05104d90f3bcf31 (commit)
via a8bb19e3163331d8b1137869d4ddd96353d654c1 (commit)
via c7e1ff0338200957269ba996263aac8224987f62 (commit)
via 1932ac5a9ffb435e2875f9e921beb486e2be8f4e (commit)
via 9b5c746e9bf6fe264f4aa57dc0e884f4fd5a8d85 (commit)
via 8dcab488bb0d93ec9b97075abecf679db04907ae (commit)
via 1f4b736004977ec0e089424298d9b5dbf49a3f92 (commit)
via f164402c6ddfc870959bf99ea9e5bd4422b7108e (commit)
via 3bc0e24e4d1692c73124fe9727141cc2552e5aa3 (commit)
via e3d5529c2df4ae726877c1ede5f4d5133e6ef2a4 (commit)
via 37d09d3125a73a52a30a418d6f532bfeb8c46b34 (commit)
via 5b38a1e7569c88f5bafc91504c42e16404356f9a (commit)
via a2f074a799b7bdf781b6562f57ecbf9dee991872 (commit)
via 9ddb7eded5402f31b606b7f22f275b890c0904a7 (commit)
via 949fc23ebbadfc8a9beaee6fdefb1dfc661d819c (commit)
via 3bcbbb7f26e1a36ad152912d8bea1a1013c622c9 (commit)
via 830fa71dc9dff20fe98af49455ff639ab39bbce8 (commit)
via 209f3964b9f12afbf36f3fa6b62964e03049ec6e (commit)
via aad0b789f4f82e50e219debc7d6d52675d7c85e0 (commit)
via 57f5c253848314ed4e6e1b8685128b29d8dc8240 (commit)
via 1b434debfbf4a43070eb480fa0975a6eff6429d4 (commit)
via 586809ac7369b85efe135f62b6281e141d88bfdb (commit)
via 0ece621e566a76a6129425c43792f136f9d857f8 (commit)
via 4cd6015489d77e842735a1ecd431ea68af1820bd (commit)
via 507da494700d05fe55a928ad57757a4af3d5f2cb (commit)
via 55f0a8f3f54c91686f910669b5183e4e95b51ae0 (commit)
via f5c6cae3cdd1a3fafa8266575a5cc8a082ef8643 (commit)
via 6c968df2fedfc4c0b464fde5e1772dd24e37602e (commit)
via 16ba5d5390af42d05d3d877ec6a476f3938b5e4a (commit)
via 622e72a46501094b0e1f91b17452448856bced79 (commit)
via e9beef0b435ba108af9e5979476bd2928808b342 (commit)
via c39eb9bbe30285a2b19fea86473b63ddb758506b (commit)
via 0b694071801534d824468dff5ac073a29ba3bdba (commit)
via e4d2d0f7380ba3a9cb62ddc87bd766e3c811f5b0 (commit)
via c8013666dc2f01d4a5c258eacd1baf8d0bab6627 (commit)
via a5c0db852ec1d854979be9c3abf24e1732fd6f4c (commit)
via a8dfa35d56e421fa565efb4de12bff2ee2813aa5 (commit)
via cc2866a7dd9a2eb30d328a24d3257f3179229a8d (commit)
via ef9de155bd156391ed1e21a024fa9bd3835bad18 (commit)
via 51e47dbf6cb9f1b78321d96c0548f7aae140c583 (commit)
via 679445b96b4603fd8f6975aeb526abae8922c2bb (commit)
via 04f940ed5e4f796ce09e1cf216da863a9c9a0d9d (commit)
via aba2bd5baaffc9a69ff99ebbc84c71907138c59c (commit)
via a635a6d6ce769d71109e683f0e0480f73b818112 (commit)
via 33edd2ed3d657798c02a75f236c087527f8137ad (commit)
via e15d409632ae5408531051b3164c2e2b851b214e (commit)
via 4aab881a61571329ebfbf9ebae3f945772c72761 (commit)
via 6ce7437c65b714361b3c100154af4c352859bd13 (commit)
via aff6b06b2490fe4fa6568e7575a9a9105cfd7fae (commit)
via f73cd9bc4a0bbc14f106cec09d3843bfec1971e9 (commit)
via abdd9be83ca3ec7267e4d04f014584fa823f7f6c (commit)
via c65345a08645b1a20f442c626f9498420d762c91 (commit)
via 9136b0a06706f0ba4e7eb2207513930b14484400 (commit)
via 7bf3c7b54ce1ad1ca86d3120c6cda7d39f909612 (commit)
via f80c824d8b1745a0cedbbb238b0e988f7acb10ee (commit)
via ad4c8681d9700216ccdcd347823c5131f2796d60 (commit)
via c185e035984740f3914a28f6a93f9d47ae8e75b2 (commit)
via da34264f7277725b59764e73be597ee7cc513a90 (commit)
via 83645c8498c57f27907113a4d062fe47f74739d2 (commit)
via f8e9c2933fd726cb1499fc03c480c0518e23a89a (commit)
via 334e3b332c8ad84b4e4c04453a43303cd2706a33 (commit)
via d7173396ffa2c262f1a3e587152bef7ad323e4cb (commit)
via 2fbb93f713ace228c5d73fc38e2b3c53eb8cea1d (commit)
via 1463b5017b2c3a0d512ac54f8e644156864452c6 (commit)
via 95e7d858ada42f6180d677d176b79b3b91219376 (commit)
via 09b659275c05d9bb215148b5c4976eed50ba4cba (commit)
via c276e3c22b719c6e604dc853309da85797b506c2 (commit)
via 36a281824e92417f23c0ecbbc0c4a88a7e9182f3 (commit)
via b177e3f9b587dafa15133ef0b8fe621cfbf95f2e (commit)
via 785e97d51905a7d7d7d32d08ac7d82ceee272fc2 (commit)
via 4e902ba369b16a1b3202a4c079af9db429295c97 (commit)
via b92b89134b7393e24e9bfbe16deee9ca99d352e1 (commit)
via 8838f82a224a2aeb7479bc2c4bc3c44c3c20454b (commit)
via d038572fe9fdd7dc3e05ed597df5297cb161cfd1 (commit)
via ea8b95396cb411187a22dc78fe460316ff2029ce (commit)
via 9823dc9e0a2dc618d3eb6eacbfe6ee29cdaf0500 (commit)
via 3edad954ddf775772818ff87221bde52350bebc5 (commit)
via 646a4738545ebe5a44c6922a2950296a0f3f7c55 (commit)
via 72077a1d5b36a470ee5c16599171a2d7cac11ac3 (commit)
via d581f79243723a01240da4b7b5adb8028d016e76 (commit)
via f5f07c976d2d42bdf80fea4433202ecf1f260648 (commit)
via 278c59a0b5db80c61ed08550da6d065af8aead90 (commit)
via 22b29c570a68a8a719f10e178aa27210b2927f57 (commit)
via ed7586fa45a5aac074e8f38a01e8ff5a48ffefaa (commit)
via 598e458c7af7d5bb81131112396e4c5845060ecd (commit)
via 511912d01bb7b2019f0a36100c524859a6de23b6 (commit)
via 69024d03d0cebda395388fdcec2ad559c093f8aa (commit)
via 544668f9aac4823519cee2b10b52315c0df4c259 (commit)
via 578910f51f1dc96876a67a02d353c396cc4d2ec8 (commit)
via 1745301647282b8db81d5618543aabe66b255dd8 (commit)
via 35a7432feeb263c42eae96d6388729476f20b6f2 (commit)
via cf7cb62d777a1d74db1abca0753e5fb2cfe0c310 (commit)
via 535abe75debe26e48284244d0e856b9cad4eeec6 (commit)
via d07c725e9a441e363a18e66965ff7e76f4de526e (commit)
via 0eefdc946964638e619c686604c7a275919e2b80 (commit)
via 7660c95d4f700d1324eca7cff26bbfa16444c4e8 (commit)
via 2092528ae67cf42002c2aef17140d95cbcab8a54 (commit)
via 806ec4bfdf1561f4e59c67550363caf10c41e10e (commit)
via 08a44d8edc6c12dbbd54b9d8b68350732ed57668 (commit)
via 21bc5facd59af83c64bea54baafa025e8ec63fa3 (commit)
via fe763709d177243f6bc22d849d6277423b71c32f (commit)
via 71d687c3666c413e0460ee6d2b992d212547e01d (commit)
via 68b075b70cd9b3570b81397d0be3ae27f3baa5ba (commit)
via f59e953a20f34118e276c215ed5d75f0e0611c7f (commit)
via 290fb63c1c7640c8fdc8801b93c36f8ca0432b96 (commit)
via cf24637df0959a2b68f2bde161790a73e3270abd (commit)
via ecd94090d4f063d308854041c4343775ab60108e (commit)
via 3483361dfe0d9b0a4448fd7e53a7c3845e66be67 (commit)
via 623eb1b10deb087d9d0dc6c6372140f09933f85f (commit)
via 39db0756e576730953d3ff526c8c124103f5b5df (commit)
via b1a9280bd41ac9de63ebad586033f6ba1326b8b7 (commit)
via 1596f2ccf2273eb5be25ff92ab196f9bd714c0c4 (commit)
via 5498431f4deb81a175135d8334d5f6011455ea1f (commit)
via b2a59752080f1aa7dbcfd677c5bfb8f65239c4c1 (commit)
via fef265b2e3bfae238c91bfd820bed760efc748aa (commit)
via 4fbb85cb991a8f3f412c2c386dcbe154fa5f04f2 (commit)
via 516bb5392ff827f17b7c91d42b6c4f3151a2bf44 (commit)
via f98caa5f314fd63d66e74461bbdd9b33910d850c (commit)
via 516417bc1e769a0fb7e0d965ec9cfee51c342624 (commit)
via 695d7c75d5b307bd0eebf042b78904396e3adfcf (commit)
via fafe5cb09e43b29a3b014a77bb0cf3c4c1ba47a9 (commit)
via 88636c950bc80ce218aa5a98bb6cefc70b653fd2 (commit)
via 81c1c17fd56ac7b087579ee46205349623f6be41 (commit)
via 39588e5fbd781cec7344e07ddadda835aa74d8fb (commit)
via db29b326905b7915c5e1723b565900f06659eb30 (commit)
via 0271260016349e40ec8d366b82077d93aac3ff47 (commit)
via e3dec9b39035a59bbdb561c12817872d66f34fbd (commit)
via ffbcf9833ebd2f1952664cc0498608b988628d53 (commit)
via 1ed6c5843426b22dffaad30ed5403e034896a5ac (commit)
via 10d9ff6b5b241f41939bee5bc5304d24b3147e15 (commit)
via 73372e3a10aef030bb33f2834357fe2f4a3bb481 (commit)
via 1ceca44c4f118ea3d0246a7ac4b67331fcf4241c (commit)
via 34081cbfbc9f0be52330015f8b58dfbbc7997ef2 (commit)
via 540dd0449121094a56f294c500c2ed811f6016b6 (commit)
via 1b46569fab6d606359ada85f6736951c969e8eb0 (commit)
via d72675617d6b60e3eb6160305738771f015849ba (commit)
via 0fc5b23ed78c9b9ed16437a5b53e45881d413e77 (commit)
via 9c88046f2b021dded1981966b930029e765b5915 (commit)
via feeb79f56b2fd672d8ba20607ed26164d362d0e1 (commit)
via ff0b9b45869b1d9a4b99e785fbce421e184c2e93 (commit)
via e5b3471d579937f19e446f8a380464e0fc059567 (commit)
via 6faea427392fa8809269171dc7d98d42177af846 (commit)
via 4f2cfbfa1b3edf01bab9c4d8b38a651ab857f52d (commit)
via 40b956102de5751498e0acc27dbf99a7c554d627 (commit)
via f4d4e14ab8c0756bd19db78365b4eb1593a91867 (commit)
via 1176cc4839f9813cd49733a077715b5b2a767f8a (commit)
via ebed5cb932b16588af7a8fafbd6fc31321154f3e (commit)
via 307eb1cc6cd90c831d95af0511424b5b468e4d37 (commit)
via 72bcad54040078acf2e30d2f31aa177616a570df (commit)
via a970f6c5255e000c053a2dc47926cea7cec2761c (commit)
via 508b40fdd135122a0e006524ac882794d28cfc9f (commit)
via ce9289d1ef31525a60dccbe0daa72e8f8db1be63 (commit)
via 0efb9524bf1782a3e5875982a844a86e6b40460f (commit)
via 067550e38d9bba5831375e9cbbd93674f1353ee2 (commit)
via 26a805c7e49a9ec85ee825f179cda41a2358f4c6 (commit)
via c79031b28b835ccaf0d683b80ae877a5c94c12d4 (commit)
via 029dc6aa03d7d72d60cb424f1121d916064cd6a4 (commit)
via 6674553cc7932dee920e06a4936ba28916552e25 (commit)
via 869ac09027a0fc146ac995e7b1dcc875e6184608 (commit)
via 2babc7e39394de13cf72b0009e47c3c994f346c8 (commit)
via b68e8f32a7128d99e69a9ed42f9d8d21d6a68706 (commit)
via 09f557d871faef090ed444ebeee7f13e142184a0 (commit)
via 2b589c6418e5882bd14bc9471d292d6fc21247d2 (commit)
via 9c3f96ecec2fe57a90680b80fbee8079c94c87f3 (commit)
via 7c0bad1643af13dedf9356e9fb3a51264b7481de (commit)
via 07f62405839389c802c751ddac3ca0dd43ceb754 (commit)
via 59a4d0243d77ae0a12a8832e383892485157447a (commit)
via 22cd67d216fb454b2aae9086a7a0afdec3c79c32 (commit)
via 91cc21601ba76a1a99a42b31530a55948d7a9c93 (commit)
via c322cadf5a251ee14444a059e6ae66611db97488 (commit)
via 36c92588ee9973fe6400547dd38a18fe62161c2e (commit)
via 9388cae1ccc1d3e5c6934c47c857a61ab17cf52e (commit)
via 27d057a79de7e769cef2a87e59e08807491bf44e (commit)
via b8fa96e3498580f2c01b6baa1d38e649edee418b (commit)
via ccdb49aa255c988dd3f03be21b9511794af288f5 (commit)
via f36f677e841adddbaa762480d0cf8b961c098df1 (commit)
via c386d06ac5239ae26446c3bb75143ac54820698b (commit)
via 56f41442f0a647c959e23d8b96ef949607ce98d4 (commit)
via f6fd9cc583267c0ddf779ee0d0fce7ffa697c554 (commit)
via e323b235b5cd03fda009b976fa7765f032e4ed35 (commit)
via 063f70ad570dfc5d8697341bf8643ebe3eb80d7c (commit)
via 363e37a380d7bbfca480e2008b3c135029647d69 (commit)
via ee0e068ef274d482659b11f444d67fa5d0b06de7 (commit)
via d5c2bffa4970fbf53178e4a1975a0e476d3bda32 (commit)
via 26b0ae80ecf50d3868ca059a5965ecb275bb135b (commit)
via e3e92f0134210e040c537753fb32e36679f4b1b0 (commit)
via 2e4d7c7f734e5d10881698e05a26193199558fc5 (commit)
via ceb084b6e3c728d23154c5ea4ae8d10bb3b91477 (commit)
via 35c53d9ab3422ba82ebd2c4f9afb73f1a5dc58dc (commit)
via 6606b6d02a352c15fa60f842bef9ccfecab93c41 (commit)
via 2b38d4d369e3519b835cda94f1f167b4b96d5a0f (commit)
via 940eae0ca5d303a9420747a5c84358f91515075c (commit)
via 8ddaa8da24109d602cb7aced60e57f8422d0c529 (commit)
via 4bca9c538b8deddfc49694d35bebd44c0bbdfc81 (commit)
via b920504363ea51053de5d287dbc68bd271a39f90 (commit)
via c9f3fca8e79716778c0b296a8a0b78db6571f4f2 (commit)
via 8acbf043daf590a9f2ad003e715cd4ffb0b3f979 (commit)
via f964dbc5497ea2aef884251c708007e97185df2e (commit)
via c989ea1255cfb2086655c86234270b10c55efdff (commit)
via 51e0b7424c7b3992e1bec9d524c7554ab33f45ab (commit)
via 775f572c3c6cedcd8f1985c5cf969cb18dbe2e9c (commit)
via 434edbb7276472a4ac31ccb63257fda00698baab (commit)
via 9b71351b47386012f178803b56b71f90a972e9b2 (commit)
via c46e16711aa802046edb5557dfe89afb04be229e (commit)
via 3056c7dd61a56b7053d04819b0b778e0297ffa25 (commit)
via f395f315a51a23ac058456ec15580231f145bd75 (commit)
via 63d34705073649a2de35b2f85cf66c1bf64aaddf (commit)
via d03c9fce131acc35740a52e63894761c77915c0c (commit)
via 7b94068163e1d90e4c10a19ec5a847b614f31f99 (commit)
via ed713618c11b1e63b4a6e10fb487a4bf3339c2ac (commit)
via 9694e88402b35b3df56880c656270c1108e0df13 (commit)
via 3b2674982d558f22f9a2db3b43c922e29ba7520c (commit)
via 2459cd14aeff5cfccac711b6e6de4a4cf42817bb (commit)
via 2d47b144b1a760c4338f534848b9b801fd709964 (commit)
via 943e416451f87977b7aa8ab63c99ccf05865436b (commit)
via a33cd74d9c8c8b2d2789e98db9ad2c94ecafdafd (commit)
via b62e62a49012ae039ecace4c4d68ad196e151cc1 (commit)
via b16f3317c3fe5fab8334f8d87ab7aa905dddb0d0 (commit)
via a00b45ec9cdb590b973e5c00135f087ebdcb9aa6 (commit)
via 3d39bccaf3f0565152ef73ec3e2cd03e77572c56 (commit)
via df4d44fb1d1b127847a61a84f5cb529a7a4cd324 (commit)
via 87e43d5dfdb2b3c470e6f3c81e786b49a9a621c5 (commit)
via cffabd52cf16cb75e896abb29ee194e6e90223b9 (commit)
via 3eb5c11e8b91bd09f8d7111cbeb8df74deb2ea20 (commit)
via df8447e4fa4445aaa26fcdd9c3a09e3cdd15fc1d (commit)
via bb117dc8ada64fe650b090a693af1df45f1c805f (commit)
via 99e8782450496aba1595fbee4058d0f1e5a4477d (commit)
via 478d2eda7aca5a6e7f4a0e017192854f100641b5 (commit)
via 8f0b820ed66c3dea6e9045bd8568af26cfa8d36e (commit)
via 7a6cf1357a4c85a794388475ddfcde9ff92cb6ea (commit)
via 8015b408d16d9d5e69c678a1201a9ec0649697a5 (commit)
via 29228dab743c022c07585dca775ee732a42b5571 (commit)
via a6cd22451be6684f4bebbc34d5344371afdeaa4b (commit)
via aac023f8e10f6922400400d614d0a0b141e79c53 (commit)
via 786a742bc321666586ee923cf15e2d2624ddee42 (commit)
via c7b293f631c53764081428f4723b8c30b5a85fa8 (commit)
via a3e596573657218631e0efba4432a4c07a05c593 (commit)
via e11e8c1488cf3e418fc264f5121d1ef711118264 (commit)
via 649a0f8f2fbfa14b4b9ef9d233fc3290aaf7386e (commit)
via cbdbafbef1e3eaebcf057e15afdef9e6b387e058 (commit)
via f0b494fa3a9ead7321864d5f28c81092226fabf0 (commit)
via fc5e5746a148b1194f5a4d623bb96d1a8d9f085c (commit)
via be68664ae1064cad2e9440fe89511b6166755424 (commit)
via ee4bbde1015148df78d7d5dbed07852866ba99b0 (commit)
via b54530b4539cec4476986442e72c047dddba7b48 (commit)
via 3c285e7ef1b8890caa12ea83353b2e03f2bec23e (commit)
via d165e1c0d34e34a24b4519106af41af74d92b197 (commit)
via 9643f2fc5610a68278c7a1ea7cc167991467122e (commit)
via bc799623e178c6d3e979df3630b8dc65c92fbb62 (commit)
via 105e9affbca307c53328b34302020d27435153f8 (commit)
via c9874eac89f0e49c56c3911446e2e2d0f6d48657 (commit)
via fb7ab395e160cdaefa9ecffb4a70343d24cb80b6 (commit)
via 7d4a5388296c225d11014d6e123cf0ee2cc8eb9f (commit)
via c706b28ae11ad17fde72a2f06a2cdf17f62a5514 (commit)
via 762f2fe1566c467eab8d48444f3d0c0c388d9e9b (commit)
via 35f65cf6f3644e5bc97d058ceffe87b2253305be (commit)
via f8d312a623932ae279f24e6ec9587696ab2418a1 (commit)
via c5c64939c80687faa5692fef51c8435209736d55 (commit)
via c183ac1349ddc9f4d76d1426cbdcd80028cb43ed (commit)
via 988bb28c2391905a35e6f0c9113f78feb8f8bc97 (commit)
via 979f2981bc3518e349e036f92f5058aac2b1faf8 (commit)
via 39a853b0f561e12604fcb31a2b3746d5d577071a (commit)
via c845a96005b28acde2d62a93ee8c046162da4cb4 (commit)
via 456a43775ebd7982ba98f57286702c420f983cf5 (commit)
via 5c56a768045e78576cc71346a5cf4de9f2a35a13 (commit)
via e21cb1dba24a85e7afea480562d2b7277050e0dc (commit)
via 0f845ed94f462dee85b67f056656b2a197878b04 (commit)
via 4f96d3ab2e65356e9cb778ba140a9b8793f8e879 (commit)
via 77e3dfce7c96bc924de4f1ca7060287c147e53e5 (commit)
via 724e39ef6eaba76229a8208d2bf33449f16f3c01 (commit)
via 941c1d8bfc0468b585039c92eba3165e20f6d08d (commit)
via 40471d97b2dbe22189147f2f46f5956c587fa34f (commit)
via fe2331e7e0cadd2e70dfecdb9cf5a67f6484a5bd (commit)
via 135b47d64fba42eda8cd194f257ed77efb441797 (commit)
via 7915732b2210e5540a74041eb759b13af0f6c405 (commit)
via 6f5ce2f575e16f71a88ac35ec93c07ecd254d231 (commit)
via b74b04e1944680e73163cb8f229111d7be3df2c3 (commit)
via 8525babb4efb54926675d2d1a1398638cbc6676d (commit)
via 4b0906418973479e742cf831d5acdb291384014e (commit)
via 3e90051aa1a97eb1c450ba223ef2659220e97a45 (commit)
via 0a3773e5df10cc118dfd9dab3ce35bc612ca8ae7 (commit)
via 07c566bffc7e726945d1c3fc58988c0186d5e0c0 (commit)
via 616213225fee89adc57c8ea19a2195f8273dc5fa (commit)
via 6f304d9ee4e85db2e3aa708ee31135c4e42ef347 (commit)
via d8183b83d92016696106a6665fde369fa7d49779 (commit)
via 9cccfbbc4e13274cc1c5750704f03fbc577189ef (commit)
via 61363a11804b7838e848b8f07cba13dce80e078f (commit)
via f1c0d7070d9a3968ff900663bffcba20e53851b0 (commit)
via feebb25811b38ebfee97c2a11a1470f7aaaf6696 (commit)
via b05bfd741c5b4ec90ada47017af8a299f8a8b4d6 (commit)
via 589f61f8a630643bdee4d8bb2be68a79fba64112 (commit)
via be65cfba939a6a7abd3c93931ce35c33d3e8247b (commit)
via 770c05222d92752594605e1fd16e9527e1d079cb (commit)
via 76aa17ae5f1b89efac30211da457196d79ab49e4 (commit)
via f4dcd7d3757902c90bfa3e7b7db1600c624ea510 (commit)
via a1df84adccebfc6e3f6625aef58f1c9f31cd3eea (commit)
via 946804f3beabb2367474b15bcb173c1fc53265d6 (commit)
via 4ebc8cf417703e5350d68ccee409a8f1f9daef4e (commit)
via 831bd66ffc6def8d0db2c0ab0761bfe0ef0259f4 (commit)
via d71ab2e5570d20ec6259b764472687c81009be24 (commit)
via f5e72732b725a2e20eabe2846a85ac8abb499f76 (commit)
via 8c1e879aeb6043f2a26ba83a07b5a3ee87ae1855 (commit)
via 17a2fab7ec8ccc6886e887f9d9bff9e51fe080a9 (commit)
via f48a3bff3fbbd15584d788a264d5966154394f04 (commit)
via 4084c5f2a9a24009d645def53697b139bf989124 (commit)
via 89d6be56eefa1ed79d9c0cfb8112243e1435beb6 (commit)
via 088a0124e731718b6e8be8092a47a087436ec1dc (commit)
via 6b65ea8a618d32efcdc0697cf0acc534b6f13404 (commit)
via 0d4abba272e2e77132b374a657aa96f65505f26c (commit)
via ca5c18086bcc1fb712a33c0f4f96c27cb76348cd (commit)
via 92ebabdbcf6168666b03d7f7fbb31f899be39322 (commit)
via d803eeaf3f71b0f68d26fde29802adf2a55fa31d (commit)
via d7c133c2e26b0174c96ef48781352feec5f9e03b (commit)
via 949a25220c81c7bd4faed53e0434446c18171ec9 (commit)
via 8b1aa00503272f26a9f0a0271cdea93aab3b3295 (commit)
via 473c1e672ab88928d1f9bda61a1c2690393c018b (commit)
via da1318c5b12541c6a101408e49108b41c2d4aff1 (commit)
via a765638850588cc8980c622351124f1319c46365 (commit)
via bf471e6c6228edca829deb48f810944ea650a19b (commit)
via 0657510f32552e1d2510ec34e807d41c1a0f964e (commit)
via 5b097c33037c8891a50d77f71ee0518e9c7c955a (commit)
via 18dd133a37d37df043d36008286bb769838617be (commit)
via f683ad20d4a1936679ebfbc3f7eabedee1ff5182 (commit)
via 1f09795960479dcbe6e53ae9ead9f969c0705107 (commit)
via cd9d3e1a07cba7cabf1118ffec7f7dacebc0ecfe (commit)
via 9c54e830d9f3b18232de8304edc490f08ceb4f0e (commit)
via 27a4c2199989c2802b0a5002000c32e6c4e00220 (commit)
via e61c29836e74f2fa7b0767b44d274ee36e8dd6ea (commit)
via 7c6999a010297cde490304a97570aa48e88b2099 (commit)
via b3d42b39d85fbd34e67c00c35ae87f823a311373 (commit)
via 22f0a1d193780fdec50bb92a041ec12c8b28ca73 (commit)
via 39f2d9ed855810f30c8bee76c0e8bf1bb7fdeea4 (commit)
via 95701a2c5bed358c1eb3f2e8d2424b9a88d8612e (commit)
via 049d11fa1b6b08e8ad866686e57726e5906ced1c (commit)
via ecd9691c1791c973b9afabe17b95c108596be0f2 (commit)
via 2cbcb533f792251d19339c8b38ac7b87db94b28a (commit)
via 02141f2524808a5ccf1ac9b620fc702e37607e31 (commit)
via 07074c5295156356a0608fbaf8d706f5eaaaaef8 (commit)
via a076d799d926d23e67f495df40812cead7425aa4 (commit)
via 01bb436c45f4d92a3575ff4ad2f5c1cb348ce399 (commit)
via a8a8909b511278b2abe1679a3c801bd1fd1ece74 (commit)
via 15471a94d7cd89204b1a7de89398255662590458 (commit)
via ca9233574b5bea413b38916a0e3593ff8c581b1c (commit)
via 5b10e071de6d724d42e2f6eba4cc78bb7fbacd23 (commit)
via 1626b91e6d458b0dfe8fb71434b52273edaeb795 (commit)
via 0f4c53e60316f5b7006d96627d6236645285fe8c (commit)
via 272122f27fdf10a479c94e6270326f6ab028939f (commit)
via d3c480a5ef3803787564cb8f1bbd92df4b7185d9 (commit)
via 3c1f096e54e42bb79fec8a28abc6528b3c5fc857 (commit)
via 8e13f44e871bde3f44ffc080a1996149d61fc13d (commit)
via 56393640a8c2d3b11b549f1c1133991452c7a088 (commit)
via a6c4e80483303e6fb4deabb586a357987b79b194 (commit)
via 0f8f7995fa2fd6f340d405305783a97b34307ca9 (commit)
via 87ba16e0b1db1a831e394650c7713b028a7d42c8 (commit)
via d6de376f97313ba40fef989e4a437d184fdf70cc (commit)
via 14018a4e7e240d9770dfd565284abdf2f82f9b30 (commit)
via 2221a1de37ddc5c35e52ad0b8b99d7dec430b00c (commit)
via 0d8bee5c0cc3cf65a0a14890d7ff76de328e4316 (commit)
via 2aa3e8e3ad750691dc97badcac032ef1ca92737f (commit)
via 661dae35e38a4b4a7d3f22b785509ceb528003e1 (commit)
via 7276a2eddfc2fefb354a4848aa4376b9dbeb675e (commit)
via 3194a2019cc116c9c668cf4adbf879e06b927879 (commit)
via 7cd60567cc43fe83a74a7799373e5e6410d435f3 (commit)
via d6c9e292ff83ad12b82b8aa73e59ae05d23ee65a (commit)
via 617aa41e35a33420f00dae8bb9f39918208dc1a9 (commit)
via cb814de47219dbc310f277838106d5b4d7e17668 (commit)
via 228807659e177e95f4a3dbedc6978dcb8a02a2c3 (commit)
via ef523ea6235d16e0676d0263c964570134a59750 (commit)
via eb01e3dce1476254ec6720798c861e709d32c9be (commit)
via 87c3781cf964f63b044153a39b05518f72d8b80e (commit)
via 02e0859b64a98789b422bc57c457825bd991749f (commit)
via d26eac8dfc0780bd6211ff90f9d922bbe2bf7edb (commit)
via 36815d6a2501933007ae6e8f823be08a64064058 (commit)
via 37753c0dc215875538463720a5bdd68afdb4fc4e (commit)
via cca0075f103ef62c5b8413bb19a90c03d7bd4e04 (commit)
via 75df517663d7743adfa1295e87f8a77ca97a059d (commit)
via 39fa196b6b7eda249caa0501feb6d049cdfd9123 (commit)
via 98c85cb61b5bcbf948e1ea0923adcfcdca753cbb (commit)
via abad8addef410474f0bb6bf6e15831902017e579 (commit)
via 78fdf6daaf77f5e341e921bb1cb3ac7084060c75 (commit)
via 4337b26b8553e2815c61e979a3c6442b38687f8c (commit)
via 9f8d746079cad652e014d482327b6a73d5ee2f12 (commit)
via 0a5d26dfc2ee6a4a84d596cbcb39e894e7432f3c (commit)
via 465c3330313daf9f80471056dc41eb53eaaa3531 (commit)
via bb7465ef1ba3853c069c44f8f1ab6491782fcd5f (commit)
via 377e88195e5f86ad60bf02bf02dcbd202daa9fa0 (commit)
via d9b56777aaa3a4e70c9b80ecde1b69aab30cb0ee (commit)
via 7027266480db82481529f3680c506a23289ea460 (commit)
via 9517e24b3bc274e4a8f080cb15358db8017c3f44 (commit)
via 42407bf17bcaad492cfb9264dd713ad0e0c4f84c (commit)
via 2ca4520e166d0d07232d0fb9cc31587f77176960 (commit)
via 42de7b42fd0fd6653489efe5412b34365d3a4a77 (commit)
via 01f159a6b4277ccbbb76b18e1de811a0a71b80cd (commit)
via c2dfa3796558372cb69c9bdad6963835c18b3937 (commit)
via 339d60a7b7a5b83a2b9c5064af5d621e3047582f (commit)
via 54c8dec80aa3198a5f6dd7123c1ca415a85a27f8 (commit)
via 009e306698d3c8018fc2a83aa2c9cb6bd787a875 (commit)
via 6f1a5fd22d80b90a5854d4e9c03f1aed13f32bac (commit)
via 0ee5175c54020abcdcbafe078cc2e2dbb53e8dcb (commit)
via bcaf721d44f6805b6b73ab92c317597066e7f752 (commit)
via 7110eb5282cb4f028867d7da7ffdccb5fc503653 (commit)
via 62bbaa215675fbf0e1d7ca34219d5771a04eae4c (commit)
via f3d988ec344e2ef369534c86952b35d7ed0a1471 (commit)
via ffdc326d413b2eb9d95dc73f3948788ca7ece728 (commit)
via 2de17f71bab2fe5b02ca8563477441dc2805661c (commit)
via 72b06693a049e925d240971d92dd8427d3fa8f73 (commit)
via 721eff49ce716e79b898ad578e21efb96e684684 (commit)
via ed363b36987c46c37452c3e83b9efce0a5117cf1 (commit)
via 8c65dc3a4cc369eab2235b0ebc6d56dae4589c5a (commit)
via fd70aa81aeebbb2768d89185d1859520cd535b58 (commit)
via dd01c785f0d8eb616ee4179299dc53e0d2c0015c (commit)
via 82c997a72890a12af135ace5b9ee100e41c5534e (commit)
via 0d98fbf5f919f2ca5a5134bd6d3527bed6606d10 (commit)
via 4ae6b8af324d8e7bcd13f3e6ce5a632f3d704ccf (commit)
via 5a67a8982baa1fd6b796c063eeb13850c633702c (commit)
via 67a39b06a802b161dd6c4c93892bf3309d30d892 (commit)
via 3430cf9ae922d5af87fef9ca4c202db482f29391 (commit)
via e746e58a9fec019dd8a944baeed75c685a6cdf28 (commit)
via 196c9f80b51bb0c4d9e9f6540fc0c5258b69cd06 (commit)
via 7f4bf2e2f9b9eaf3410bf22f918478cf4a5f0ec2 (commit)
via e45f325ec7438e0ecd09d37a7d938348232b994d (commit)
via ec4df20fd7827f622fd9aecf21faae072337b32d (commit)
via 4ef6830ed357ceb859ebb3e5e821a064bd8797bb (commit)
via 016c6b97353dd57158d1da0f574912f2823e74e1 (commit)
via d406f5702264d689e6c1b7dd6c285b7d5ab06db0 (commit)
via 682cac0da7b318a81746d93424a638faa867ee7c (commit)
via cc508f6c28f0fa48720d1d78dc89bc4c287e207d (commit)
via bfcf73f7b86036c774b90e1df948e12399269385 (commit)
via a8a6595db650be70a46d4cd78f9cf8202b2c74a1 (commit)
via 5b7b932c171d48009f722ea20ecdbe852ad2b8aa (commit)
via bc25129780f807429a8c9f4cec569ed35f6b066c (commit)
via 58ad4fd28118d01c5207fcb00422f3a88e50cfab (commit)
via 48aa711b39c5c57d569d6738257ed9e5b4d34995 (commit)
via 644a03f0da719b01456ea0115cc0bd661ff9e636 (commit)
via 89a447a134798d2d59ae7a5565a9f07271ed6e9e (commit)
via 292f28a54f954f44b4771f359c9a3cc4753da22a (commit)
via 1bbc1906476bee5320eee4acb954af7b47c2b2d6 (commit)
via af62ea00182e18f2bfa448870ca37490eb830f09 (commit)
via a84352bebaacb3f414e5a01b1b25a3c7d3e50bbf (commit)
via d4fba54ddee752b447164bbed089735f2999f28b (commit)
via fd38783ce713c1ee718805cd5cc7bd33b07dbc4c (commit)
via d474c8d06214a2ef3dfc991dbd7b73dbfa6572b8 (commit)
via e7855781ad5d287d91c883143bbe2552bcc29f8d (commit)
via 59c4a138d18a6fb2a0adc0daff488406b6a6ad09 (commit)
via af0db15b1727fb986a5a08819624b231f97999e8 (commit)
via 3401846345cc6654eec63649e6bf1c71261b8b4f (commit)
via e68217dd349bb078aae6029b95e182ed165e7bcf (commit)
via c6e820b3078f39bfc19c40afb1c0294c15c16707 (commit)
via 3506d2445092477473a9f15134b8de39dd91aa4d (commit)
via dbabac08932ffda4307f7176be732b8aea655e41 (commit)
via 4bebf8b48d3e6b9b9b1b50618671e772b7e7ebab (commit)
via 29748e7583d0646b4d668c9b70c8c07af49adf98 (commit)
via e1b4215d69747ee068893e167e96428dfdbab1ce (commit)
via 30e7a2c7499274f1feb8a02fe37ab35144c20689 (commit)
via 0202fc5aeaa4e339d55c93eeaf83cdfaafda559a (commit)
via b9c531514beef9d0ee0d706f2850a30ba20c5cd1 (commit)
via 19c93b0845947d0bb5ff60c602d5fd3f64c531f2 (commit)
via 18f8355aae2aabb011a57176ec7381b656e52260 (commit)
via 4d6b150d0605cec1086f05c7557c7990e4524188 (commit)
via bdeeb21d2f97586d4bf754b4a1156412810ef441 (commit)
via 8df3463968ed8d79144303a9ec82accc7f7302db (commit)
via e29189407016909a302b2c7d4e9fb77f2ffb50e2 (commit)
via 894180b8bfb30d7dfd7619cccffd0a2b4c3d4a2f (commit)
via a06a52e6609159bb97b49169192b19d9657aceff (commit)
via 4c9ef2963127e8d887a2ca38435e61a28f1c7c63 (commit)
via 43ea555f6ae497ec40672b951d8a00c437b89c0a (commit)
via 04e36988a16fb36ab84ec548a174b148924ed0d7 (commit)
via d2cf6a85554d5d9e02f1073bf41c61206a82effc (commit)
via 32044b839f83bba409fad29df68e94247e7a94a0 (commit)
via 6ad2bdd472dd5ef1156577667c09bc0bd90d3f39 (commit)
via 7076a02b24e63b0fa542af760585e6ad95cc421f (commit)
via 73656261a319aad29c3b9c405e8a7a6d1b9445f6 (commit)
via 64b9c9084608a24a11c4acd75bb2bdd1b231cdcc (commit)
via d0d6ad3146dcecd50b276f26ce4ebaa3e6fa84a2 (commit)
via 43e5ea02d51f60045318fde277b77d3603f5194f (commit)
via c0153581c3533ef045a92e68e0464aab00947cbb (commit)
via 962c4c299ee7fe733267e7ffdb1cf3f6ff3ec394 (commit)
via 0583652b5a391ec078361254b6d473fa8ffdb4a2 (commit)
via 79154442df1acbc7b00e57091b94a97086134a71 (commit)
via 81ddf65784bb1089f4c6761f018ed1ada5a45ad2 (commit)
via 675e72e818b0a0e61ee8e4c62611ce41e1b5fd98 (commit)
via 6cc73bde44fc16387788cf53906d1c9ecb4fd947 (commit)
via 2cf3214c48f9825347dad3b3bca6e6e6d8b8b6e2 (commit)
via b61e894db16aa458c9d3d405210c357e1fdde657 (commit)
via dc342c8d7ff4eff388bc596e625e54ac9617c09c (commit)
via 326b3dc5b7383a9357dfe2f9d75e1e05b780153e (commit)
via ca87835d1ec6e3393a98dcad72b6c96d43331e39 (commit)
via 497bf6025e70ca767c703dc192a11d968c0c031d (commit)
via 5d8b4032ea2c69f3487d5af8627350e9e35e619a (commit)
via ca3f4ecf2c5dc37dab7d9e89f4bc41ee000f12c1 (commit)
via fcea4de8a92427f8b0910048ecb3c7c420d6daf9 (commit)
via 8288bf44eb68ee5f7f098beeff11e9372065b757 (commit)
via 6d1f4e151097981b21acb32175756afadf432fe6 (commit)
via d3c2b9a2fde279369ebac19e23153963fa855d1b (commit)
via 313bac393cc4a097be0bdeaac4e21bbef1ba0aa2 (commit)
via 4d0494f1618492afa57a70d036d63a6ac67ee894 (commit)
via 3945e7047b3e94919601d9d4bfca36e54310c43c (commit)
via f623694374d70db2e5cae9f78b44fc74d4580f9e (commit)
via cf1828a29fe1afd7382ebf41417b98c027ac5bfd (commit)
via 4ab333e8a81f1d83d03a1f78100823293ff3415b (commit)
via e6bd8c1c59c87dbbed3160350e063031839d6425 (commit)
via 680e748149d99afa27cf4a5a7de83d1652db06f9 (commit)
via 2ae1f990955e7a2b5aabef0ec93448c6c7e8c89f (commit)
via 338cfe287476bdbb2867cf3e745738435a0e2462 (commit)
via 23059244dc6933f7eed78bf46b6f7f8233301618 (commit)
via 5c03ce929bbda1f0fb271b35b5b3dbe1a12696c9 (commit)
via 6d40ecfc230408137edcc43f2653c70fe12589d6 (commit)
via ed2fb666188ea2c005b36d767bb7bb2861178dcc (commit)
via f33bdd59c6a8c8ea883f11578b463277d01c2b70 (commit)
via a3e8b0ad37ebb999e29c390332045bc9babfdc3c (commit)
via 45e404c1b58487d180e91d4b4eb0f5059902b190 (commit)
via c04fb71fa44c2a458aac57ae54eeb1711c017a49 (commit)
via e773dac499b952107dfc5e34290ec421b8bbf7c6 (commit)
via a50149705d6fe5e9b50ab963560ae9eee7458c45 (commit)
via 5e61d601d9424aac561c0661061e19188ed8d93f (commit)
via 57a25a015e23dca348c1e1a3c5821d64981277b1 (commit)
via 3ce8c82c2a3ef441510d2c7b0765169c2177f358 (commit)
via d1c359da0db07875b967e616b07e5d56bcc51588 (commit)
via 41d5b2ead11a334e89a7f04ab38571e2795083d0 (commit)
via d432cd1889063c53a6d5249be9bf9c6029736364 (commit)
via 6917e1335e35a4465b9e242a6c3e9b38c909d201 (commit)
via 2e7cd3c5a4935c2957d800c9e9f29e191e9bcece (commit)
via 0fc40f1677c6bdaf1e2920c6f86b2bc6c15b4cb0 (commit)
via 1a0742f1fd61234a6ec395729b0c4c4cad64aaac (commit)
via e7c52fec35a1f6a49351f58877d21fd43b4082e0 (commit)
via eac5e751473e238dee1ebf16491634a1fbea25e2 (commit)
via 351b82d0ed7d964803be52b433706377d7b2f780 (commit)
via e42db3d2834457c04e19c256a67f4059cd36860d (commit)
via 971e77005e139347d5d4fedf0cc87bfb26b2685f (commit)
via 6e99f2cb85dd4722f7a5c2d15ebf2458cecc7b32 (commit)
via 18b3c00bb06e51f6a3b16fa634983a43f65b8e97 (commit)
via 79fe7149c3d2cc20d4bef791d27d5316702de53b (commit)
via fa4e16aaa9ee6e680606f0211c71c92669b8e74f (commit)
via 61dcb03fbe2c9a4e3df6e8afe269c0d94f00d925 (commit)
via 60a57d2997cfd0bfc27d580df175b31589fdedbf (commit)
via bf1529f69378453d8fb0b75cabb5d560e278d1c3 (commit)
via ed780cbba41428341c5956ff5b032d829d059cab (commit)
via 58f2a7f08242fad52c397bbf7681b209dc84c896 (commit)
via 407655388fe055d2a8a950ca31e36a9102b59682 (commit)
via 41e40c5cb4a62d32889fbbaf704792add24b02d2 (commit)
via 1949be3727367e2b4f9a399f3678a8fa1e969bc1 (commit)
via ee58a132fe6313d7d2d9ecc7eba8b455975ebac7 (commit)
via e15f18aff64c8f7481546e877f059290653b9926 (commit)
via 6916c32a3f5f409a01157d1957ebd3c9e296ed99 (commit)
via ee90ee28b431aac9b513330e31032a20d2c0ac27 (commit)
via c3ef6db1a2b58b3732f1d2aa169127ade79fcb77 (commit)
via 5fe6fd9977984ddc71d71266afb41f963012ddf9 (commit)
via a4ff0d97db3b04c67d3bdd246c04d1dc4c47fb4d (commit)
via 4afd4b896c591245f51b992296039ffd5b3991b5 (commit)
via 489316fd7418436e94de387ce9477c18e2b4da59 (commit)
via 8b4019ff1577d1d32e9400c2baba77c948d0bfe4 (commit)
via 603b0180bc4c1e9b7501c9e3292cad6f3427bf65 (commit)
via ac446f02802bff0a91af4caa6e6717b8f0ff350c (commit)
via dc6150ca4dd1d7dc44bcadb34acecb2d9176a9eb (commit)
via c4004066611a83ea5589af57bf07172da5a4aa94 (commit)
via aa0265f874883431407a6ed6a2f0d543fc4e8507 (commit)
via d8991bf8ed720a316f7506c1dd9db7de5c57ad4d (commit)
via 95d6573794927ff46242625736d894ca4758f55a (commit)
via 464664e3237b5dcb61deaf9ba4b5224bb11f412e (commit)
via d05d7aa36d0f8f87b94dba114134b50ca37eabff (commit)
via cef64b540e2353c080a8938ccf1353a11eced58a (commit)
via 4507063fc988329516dccad11ff28936aac8a08e (commit)
via 1095506737bb6cc7cf8b0c00ea0803ffcbc98ad5 (commit)
via 0d3ac3452be35e57346715a296e05deb48a03ccd (commit)
via d878168fcd942b66b3f88ffd162010dd86ab85a1 (commit)
via 8354cca4763eceb0dfd3270823012511c14512cc (commit)
via c6b3ce6d16f92b80db77487900fb57a7a67b3203 (commit)
via 454dbb49604074ec1ee9136598b3ef0ccf3af256 (commit)
via 20b0efb43cff8020da949df03ce13d58ee6e4eac (commit)
via 9785efa0afab6667a61d322f9a3a3dee048dd0dc (commit)
via 7f644c2532ecf0f3dab826a091fbe900dc5352f4 (commit)
via 5b0a2957d3a806ce84ad86dd7789edf3ef495d92 (commit)
via df2ee17ce15c5259ec2632ebf536f9f117419c88 (commit)
via 2ba18911d1daf85d52fea1e380f9425b7e8c3101 (commit)
via a81a52389a3ce477db992da27cf09cb1adc0e49e (commit)
via 6d42b333f446eccc9d0204bcc04df38fed0c31db (commit)
via b4f1cd645efbda886e123943b5e14222b40e7a9c (commit)
via 204733844d2984ed49eb8e2da89666f6a1729732 (commit)
via d399683b0d058126c2889d090b6067dcac0de859 (commit)
via b178f2a38b94200e35758f6e8d7e570a0cfc41ec (commit)
via aa6d3a22e29241cb6b8efb228821a99fd0b26240 (commit)
via c18cb310851ea2f558fd2b6103c9f5badd4cd337 (commit)
via 6e42b90971b377261c72d51c38bf4a8dc336664a (commit)
via 382705e83e65a261d1dc3b98407226c1340bc90d (commit)
via fcbb1a926c62a986d97566002bf318822161a5e4 (commit)
via f1c399008dea27eed1200e392afd08929c5f9f96 (commit)
via 213d905ed8661b9d1f5ce214c6f391e702d518d7 (commit)
via 334cb51d057d58fc24049e8605a136096be8dc3f (commit)
via d2846e4ecf8679ec53b2cd16e543b7b2488f2692 (commit)
via ef242ca52d93c62bfc2ef42260ebf5069d31cde5 (commit)
via 5e4b2f06b8dfe4cf1eb7f26836942d10e3c719e4 (commit)
via 2a26b3fed052b62aaa2064faa7e9ce9196bbceca (commit)
via c5b11e54a93c528d549a66206f0ff2c2ddf4273d (commit)
via 5eea9d893ec4ac21e89f8b3b0f3e68af625eed48 (commit)
via 5d9916e93986dbeb890cbce2782d67b9f535adb4 (commit)
via cdb34f7debacbbbc4adec956f682c07e0a77d706 (commit)
via 5482fb745b8fc0f3a3f80b25416752682c345211 (commit)
via f999dcb8704206c2cc1ea09ff9a4ca76a9e5affd (commit)
via 6d1d496ef090cfc188842a1fd1f9d0ca1b695826 (commit)
via a63b3077dd102512b9002e204a955f5b8d783ff4 (commit)
via 16156f814b54dffdc0304016aa41b0746c613d3a (commit)
via 361853d37c33abd413b48f9bc1c80578840aa901 (commit)
via 328ab84c1ddd6cf3f7e45a1b6fb6e1552bb26f68 (commit)
via b4b1eca7131cedca72a8973718881c1f18d2010f (commit)
via 5e8a8cc152ef5cd151f99389f475b2a16832fb9c (commit)
via d63e340ac318b68e1a04458ba9669f159d6eb508 (commit)
via 1954874770a322999e04315397c25a56cd1f1b3a (commit)
via 570fc14873220c13f62e622fd3f227a73ae9a106 (commit)
via c77a043a8a6f6216499cc9697d5881c65bd9ad2d (commit)
via 58875c2089c61e5174db855530e9de237920c669 (commit)
via 2bde65105b86a512aa798cac5bcad723bafb38c9 (commit)
via 418f35f58458de0480d6a58299f06721481d9196 (commit)
via 010bda2e96f5763e6eec226ac1cee96bce2588e5 (commit)
via ba91cff481836c5c8c1993174727646213cea60b (commit)
via d72ba709f11dee0d0bb90868fc042bd8caaf8626 (commit)
via b4bf719aa55ad684dbfa5faa39aa93ebf89bac18 (commit)
via befc536e55419f950791c6b32398ed6416d1abdd (commit)
via fac95b174e553ac8eba5805c47541530e9fbb2e1 (commit)
via dd35de3a43c6aec1cb67135da6195e8eabf028e8 (commit)
via 0af5b54f1c40328d94b053bbac09811bbdf3b116 (commit)
via 1e82366eb6afcaf56fefd13f3158487f02935e13 (commit)
via 7341eefceabf675af89a4514b89f0a45ba4a711d (commit)
via 29827fea890f2657705256b70bd38e6295d37588 (commit)
via b99e16aa62aae9000905822f4a76bec8fff3591a (commit)
via 7a55d6839c96bc41cb440e30822e219911ed2ea1 (commit)
via df2c166741be35284c9a7711b28513fa0f5a128c (commit)
via ee27d69e45c9632a482d610b28268f22af85ab5f (commit)
via de12157bbf027dd4bdf90201a4699c1a7287a819 (commit)
via 3b778c9a251f470985f330efa1a314869f5cca1d (commit)
via edf1ae8f84a9bd9ff5725c80c9f61872c8658fc3 (commit)
via 49ee4eaf84ad72a044cf4957af016d1e8e15cab0 (commit)
via 87fc0368a47156638c189fbf071471ee0d9c4463 (commit)
via eff9543714f70df752ab02db33b7a7b579b6a225 (commit)
via db3f2d81e975c2c82e575ba2947340f520f763a9 (commit)
via 2b6d8c99eedfc5e77477461963bff7ae031192bf (commit)
via cc8e7feaf34f1028438854453b4811d7283852c0 (commit)
via 8b7ae5faff5a12fb2841e8b35b43253c0d67c22f (commit)
via b021224a6314396eccfb9c7e0af223474079e864 (commit)
via 18155a69d96d21d845f990010d4e6fdf9589fd6a (commit)
via d1da93e647dfe42cebf516f34d1e5eb732f9cfc5 (commit)
via 5f57054ae173dcbf802f1d7c03a485d7d72e1814 (commit)
via 9a3ddf1e2bfa2546bfcc7df6d9b11bfbdb5cf35f (commit)
via 7c20771f4296820b7efc22b19a0783e7d226736a (commit)
via ae6f706d85ae90ca971d2cfef2abfe7e5bc7856d (commit)
via 9a306b863a628351a84cfafdabe5700d4840be6a (commit)
via 42e6736faa21f98de53a4ac0bc52306041581d91 (commit)
via dc50d73a4980ad1900e0445a1ea0dd46bf37245c (commit)
via 348d7dcb7f31831e5db9c282c213aec0795d1ff2 (commit)
via 19e016eb1b80db43645aca734b1f511ff2b142fb (commit)
via c730f9ab1b00b3611d82d0c00a2f0faf56b8162a (commit)
via 253803efbad0e147f37f6ce7a07949bbb79909f5 (commit)
via 9df9a672eb672bd201e3762f3f321082e7e1d17c (commit)
via ced15db4ec415ce080457136acebf66bebd7bf7d (commit)
via db2f7c545136ec475f2afda44ff182b195e2ed28 (commit)
via f57669ee937a15733a04e069aae01f3fff920f0d (commit)
via c7115f5b90dc167552fe2de1178def62a948afe6 (commit)
via 017278ca8084b9420fa94a2e9cc44aac9ebcae04 (commit)
via e61a5182aa295c0a96428d09e1d259c9b8b09d0d (commit)
via 8e94ab0a3baabd2b20f181243626168df4a56ec1 (commit)
via 390c1a763df3dbfcf0f24394464c0db89aaede9f (commit)
via 38ff6231fc72640e2d67de0583a6af1bb98a1e3d (commit)
via f3599de81907489d828748988c660d644257f0fe (commit)
via 968625c32a07347edd50550da5fcf13c17b61d87 (commit)
via ca59c5c8d5f309657b3f24f9edcc7100fd9d07a5 (commit)
via e9ac9994ee59019ec0689b94ef4d5cb15c0e5699 (commit)
via 53f6ad9e37ee9df633af7256bcda6bafd3cdc1a2 (commit)
via 32f82dfe9ca5b9a66104b0cec748a16dc648a0d0 (commit)
via 78981c6ff6b82a580786f332e58b5d3cfeed4e2c (commit)
via c25adf51ab0d73e162e520592daaa3e70ead8c15 (commit)
via e09ae1968e2c225fb42175e3962168953d4399e1 (commit)
via a57ce9dae4185a4542401a5ff6212f0bdf752488 (commit)
via 51861a16ff9aa86bda48baaca5cac0c37eb10dfc (commit)
via 0507bba4a8dcbd4a3e7f0f6ad03eb5df6068201f (commit)
via 7e343de52c027ac35f231f906cfdd19500e8b7c1 (commit)
via 46544d453c97522254fcbef13fb57c67be622cb5 (commit)
via bdf66e1261cd868e356392e478b0361fcca349b8 (commit)
via d6b484dc1a2689bf7fc8f1cfe2454ddb025b2a01 (commit)
via be6122f520871344324be1ceb9f118195d508bba (commit)
via 8cb8350f576413a4a5f0cf6fda366219ab316b89 (commit)
via 472b197fab494c18fa3ed25018176997da20c22a (commit)
via 8c4f48f99c0a25c37b15f7819aa5c01a3a1b6844 (commit)
via bd4a777c870eb78b1cdfc6df2c8e13ea93480c12 (commit)
via 5485e7248939b67d6dc94b18925cee753df14390 (commit)
via df956ae5a69124cdfd7704ef0582adfe254df802 (commit)
via 7c2793d41b23377529c667f89ca1afb69238a7a1 (commit)
via 0a4612cfe59ee2149b885550e467f4de42236bbb (commit)
via 780a3b3bd3492e500154e53e6835d82eb2eb3131 (commit)
via 5b41c700bd362d76ce19b032c75662f64b5a9ca8 (commit)
via 8f594ed3b5bcd0d29c1ddb2fcc0f91854503606e (commit)
via 3a7fba8e0764537e4782b8625b548bd199e30e28 (commit)
via 7f026c48836d9366316469190e0f82b882b228c0 (commit)
via 03db051a722775e59b803bd44e02d1faad2a0a9a (commit)
via 5b6251fb9bb2cd793fd9f86719d1e359aa427cca (commit)
via 6e6cff79121a939cc961c8f36a489cbb2d0b7074 (commit)
via 1a8db0437ffc3367de7762447cfa1f4efa83cfd7 (commit)
via ff10ffe92269cc3cdf85826fd81e2c01dd909801 (commit)
via b6f5648870e85b8451b624391ad22bcb04a219d2 (commit)
via 43fae7e83655a6bdb1e1b57654436ffaa8fad8c2 (commit)
via 007341505307a53b43802c747fe09435346bbf46 (commit)
via f57c476f33531dfdbffe892b0462338872f64870 (commit)
via 7abee20ad8cfd20c3e33f58c414798276b48484b (commit)
via 7ef14b47d3bd773155e4b6d9c5a739b0213e8f28 (commit)
via 253dc4476ac2052dcd9af515ea4955b09c91cc7d (commit)
via 741e96ebecac59ad257288094eab660661344b39 (commit)
via fc951496b21edbdb9c6ac53ab8ed596035177d54 (commit)
via 430c0c7e0c2b84bbb8d8ef5a2d27daecb5af69a9 (commit)
via d6b2905297b0ab859ed0dfcf67eaff340e7eb1dc (commit)
via 2419d18342284ebc4e7099e65f9a8ce2d3dce15c (commit)
via d2b107586db7c2deaecba212c891d231d7e54a07 (commit)
via b8679425f97da697e4a8766b773fb9d400af6d79 (commit)
via bee3ea9fabdc724d9751c073ecf6f71f7492c286 (commit)
via e5d3eeb0edd4f47af44fee8a5e7ec1b165d8db30 (commit)
via 55f676522e6df80e3293ac5c0f845805f622f3a8 (commit)
via 9368ade9509ac5a2278c52d4c36c3a84d872531e (commit)
via 77c1644d0d1dc8f3c1589138c335323b6a4e5c35 (commit)
via 45087a113407dbb6b657f6e410dcf8158219effe (commit)
via 3a874c84baf65289d49be69c041b8340c946617a (commit)
via 51c802b42eafb9d5da4c7f681051cb5fcfbd3df1 (commit)
via b037963e65fcc56bd6ffd847bfb41d172ad1af1e (commit)
via a9d957c18a7428dfa6d57e00baea49652302b4f0 (commit)
via 8780867306315f587012fd7ec9c623a0c439ffa4 (commit)
via a5c5c9e3456d94ee0465a6665dc1c2aafeb21091 (commit)
via ad2c0ba137e17bcf43b9be5a286d99696fd3009b (commit)
via 78af6ba875985a8c33eb1dd54adbe35fc720574b (commit)
via a0b8bc3685dcffca7713bea4d6d738827c045be0 (commit)
via c855ce32bf071dae01559afa743e757100d9a441 (commit)
via 11cdeec052d94483964f379d822e58eb58ffe522 (commit)
via 40d9424ef15d3c6d484a03772fa15d57781161f8 (commit)
via 7ffc29a5c462f9359d6059e6b14b33653f6da0ad (commit)
via a169232953e34690749708a29b9f6c22e94b2dbf (commit)
via 65b0c0b8a22a6f066ed6d9cae81f1de3cbca4682 (commit)
via 7775ce49e0e07406db6d7dc158b7c01060aa93dd (commit)
via 3c7a0170392994d0c6c67b40cfa8b0d902d5312f (commit)
via b63fddd9ded87b34a2f9072d118bbfaca420c045 (commit)
via 922c6e4ecb69334bdcdac0c9fa82c4615c828609 (commit)
via a2a6bcce71abf5bbd7ee1da2d375a85c297ee21e (commit)
via a9f39355b94fe20924aa02f849e35e7000c67631 (commit)
via 2cd1c10416f27e6b054c3fe4726ea771f63d0bb2 (commit)
via 2652e68f004f5c329b4979e93bbc020fb59af32d (commit)
via 421e22c7f1437c632a7e9aa904662a9344b13446 (commit)
via 0fdc68bb68b017223f35a3dc39e5255a5f255d10 (commit)
via 3a98bed98afbf27ec36a941936d1bf11c656178d (commit)
via 668a507bdb0dd52368621fe7b8aed41b796a9529 (commit)
via b14e0fb808dd161c89a094924f153393b3fc1348 (commit)
via fc735da7e6210f87e4e5a36fce91eb990b8bbbc8 (commit)
via 3a0d2b90ef3766a8a105a8a9b04c92b8620eaeb1 (commit)
via edfad4a9bb65049599adaf9cf68baedf7bc072b7 (commit)
via e93ef80d1c7e0f431f37f75c68f45c3aa78b70f7 (commit)
via 0f00c3880a0cb7498cd5adb86831f779d57d321f (commit)
via 696f1e012969f47880ecf8c5be0c5935a99e3501 (commit)
via ea778b2bd22edec0b971791ae59008328163542f (commit)
via bd74cbbdfc7828d5517089d1c9d4f2768b7bc7de (commit)
via 7448e48ddae3c48408bc95457e21d0ca189a9ea2 (commit)
via 310c6201416fe8b57c691a9bbb045d781c601fea (commit)
via 32f964bd9bf8ab5ee58b409a3006779b9a504b03 (commit)
via ea28774bc7f3586c2ef33028a88f2c51cfa9b691 (commit)
via 3d291f42cdb186682983aa833a1a67cb9e6a8434 (commit)
via 3174827557c878ee62a0624aa3ac93cc3fd37bef (commit)
via 95cb383de7f191fdd4e8796ef52b469be90e5523 (commit)
via b2f0388d5396ee32c6e8b6593130fe87e26153ae (commit)
via efe8fae1cf4f3a272c8da437113f1c7b90c9607a (commit)
via 9206de78991d2dcbbfec451e896dba0006bc5b71 (commit)
via a43f9f2860f156daa1a8caf95d6c0ff1a95323a9 (commit)
via ebe5c03858d0e41ae7e3a8477399d0f54712569a (commit)
via 1d9ea23f74983eb952733a9cacd5cdc6d99db5d4 (commit)
via a66549d4455e23d4486d9287157790ccd49a942c (commit)
via 007b1f5cd7b48bccf501aa624325388e360aa828 (commit)
via e65eaed0d786f8939852b6368de827f7294d396c (commit)
via 983f11cfdd11153b234ff7626c5ff1dccb1baeb2 (commit)
via ab3f32eee4e5eb1d696ac88392497e52b994fbbb (commit)
via 11de0085d24cdb5dfcc5a158cd30699cfde90159 (commit)
via 81847206409e7cb4bb69bda8a22f7fc54a7a6b2a (commit)
via 2ca7ef10889eccf7ebe70f1662fed83c33f62c19 (commit)
via 2987b196ad617e5cc8f759717edae50866162888 (commit)
via 554c01b75c2e1a16bebd00a2d1476b0c85f70ef3 (commit)
via 054d97ceb5c12455b1272e00a860e111f67b6ce9 (commit)
via 425062cfead674420744cee41784e4d894f9ac2d (commit)
via 0fa66c2662c0f8eac63ed44668f90d8b687859c4 (commit)
via 1b1c061696198766e9f6821e69cb1658f83c2030 (commit)
via 2379e411e9671d4ecbee71ca5a9663053a40d091 (commit)
via 94f2a0246ef707722666dc81c68cb58ac85414d4 (commit)
via 8ec2caa761eac2681485217645e7e328924c3c7f (commit)
via c27421b7e7b41247b41aca0de903f328048ab920 (commit)
via dac0b87d5b0466e3aaf0f49ec7b2362606c03415 (commit)
via e626839cc9e9a74aa7c7b560c4fc2997394f24fc (commit)
via c8050a3b7d7e3060c59f14c777af5a8ad675f234 (commit)
via dde7e27a9effb9702e99f2145254ae13821d7667 (commit)
via 638087de4edc54e2ef26fa7e444b743c433b44cd (commit)
via bd3f52daf52aeeb006cc7ac420f11bdc9cd284ed (commit)
via 157d04befe4e1b7a3a9d3fe34d2a39af5709ef1a (commit)
via 21034c5291df20ab7b9ebf79c16336af25e16594 (commit)
via 3ffa4a979da3b91b0c721d931f0d9ad333f076a8 (commit)
via 9f797b9baa3972fc391a6bfbd2a6a9bae6fcf375 (commit)
via d0080895a3a34b3e6d02c38021700e7a02245afc (commit)
via c16bf9a2b17f26c73b5be8aa9a7b4f3570b8f3c1 (commit)
via 3cc9fc1217967770551c6acb3ba8f6c6a0387864 (commit)
via 56e4d09d7b49fa6322c3ed52a2c38898d8a27046 (commit)
via fbe7def71c07f6f36af8f3d71f1bc83cb14c9e3a (commit)
via f7b60d9d56ce0447fad6f6b6ac241477535e7235 (commit)
via ed508fe48ff9a13463ef0bf800a74f7a9dec14f6 (commit)
via 5543f4c43126a4477807ab2c043907cab8f7b04b (commit)
via 6e4e55d9952dd59962c161d2fc2552176f805244 (commit)
via d1bbd4878ba48cd9ebc623b1e8666f425c86e84b (commit)
via 49305bca03037ecf7cc2f91f75c4fd1c2d8bd5d1 (commit)
via 056ca56102ee79dbc19629cdc6720d864b7a7e8d (commit)
via 797308ff2aedadc0281c8ca9c84daaa3c6d6d42c (commit)
via 74625ee6146a2b28c4caaacef2fb9c9ba9876dd6 (commit)
via dc488cb9679c44617fdac1aebb6a821bddd39736 (commit)
via e05af734b0416593a0978c3ab22bfd2ee1d991ed (commit)
via 88ada22821235de67f0d3efa32394b83d11e3b79 (commit)
via e81de23719368f0fcea05d1607c2ffafb76f0359 (commit)
via 67704c85b785065865a1b3963b73ba2dc70e5998 (commit)
via 6ce96d1b10bd6300d193df65cd17c4976c0533ce (commit)
via 9bbd16649e750e9a4c7ce5fec0265f91b10cc4fb (commit)
via 926a9b1b81f91338aa03abcbed25b962ca86d209 (commit)
via 27dcf35fa3b3d65a031df6c434343db2d5ddb8f8 (commit)
via 355bc5c22b7c83f6d24f0e4268b1eff7d1274655 (commit)
via 02c64383d4f2bd7a7c878f05df3ee2fbb4d10ce3 (commit)
via d09952e117b38c8f55a4abbb2f50ddba4874d86b (commit)
via ea8be8f804ea9d99f49d299b90ff6cb735254c32 (commit)
via 5aec5fb20b0486574226f89bd877267cb9116921 (commit)
via 567853b380a1365f0e3fe16a79062ee710f2e167 (commit)
via ab5994c2e41c297aab91789dc18896beeca96063 (commit)
via d6c6fe485c6ed9a4ed9a2efb4792870a58b33c7a (commit)
via 9d30cc97c91754a94f15ae288220401fb11cfc06 (commit)
via 15810521bfeea89e77334df66512863911a4d03c (commit)
via af91bbf6f4104b9731264270e2f1356b0ec74981 (commit)
via 171e04fdad35ccb630a4255efba73af01536f9de (commit)
via 779d5913cc017664e72d4e6008b1e7dced1a6ae4 (commit)
via 523e679dd68695f1815cfb5e0a31cc7bf4f742f6 (commit)
via 41439e23c82d769ae7e1bb04453723a8770e6c9a (commit)
via e12bb75e5e456ecaaa738936aea36fc255d342f7 (commit)
via c7c8f0af322c42e62e6f5168e554e56776230a79 (commit)
via 7035e189a7f94e8284c1da4eb538e6db24fafca4 (commit)
via c049cba60db67b2ab0beeaccb690d77bd0c63689 (commit)
via dcef553fe87fd8c1be039394ad258a20aa8cd0b0 (commit)
via dcd88c02239b954f9617f998852c4399ff1f9a75 (commit)
via 9d2bd293b53d41efc0eb27c3f7e6cff01d264c8d (commit)
via 2cba268b3ae90e44b92227ebbbfa15e0fd3bdb5b (commit)
via 7028594590cdbe96b995ffb104a79551844999ee (commit)
via 827f143ed26198b3726d0921590f8e91fae11b4c (commit)
via 45baa7aba39e666d0eed45e140567ce3be62066e (commit)
via a786fa02df431025c8419ccc7d0e8f4023e6eaba (commit)
via 25fec05f27988d1bf5dbdd87ab67ee61e6ff15f0 (commit)
via 92ebcb9a94e233362660b3d3f29df319da919286 (commit)
via 3a6139c33b7db517e76723d631ee49d47dc691dd (commit)
via 0df2936c8f985071e97fb17d9685266fc0d443a5 (commit)
via b1c398bbc0d7b7b066347cff13ea8fb310bf4f10 (commit)
via b9fefbedb20b98f3a8a39550a9a1183208eb10ea (commit)
via 777c26581b971935e9d77cc1b70f0909ab3534e6 (commit)
via 7212536c15e7374828bb74657d6f6d30cac4160d (commit)
via 347a2846eaacb84791fedcda6537d2fd8be6160f (commit)
via bc6a657a16a55350992cbe251f010d0a46d11dea (commit)
via 8b82620d477ff1eac478407debdef85286f34f79 (commit)
via 946f8117826ae2df528c76901f6e1426cdf614e0 (commit)
via a555e12ddf75657f9ba417f1bc9366c8ba094854 (commit)
via db63f2e09dfa7e2742d06dc72f7bb4d52c3442a1 (commit)
via 4ee45a9b6fe66798c2aa22ecfc6b9522b9ba8d37 (commit)
via ab1890f4594c873aca0b7f483234de5cd3e5e01c (commit)
via 71376fdfd9be70f5dc7f28fcec5bb75fbeb27ccd (commit)
via 6849529f7e974888fcbc49c303b709d7c65002af (commit)
via 984abbf9ed6f4b7fb8b0ea884fd5e3ea38469d6f (commit)
via 270d14dcd9c376ec207f61bb736eb6ef8e9b1a2d (commit)
via 226b4d1a1e9b94d5eb9eb92e6a35f24a1ea1d0bf (commit)
via a41cac582e46213c120b19928e4162535ba5fe76 (commit)
via 3511d115d6565b995ec092037359933eecb7c704 (commit)
via fae036eecd789c8815f8f52f5954835ef9fcfa82 (commit)
via efea64180e770421ac70e4a607e0b5587433b55a (commit)
via cbd8f4ddcdff128ec9f545b35151a8c64affb063 (commit)
via f61a5e73721ee68e4fb330f59a15fe5baf8fadb2 (commit)
via 3d96ea46c8191bff66e8ec93b00414f6a78a66f5 (commit)
via 358984aecfd6a7944624577f931e09c3fd53febb (commit)
via 86d1383a1a5998ce4fa7cac17b9f15bde7d19712 (commit)
via 38beb040c82df7d6502f3f25b52773aeecad6041 (commit)
via c5352f69e7fe7b9071066b4ce40d47c2d7948a1f (commit)
via 71eec22b3e41413651107079d9f9b5cf34d2c7ca (commit)
via 9b1ceb841266b4df04ac717186c86b22f77fe314 (commit)
via b18071c40b073c7f38643dfe19c7d10f2127abfc (commit)
via fe14d0cfc32360547afc55ceded8c69123a70214 (commit)
via faffa474e24d5e3bedc06088a388a0965d5059af (commit)
via 9163e6f7a614dddc6d1cbb7b9e879b802fda14ce (commit)
via 58335cecf1afa60e325b5ce952b23b40416bd4b9 (commit)
via 632a5596298f9d253693631606e026e01d8e7fdd (commit)
via 1ceeb74c1eba0ce65ea05a55fc86149d3fc733f8 (commit)
via 31a77cdd4fb12c30e9222e9e045fb998487b0844 (commit)
via 8191aec04c5279c199909f00f0a0b2b8f7bede94 (commit)
via 35c2010ce56c5fafebad8574184d083d1182534d (commit)
via 19030355897c6185f75193f299c8666b595970ae (commit)
via b3befd1fe7fed482817a7ccf05baa6292fd7a56a (commit)
via 031ae7d0a205b4a82593ba4767e8e4741b3f8081 (commit)
via 9a2922daaac4af5b378144b518a1b1fec767c798 (commit)
via f49e54056a5485a18ab7181256233c3af7f1a73f (commit)
via 2ff30d5d4b5fec6fab22c0ef4021bbc85c9250e2 (commit)
via 6f11ce8d8e06bababd2f706e2affc915e3ed1e02 (commit)
via 99d9be31ae3323ee9f539b01bc74f3797d968e3b (commit)
via c68b2f25695d5fc4c64d8a45a7414695a8281c53 (commit)
via 71de47f8d14d274ae2223e56423d6022a3cf0369 (commit)
via 5cc7f89d8f2fc1ebb563556fa7e250ceb3d4aedb (commit)
via bb08c0bcb4791c3c39f68ae8cefc2e9402ae9b92 (commit)
via d2460249eb2a7340b8adbdf12306744ba255eb6d (commit)
via 003aa4bedf13cd9d7b2e73983aa593deb6281435 (commit)
via 21f3ea90672b5bf84886e58f08f97bbcc63dfd6d (commit)
via 2935657cc5e260f54be045b28d7184adb406d3d7 (commit)
via 77010a2090eb1cade512f03e880df4cca13e9eb4 (commit)
via d340d8278228a062664eda13926f5af3daef8b35 (commit)
via f26eae2533b0b2c6591ad6aa9116dc9ba00c462a (commit)
via 70c715efff4255744322403c03a7be1e64d99c5e (commit)
via 6a4ee80ea2995124b737bd718f16f1baadb5c7dd (commit)
via 508e3554d5167a27ed1287a3903f851bfaf11a17 (commit)
via b881b03e1c8138fc7385ee3f98a34ef3a1489bb0 (commit)
via 95af51c86a9d480562526421863e47df7260e6a3 (commit)
via 2fc57fd693aa0f849d91f39881006099769c6026 (commit)
via ca04c553327800d4a5719dfc1b1343927a9ffa13 (commit)
via 09e6babe2643d0222b4ff99517834cdb07d80aae (commit)
via b02f05774d65cac0b87452dfea6a65be73e08069 (commit)
via 1ce2bb2f000d2a11a2e1e299e62ee567d81c8684 (commit)
via 3bd3919f267d320eec044a8fb8d2aed62f176e08 (commit)
via b33bcfe2c8c1b651f5da66e03ea7fdbf1f499c36 (commit)
via c269cdc11c02affd7b191a4aabcc56bf6894ef3a (commit)
via 24d49c3c491cf82fb5e0134d28e8eecc2428851e (commit)
via 547094b71f9306e1130926ccb76160141e2b080e (commit)
via 98bcbd53342827be7a5be66f8ba69f7e37559c2c (commit)
via d97165b66927e4a58979db9558e2b2f37fea17e9 (commit)
via 9b95e643da6c3b1be36b0700e8198b9fec870515 (commit)
via 53d462aedda7e6f2527dff34a6da25ef58ec298b (commit)
via 1867eb5f882da2cc6cb5c8be897d9affcf125e28 (commit)
via f24dff697d824255a76586d0ff879e8445087d08 (commit)
via 429d200e948f44dd4d7f668c2f8d46883f576205 (commit)
via 4ca40253f70421e0133e2bcc18dde133b4851f67 (commit)
via 525021e0a8f3c00757b66588208bc10528544416 (commit)
via e51d6fd38a9ecdb6d8ff200fc152aaf5bf1e0cf5 (commit)
via edc1911c2dd4b941d4658aa0e677f4a8ef4ce819 (commit)
via 1c5051938f58da3dda1b327d373b4df7f09ca7de (commit)
via 7a0ffbd7818d721d06b1864fe840f89be6f65b8b (commit)
via 43c9101c5e4a721fde8f6699dde7f45f12b80823 (commit)
via dd34e731a3998c656995b178988e12cf0bd5c2b7 (commit)
via 5da8f8131b1224c99603852e1574b2a1adace236 (commit)
via 77def77e9ab95155f91dcd9506de0f8ea1001d8a (commit)
via 61a6cfae29c657daedd334fc2a157d57e85948a3 (commit)
via c188affb416fc02d45a0932fec1e788303425d88 (commit)
via 64dec90474339bab08323f8b76cde47ddd2d8f73 (commit)
via ab78760e3dacab5fe2d4683fe991c2643fdf9877 (commit)
via b6925301471d003931305efa8e39c5704e1413c5 (commit)
via e58e863991709132601c263b8c04ea4e5a0593c5 (commit)
via 4bed4a498354aab3faf234bc76b3f0fbc5a94fe3 (commit)
via 156c5f32bb38cdb94361726fe4f0d40adf39bbdd (commit)
via 587f61464634214c7cdbbb204652eb63b11eea3b (commit)
via 1ee8b5efc468540d5542b300bb01bd0ae50fb339 (commit)
via 67b935d7e7bfd22d0b7341c8e81908c9b5dd19d3 (commit)
via b222f7faadc7ed7898a936a2b97b2a385c6a2a1b (commit)
via 159d0dd5e07877315eaa9690cfe141f3dac1f1c6 (commit)
via 6ebe6bc5875d21ad1d822c251462cebe3e769aee (commit)
via 6bcd96718c01bdbe0d563f6c67c46b441147ba60 (commit)
via c1d7e3f9bfa473478a1070467bf0773f02e54b6e (commit)
via a8186631e5cd811b2ebf8df6d837182ea39177ea (commit)
via 42213653b829ee68eae11af916dbd2164c96d27f (commit)
via 2ed196723ce382b98667029ae24dae42361197df (commit)
via f62ca4a0b64ab0d417063105458f409b4ac027d0 (commit)
via 8b143c88e7413b43086ca538e84af945f7754447 (commit)
via 36cb12b086ee2a1e42f106ce9677eae708532933 (commit)
via 8118f8e4e9c0ad3e7b690bbce265a163e4f8767a (commit)
via 1f72551a31350aebe5709f011f58e2e0ce5b524d (commit)
via 5e08967f518e716ecd929249f7366a2d7faf6306 (commit)
via cd6f223e8f3da567fc320ca4b65948eae9e806ef (commit)
via 946d6e879a77f482a8887d47deb797990dd66544 (commit)
via 09c5415d6a9db4161f280bf8172d21fd796af068 (commit)
via 717f7ead4cfc365fac6c8d95aa79f835885b5f90 (commit)
via 397c814090c29f55eafc869702d00e0c15bac0fd (commit)
via 68e2c05b1f696136e683765c4d40170a45f721b5 (commit)
via 7917c9005e71b17be80a4ff27e21858d82a53e6b (commit)
via e3a4ca39f16eaaf8107d688c03f878ef1cbb68b9 (commit)
via 0332ecca45c768907451a893663f526139ffc2e2 (commit)
via 8c0de3883e79cab57fb1dbabd56b8f7515823288 (commit)
via 9c25723eb98513953e93a16a5bb5b83cdaad015e (commit)
via 2a2f47d37519d5ec4cd310899d7df482145f5feb (commit)
via 60a17382794076ae299eed542eec4b2f8825c120 (commit)
via cac71bc308843f84ef9d1b427f1d401f036ef1b7 (commit)
via 73f842441ce26df57c3af30f526b59e3eba884cb (commit)
via d43299cb0e6680458c2e831048aaa16acc0d3017 (commit)
via e532e4f5648f23bec3bc01d01bec47abd966e07f (commit)
via 537970619d87f2556abcb7d8cb03eff47eda0d54 (commit)
via c612e8e22e27210ce85ff4b1f2924686567aada1 (commit)
via 1ab87fb58246a94e4172188ac80061cddcaad3a2 (commit)
via f714c4ca5abfd11559854398f6f47ec403a3f7de (commit)
via 4acf38036ce68ae59b2da053af471bd0ec40ecae (commit)
via 7e575b706d7cd4d9d15bc9bb6a623ea9b9077252 (commit)
via 80d16e0dfbaeb8053cbd29d6b0512d75351a2c25 (commit)
via fd911f4775865a204a9a67dfcf290d8395e0734d (commit)
via 36f8c6c15ea3e8a659989df7249715df0e0d2ac4 (commit)
via dbe4772246039a1257b6492936fda2a8600cd245 (commit)
via 6ca9ce763678cd8ce47680d083eeee299889427a (commit)
via 5ed66be3e60157ada3a26fc2e53cac3790661c7d (commit)
via 40e0617397c03de943d81e05541c4de17885f6f0 (commit)
via 33352a6b9af110cb06c40c8cfbecc26b5e3e89c2 (commit)
via e567e51a23beed32f03870d9885e462e1bdc276c (commit)
via d65d61499f0f2ca9547d90e3c36fcd83d55d8ba8 (commit)
via 8c00b35f6427ba14e85e7c335367418b28599154 (commit)
via e6a262a1e5a0091b06840a46a20a53eed38eee42 (commit)
via 64bc2cfab72d67bf9e4f3cd19fd6ebb586208e67 (commit)
via 71007940646f888480f85dc2780c0d600f64d179 (commit)
via 439ab563402b77e745db3181af1751ebcac519f3 (commit)
via c1ca91c677addbb7a546ea8e36cc67e0717f1fb7 (commit)
via cab5acfd0cd42260988424e06d926d22e37e1e11 (commit)
via e37828dd1c90541e6e6d7f8d373d95a25ce6bbea (commit)
via 16f7fffa1820f7e65d28e807a81d89dd59390037 (commit)
via 923462dbabee6917d6693e62c80bd0f7c8fd980b (commit)
via 807607ead100bba4afa0c5af2697ebe6dd2f4ec7 (commit)
via e686a5339d6927d4ef7bf35a8e5d9c2ddfa98149 (commit)
via d4389a32b03dafe3c369e9d10d0001687ec74f55 (commit)
via 72b428b6f9178a08d8e5bf68062b4082bf0b8429 (commit)
via 01e1910c9db5d1e5dcf2b5ea01afd7aa8075ebc4 (commit)
via 89b1ca9494abdcaf3ef2beec3fcb015b3328fea7 (commit)
via d3a4dc9a80f85583f71c789555deba8fae2f0381 (commit)
via 1ad0a17cfbc48bec8620958e3495720355b34f81 (commit)
via a292c65c3c68b9bccb2a30169e4bcb613da1ce68 (commit)
via ba0004b0092cf2d7652b9a66212774d19f91517f (commit)
via 29488dd4130d944487bb2fb813401773ea155428 (commit)
via ece829ff0b6b5117ecdfe22439c24b64a8523683 (commit)
via 127d2f3f9faeebcde23f77f64336e7551af525dc (commit)
via 9deba8e6b563fbeeb98af85b89aee5c9f4878a03 (commit)
via 26f53e4d06284cf0854ec06d14b5071b26996347 (commit)
via d219feca6015a99293d37d030ad791263432f533 (commit)
via bf1e13564ab29539beb465d212a51e895e640915 (commit)
via 63e17482bdd2e68e56f2642267169a101af9e012 (commit)
via 6ac0a0914155c3379e6608d3b6bc884ba1e59527 (commit)
via 82679565adb6001d2b445cc932935d9a0561fafc (commit)
via be23568ad3e10931387612fa90f8d2568e9878e7 (commit)
via 42073d9d8c264a33ca883c9e80310cb91de706c6 (commit)
via ea97070cf6b41299351fc29af66fa39c6465d56a (commit)
via 199269b851a76501bba4ab7bd6f8c7e088b45e74 (commit)
via a6f80229344ffd81fdc3c6642b3ab5740dde4bf6 (commit)
via a9b9f641970b7f2db1b1183fab3deaec454b5b75 (commit)
via d7fe718898c7a8d1fbcebd4a4d47548d08ae1d98 (commit)
via 33f75a53137bb954ed660f41043c6887c94e398f (commit)
via e4a817403b39c77a0b3a7c015e1f77f241c21c3a (commit)
via 85fcde63692e73b4ac783d2300851b94578575c9 (commit)
via 9440c155bc74679a5a9526c1507384790a62a057 (commit)
via 425ee821bac86ed616075e2ac89946863132b46a (commit)
via 66564c104a5c8a918a39e7da4cde9976160362f1 (commit)
via bf2a2aab27a29e23699f42c393ac31b324c7f096 (commit)
via 26f8bf358b2eceec472e341f1f380bf9474913de (commit)
via 4eb6ec7a740b1ec21334cc69b0272883c8e874cf (commit)
via 56ee9810fdfb5f86bd6948e6bf26545ac714edd8 (commit)
via aafb86a4203109142c6f888a2548fe57dd0dd701 (commit)
via bde0e94518469557c8b455ccbecc079a38382afd (commit)
via a29c0e18ea2b70d6e4c2b2bd48199240f4c909db (commit)
via 64bc0886402d587b65343fe6faecf35ed59eb029 (commit)
via 8ed9ddfe7f17d01d29cdaed8ee800487413e8632 (commit)
via c226db710c71f94fe3506a588bcd4566cab5b2dd (commit)
via b70a49c18a153f40aaf129d6d3f25039e052e887 (commit)
via e5094ef98ad34b001152d14ea2ea4c6ae9fbd85e (commit)
via 8244c9cb7152aa47be60e0f86f02289fd67a6239 (commit)
via 47a2fe257cc5d3d3902a7db667d1b5456bddc447 (commit)
via 572c201a7d17bde8990ede8d986bf45e53d22842 (commit)
via e08a21c3bdc637a447d68da727fa3f4cb6a26112 (commit)
via 419690bf77f83061c4df46291b27a2a5769e1758 (commit)
via 12164374c127f1e9484c63f2f365ff7395768599 (commit)
via c2d40e3d425f1e51647be6a717c4a97d7ca3c29c (commit)
via 1248e88ace533f61e4f8865fafa5f0c89470c6cd (commit)
via e8a9043a2508edd209a01facb71fe4bac4d9eceb (commit)
via cd146974efff9794c989296158c0e05997da1edf (commit)
via 42569ae1408198a605b93aa1369e32fb62dc8ee2 (commit)
via 14fa32e0dcb2d77ebc7d8a5938b37e36324b8bef (commit)
via 018e2a97c77a95d84505c45f3e2d106e753b18f7 (commit)
via c1d838b4ef7ed6a20b43987713981ff521aec274 (commit)
via 41aef4f8135de17e94f3d703ff8f461bfeedd94e (commit)
via e7e1564b312b70ed4d0c04e59025e337f0210729 (commit)
via 9b41a2252bf1db1fe686ed4cb8b20d3fbbf1d5e6 (commit)
via c13f4a4ab9ddf938d9b5fd62e08574dd8dc210b1 (commit)
via c82208ea06ca7d72186cc606cc4038bb35b7afc1 (commit)
via f34eec70bf82d42c27a0df4709b0682abf2f7b2a (commit)
via 454afa9877926214539d135f239035706bbec035 (commit)
via 4b68c9bb855e1cee112ed6a0c3dd398048930824 (commit)
via cddad16de73088a9e11330746e33a1faed947f64 (commit)
via 32931752aee5c1c61bf6758a3ad34a77eec04064 (commit)
via 9346c7a0f4d9dce50b673e66b5cc992f9b32d18b (commit)
via fafa0dd4a0c97aeef84b7c95ed624c3873ca4cb4 (commit)
via 85dff79c7ec72f5079dea7646baa557de2e98c5b (commit)
via 2ae9ac7bb5a19741eb21b0ccec951588c5672d1d (commit)
via e7591db5df9c7694e1e0e3f7dd3e2d77753999c1 (commit)
via 453f362d6c7bba06582e713b9fa243fa0b8ef5b7 (commit)
via 96b1a7eb31b16bf9b270ad3d82873c0bd86a3530 (commit)
via 704e1a50cae62f61fe2e09d5bcbf59d30c4aae97 (commit)
via 9a49846cb4456b17526c30799df03c9a06c8a318 (commit)
via 18665064e7946e537931caccde60c180e9796127 (commit)
via 7e0b47d099a071d438c45e7cde177d89a0f3151e (commit)
via c738282e2dabd4004dd451edc1d5dd36b4e59df7 (commit)
via ccf4516112346e4722125b2004138b7a78d701e0 (commit)
via fde6aa002772b6529f185148ce22cdbfabfe6a49 (commit)
via 87a710de67996ae01dcee1c2e05f09c9c392f82d (commit)
via 74c286503e39fe3f70a52a0f8b7b7c206508a494 (commit)
via 7acdd8d445081e1a4c70ed58298367a96ccb55fd (commit)
via c39edd26a68da49eb507b3d842b8929a57f2710b (commit)
via 2f0e203d44adda18249239ff33a75102a8477e82 (commit)
via 46d8b0039515a169b9a0297ff530aea7a25f0a54 (commit)
via 16dea270c9125075b74983e10cf1af7b8851f37c (commit)
via 88fe64ea760b7c2f4e2f5d4fdcc8c298481b46b7 (commit)
via 143264e9d77c446e9f5d9f2611198bb73de82243 (commit)
via f3cd2b49392a4883751e2dbce2d8677e46d1f167 (commit)
via f45931cc8a5c0924d77fb6c6681eeb467dc91302 (commit)
via da3579feea036aa2b7d094b1c260a80a69d2f9aa (commit)
via e7a483b3f53323d03b64f49385d173ceb595c39c (commit)
via 5162bd7739ccfab83a0e81e44ba5e46314eae0cf (commit)
via 54c8596393d90528dad115ccf111a5dbc51c5b73 (commit)
via 367da9bd6d92f7fcef7505bf8c54b1a702146eef (commit)
via 14398c533d5f036d1c005565d9236f6721390c5d (commit)
via 62b6680e17683955b24149d63fd9a3f329e0636c (commit)
via 6f344e77217625e36e924f41c24d88ac0ba685be (commit)
via f4130b056637b3b14635d8211ad7d720a74ae9c2 (commit)
via 192787144f8338dfc6443e6907ce2274fda95ca9 (commit)
via 0ae219d8e8bfeacdfe026ad1e3576ab401d79724 (commit)
via 946442d00fdfdc6657b5d068b6aacd859c80383a (commit)
via 2500db669d6463c3ee2f607be63ac0690cf7205e (commit)
via 6994c90205b1c8b7233a47a6ad8b07060ed6b9bd (commit)
via 1a488b6c5ef9970422880a4a6a348d89a66805d7 (commit)
via 45d2c5e07fc8eb3b4a9493470118fe5765164365 (commit)
via 58639475bb07feaf4755b83942a572abb2943743 (commit)
via 9d7bd1274b7ff4578e108c8c13bad81cc2867d14 (commit)
via fededac895f51f0d3aebe33486e0c7303812eb34 (commit)
via c8d00f7c62ecf19879d70e6dcfdc0ab14551b6c9 (commit)
via fdb231c9e31422dea878922607a0268675f50ec6 (commit)
via 5fe71727421c85412f6df8c77f644f69ee36c6f4 (commit)
via 861f24ea145df8212c1e9bd7bbd601fa474c9e33 (commit)
via a66bb4b7adcdf3299fbed50196db06c8dbf16058 (commit)
via cc489915a21327fff39fad6767649c6b2b55ec79 (commit)
via f91e51c212f329dffb26d50643d41a7d27552227 (commit)
via 94bd413ac2b2223802b2185855e7532f6a799dec (commit)
via f5b81693da196d63c248d8004ff02176b23882a3 (commit)
via a5f749af5340f59c4ba00ad0179c4d5be7d6b8f1 (commit)
via 498c4d68645d93aa18da63605b2fd6845a88b886 (commit)
via cb05a97990d769abd3c4e866d8a8f2ed4b03c804 (commit)
via 3a7105d6768c244c486696535da59cf8da25cf5c (commit)
via 8f4a847270d7d8b5585e30d9c01230c8c79e10c5 (commit)
via a8594f39c30f93b0a0968e89779f24e358e20211 (commit)
via cc51cced3f953affb8c87baae5683542b368da2b (commit)
via cfeb578adedf7f6a98b293507d446c886d9b3525 (commit)
via 214d50fb389c4439614e56aad6a252c87edec4b3 (commit)
via cfbf803bb49ad944bef4070fec9f2c999a98cb78 (commit)
via a2b82fc102f6a4f987bf9ba59b05a336dfde8ac4 (commit)
via 0b2800af6eedeba3b71108765fd4e04195c0a4ad (commit)
via ea07789d058ca92c5617c2aca304fb8ad8c69c18 (commit)
via 1e1f87c3540b2f9de576a0267c694e8ff19a1db1 (commit)
via 2afcb422fee3bbb9d3c8403f98e489f195184471 (commit)
via 38a875a2d8c9337c1019b11473f989519e16a778 (commit)
via 16ad2c551b5a2e90d0931644a07caf02b254fae7 (commit)
via 29b82a61f1826a72597adcd7ff38e153a135379e (commit)
via dc25eefe48a9c76b9889dde024ce8e88ee1eb363 (commit)
via a41cac4287573ff826bdad6e9ec61b5991c757e1 (commit)
via d5f2a0d0954acd8bc33aabb220fab31652394fcd (commit)
via 7831eb91e8d50e8cdf9f17487c86584efbfe4855 (commit)
via 994dfcd88af7dbc2a8a6221cb662285faab3f6a3 (commit)
via fcca4e8c09253602530fe66a04e8e9b71bbf4739 (commit)
via 76b4abce83b333c61b596df651dfc2f9e3d72100 (commit)
via 592db047a5a82c5af9e837dc2fc9a9df102af8ed (commit)
via e488b9b74d562517bf430b1de30747d575966b07 (commit)
via 72e89527efb75c3aaec0843bf81b08581c3b1f58 (commit)
via b9b2b5e9a5aba9c87939ff116d103c8e9d42c550 (commit)
via 1519438c8f9c74286b84c7258ea9ede4a5a03a7d (commit)
via e817fc7e97fff519a1e6b7a0ef56fd966a274039 (commit)
via e0d3a70defab35a0dd3f1beffc3495b8965883d6 (commit)
via 4ca74b26c112156ec41226073e8e70ea26d1ee0b (commit)
via 8fa04182fc57c74999d148bc14e5b6a6b3929c4c (commit)
via 72fca133ddc102924bef14d4281314309fb51201 (commit)
via 4bc06a779cbe53dd5ad91ef4fe0c0262e430f03e (commit)
via 9204d1f5951ea88ce351297885c83be3ceb33344 (commit)
via c077699ff43ddc2dff552b616044dab306f23666 (commit)
via 2ab76e6181396639a60994b5c78493fa2235b304 (commit)
via e41be59e113c11dbad91634d47c832dffa628e8f (commit)
via c7e380374b96d23f682e81a8ff159c8e39538103 (commit)
via 48bbc35c15b307c83ade8ee4dedbf2217aee3530 (commit)
via a569e3510c94ab28efa0a6a50ec657ae37035d43 (commit)
via 8cb0a56a019c3cbaacc8ccdeebaf3906eeda8a74 (commit)
via 0b690e4c4a93543a1f0e0a2aff0206d605ac1d33 (commit)
via 7f8feaab9ff4bdfa54c61e4cdcc5e9216fab3c3c (commit)
via 686768228ffccfa16bc367776cff3eb6dd9f86dc (commit)
via 03ef197742328752227b359e5c10cd12de97a967 (commit)
via 433488548230ab8742f9d7b71fa33bc56484ec86 (commit)
via ce84abcf310cc0470a310621688f0947609e6732 (commit)
via 392c5ec5d15cd8c809bc9c6096b9f2bfe7b8c66a (commit)
via 8b47d98c888f29facede10aecc8bf4d39d8f21ce (commit)
via 8da51f50f03d1b84d9be0bb73d4091ddf32149fb (commit)
via 52588836afbc4f6d66fd93d30bc37867bfeb2eb8 (commit)
via 02fb830f3e15239a263d109e8bda0256418401bb (commit)
via a5eb53f5b3fcf01e6df58325e11269e62e259197 (commit)
via 0dcc4f4feab96a9a76d33137adeadc488884b0ec (commit)
via ee886b45eb503cf048abf8416ce7b37430f6fb25 (commit)
via a79234992101f76634568d1128e51655ab45b42a (commit)
via 7c7bf1d126cddc59ca3ad540e2c2015853509a51 (commit)
via 49ef97d8a1b783a35a81db555eadbea985b84a11 (commit)
via b48e4ef0e4f09fdaa48f11dd97e1c378007cebaf (commit)
via ab579f5245e995718410e2f59bb7a82814f138a0 (commit)
via 943f4ea870d5d00101dcdb4dfb5a6892ed3eb634 (commit)
via ef6d94432720eaf1fb73d65d78915a23aada2a25 (commit)
via 9eab41529f7da65498b241f988a8ca096a4286c8 (commit)
via a2500cf8f53c5beaa5e9f872458f10650f91b95d (commit)
via da4f84b70faf79d279dd1ad978ee007b9fbd6447 (commit)
via adc3e2f94dcb38c4594408774599d946605f3cdb (commit)
via 60c3b4447c47bd08d990d91a3a412ac6b329ff08 (commit)
via dc4cf0c6d071ef5661203ef42894ff8ecc6a90e4 (commit)
via 1a3909427319f08fb5dacef02baa8c89b0b97408 (commit)
via c8aff6addd2137edd3b5b727ff40c3f7f6b00e71 (commit)
via 320f10a5395ec1799f11d172ad6936da5467e815 (commit)
via 323d64cc1465f7a1ef8813fa7a541f495c5e9dba (commit)
via 2b5052e66a62634e8da2495a0d97018a144b7919 (commit)
via 3d234878c77fab6f7bb172c5ef2bd21cde768a72 (commit)
via 935c7cc4033d440f270cb2d62114024e3578a4d3 (commit)
via d83205544b88dc4a9c5584212034c03a4f9e32fc (commit)
via 98a1e1dd654b98ccd453f38618f1ee0ca0c33a2a (commit)
via 1438cd564759a281add9fa7c8ed04280848f6ad4 (commit)
via 7f652b9b12ac550b784db4fd52df91f672ab4d94 (commit)
via 85b0a75a0499f2a7a08d6f597e3f90ee4f7be636 (commit)
via 7185b57ec3dfe44fb5ed0188e6234725cfb8fff0 (commit)
via 1910b46df9c17fd817dd38ccf6748cdc49c6e821 (commit)
via da5d0b9b61f06c13ecfad23fd0dd4b4fc0734c77 (commit)
via 3f5fb3767356584c73a7de584adbc4da6159683a (commit)
via e1ff4dda628311346a699da5adf402d859a517cb (commit)
via 929f85f8deb68ba58610354c8161c0e1c0806006 (commit)
via c7b8894303b7e88629c0b838080bfc6097c05ec8 (commit)
via b3dda76df66adecf3af2ba419a23b4169bfe9541 (commit)
via 39387427187c807bdb47830b19224e6dc9dff52f (commit)
via d35ac0a1d9375ec6e4c01db0d2107ecf177b7ab8 (commit)
via aa528462f0f90a589d7773e8d952c51226fc0b96 (commit)
via e561cb2ebdabe6a41ee3e702f407c853a88857ff (commit)
via 0f3dd4e12dc74c1c5985e9b701b59fe2355af931 (commit)
via ac01dfd069e66c85409e93b40385a393bb5c5a59 (commit)
via 61e6c39d6f14c8d06956e92002b53f9448453b4d (commit)
via 598d81ecdbc43b8e6869ba65c60b1d7b4fb155e3 (commit)
via cdb6afba87fa03187512682b8348e65dc5e24a0f (commit)
via 5dc80a3df42d7725b56cc33d33249363a7a14279 (commit)
via e67c6142d40cfde0a9c04c0ae491b8f8754bf69d (commit)
via 2a1a86b1b753b7fdc7db4bc48e8f0838a9521187 (commit)
via b9c101fd307c164f7551d00068358828af4ed549 (commit)
via 025659c532f8521d8a36476d2f1a14593c9690b5 (commit)
via 7482587271347cc071f95cf1709a0370c0087fd7 (commit)
via 8c06e54ac7cdefa669326da0280294ea88196575 (commit)
via a154032687e8ba7c5c5cff880f34cd37166688c9 (commit)
via 6620f7a7d1c0d952ad6da4b1441608cf56c2702b (commit)
via c832257c559d982c221595331f75e16df2e0b5f5 (commit)
via 4be4af38b1179f61740628da95a5c0853e1a17ea (commit)
via 79117eccd04c2a99d80026f7c0df14a8f6bccad0 (commit)
via e43764fda36554cd8c8f7a8f6aaec51ddc48014a (commit)
via 969260c87bd0916bfa951f4801bc7e4e60872fe2 (commit)
via 1e39232770b629d8e944f62c5f01bfa6c3b09384 (commit)
via f9028be22e3a0ad08b2f0e51d823bdf868621019 (commit)
via e448bbba3ecae68b261612954aa9777edc384be4 (commit)
via 4df6d1597b68281fee3999542219c18029b168eb (commit)
via 066c983e091831645218810fb921986a659ccb1b (commit)
via 95c6f676d504950bab0964fe9297893f36d4fac6 (commit)
via a3d4fe8a32003534150ed076ea0bbf80e1fcc43c (commit)
via 650c6ce282800e712fb378741638ac66240d59c0 (commit)
via 7ad877ff1d3b88b2b8430a80f6102590837c2e74 (commit)
via 73b25590661745a2e39c6235698ee93bf00ddba3 (commit)
via 2df5585f42f1af03b89204c7cc22569720a26ae4 (commit)
via 298fd19fe3405ccc76d5d7ab8f91d80c53bcf44b (commit)
via c312fa4f4866b21aca35bc4987f8f0e83b198ef9 (commit)
via 69cdc5f74d067196ec35daad43f842f716897bcc (commit)
via b3c562f2bd85fa9f825d59f8601ba99e04e661f3 (commit)
via 18a4180734c1896f12afadd40a2a658f3c5c795f (commit)
via e62bdea81b790e8694075cad0255244226a00e5d (commit)
via 423740919b7a050dc1255a0ebdc6ab6b71ea777f (commit)
via 504f4708e55050fb6f9dfa7f784dbcceb5e1fe16 (commit)
via f0229b977d354a137df49137655eb3ec56d96f97 (commit)
via 2cab68004571463fdaeefe5659380fbbf9c54e01 (commit)
via 807f3b5cd6b5f7daed92413e9ab771ec500b6536 (commit)
via d7fa28172fa3b2481e1f1c1ddec05ff7ae21bce5 (commit)
via 772de981b14ae73731b54e6dcc5b84935584f1ce (commit)
via bd04d06c2d51af45ed818e90ef500936dafca1c4 (commit)
via 44160bfeffd56f5ccd6c8400eb8c3e883ac10b19 (commit)
via 29f6d05f66afabcd46c532622b10b370b65d3090 (commit)
via f941f042dc2c953390354434e9a4563cb67b14e0 (commit)
via 54b12f14d032796a8d6638cdbbf6ba5d63835478 (commit)
via 689aa37ea4cc087ad0affeedb2b4f8de5b29919d (commit)
via 211389e129c7c27129fda481deda0dd649630a98 (commit)
via de6e68cf258f9349795368f5bf773f8911a79acf (commit)
via 3c98ade56f07c44740f1bde2e37f8fdc9842bd18 (commit)
via bf30f052334922f729c8805f109f838ec1a126b9 (commit)
via f4ca083e427788a22a4979a104b517a5a4ef3df9 (commit)
via 3a6ae66c2a737475ed55fb28cd3cd49b49e7295e (commit)
via 7184eb4185f5bd6577dda90e5100c13e238ed8fe (commit)
via 2f808b5a9d101109016361c6143b1add3887d1b6 (commit)
via 17590957db6a352e86c208c3520a340349b340ea (commit)
via e2889aabb93e201bfaf49046472ffc42df8b4b77 (commit)
via 9e44958839b3f9d8fbe6cbd1f0c8f711763396fb (commit)
via 76e6edb548bfa6eb3df302ddd74ecff71caee3eb (commit)
via 6bff7db76bded2e571e2ee827c776c45bdcd6c6d (commit)
via 5bfdcfaa4a495e36255de546354d1673e4581d3c (commit)
via 46bf6c05bd7c5aa7cc79af834843db68cb6b5471 (commit)
via c6cadebf7b86855a7da91f14f66bd7d605ee13ea (commit)
via 2f220f9ff1a51348ea225d72a75d58d5d81031aa (commit)
via 8ba0a7bc496c135f80c71bf035fcca0687a16e27 (commit)
via a87cf3103eb903132064550a8e75715d9b644620 (commit)
via db8cec0cd9505bd0ab091e6258822dcb2891338c (commit)
via 3471a0a39ec45a25c96905ce634e142fd4a06902 (commit)
via 2f81b4961e70a653a09dc24d8b07bd9eac1ecd04 (commit)
via eacf6593309211e0cdc1867d69d4eeae86bc450c (commit)
via 25e6b90892b1b93c129606f175a905c30ec62a6f (commit)
via 04c159afd7ae18b98b50d1a3d2e5f1eefdb3bae8 (commit)
via c3f6b67fa16a07f7f7ede24dd85feaa7c157e1cb (commit)
via d5a472119f28ad0f7f15756997c741ba25208ab9 (commit)
via bc3d482930fad8d97305f07e116093ace8110e9c (commit)
via 32d750c6674613bd630eb1e724adc82b8ec8a622 (commit)
via 7bea7dd8c15ce158908b8c42c93533089d12da8a (commit)
via 4cac3ddb232eac0e52451c0aff718f3207fc7977 (commit)
via 56099e3711191e22b41cc6f404a69569c1f3b7e1 (commit)
via eda6761d2607385c10bb6a9e8bdc5c1209405093 (commit)
via 8d9a833a05515867753578af948c537a4d8ad127 (commit)
via 14a35130eaf81efdc8483fdfb1b8eabbc97653be (commit)
via cea0d1058d5b96e616ce8f71afd0b45a1f81b103 (commit)
via 3ebd4b039b2596328c1c112398a9e629c9a73001 (commit)
via 89e0087450e73ea2336f56c0f7024f4741eaeba0 (commit)
via 0404a592b185c5473343a3faa25de18845cd192c (commit)
via 6ed2d962ac93a626c7fe4494a0a060858c4125d0 (commit)
via 00cd04ff09dd403968633193ad51ce81bbd1c5d1 (commit)
via 55f6c351193ff5aa17a2729bd58d6e74fcd17933 (commit)
via 0ef74c3696b50b84cc1613fbdff09acb775c3e10 (commit)
via 654ec49fc5f8976cc6567b31ab8277d392f79b76 (commit)
via c65fde7c57736e089aa9d83f82d340d7dfb167e2 (commit)
via d4a8366000c44fe71b70dcee0615d38253b4f932 (commit)
via 4d67c32a6d5ea8a09f9a9dc995b33619f56db5fd (commit)
via 2cb89753827b015cd183fff26b52e7c28154727f (commit)
via 211025a21adc6230894092b606c8f86cef3dc913 (commit)
via 427bc05eee647c9c5afb013f07c0cc73ff17aecc (commit)
via 4f5e8221042782288e57974e38d4ab7d9de54740 (commit)
via a6949cd6433d089e240c6bdec095e12c4230214d (commit)
via be918f9a8b657005605fc8856ed037fafc2374e9 (commit)
via 0857d558e83343a266e8bf67f835017c07f773c0 (commit)
via 03a458983cf7d345ffa7d9d1b2cad0b509b3be54 (commit)
via 299f1f5ff94bb77306019c1d26d12e57a30a9ab4 (commit)
via ad0e3e26471403d9b019352132cbc04b2c1c055c (commit)
via 2015f7356237f5004c6aa9b678b442575476681b (commit)
via a7dac3f7ff64093a4958710cc45a7dc28f727182 (commit)
via e6d1be1a6718874ab37b0f403c3e266c90bcdf87 (commit)
via 148c5bbf6132ac5b149a0a97286f13ec212443cb (commit)
via 4542e8960d4accbef74ac41e5082209f3776b5b8 (commit)
via 56d109e8a0c5d46ac542870723ba324aa2425b88 (commit)
via 131a89175e97714ac87f14f95703cd76df7a965c (commit)
via bc6af598cfc62d68cc4a80c0aa10d8573d63cfc0 (commit)
via 468687262b865e264f2bdc235ec4aff45fffaf29 (commit)
via f1dd9da6c3582b6dcb876609a142f9b6c80389e1 (commit)
via d5d0d09842cd041653044e90bb6f2db0525c3f33 (commit)
via 5fcf58d12137139ba41af390550a0b73ea8a9ef3 (commit)
via 9bfdf12f2fd3108a4b3b772c20e9b78396164042 (commit)
via 4daf58b65f126c84a0d68d102ad899e260ea5662 (commit)
via 20a8d4adbdcc32b17fabc43ef1e6af4c4b7c67d2 (commit)
via 7e2d747ba455749e7ccee8eec36fe5f7e51ed749 (commit)
via 9d90db228e26173b0781d090ea2b0e078c7969f6 (commit)
via fc2a9e9876510fd425d693aaa5b93230606d26ec (commit)
via a19cdcff7accbce09eab6bf7ea359a830564c1f8 (commit)
via 5306dddc94101bb87bcad59d1c29adc7bef4fe75 (commit)
via b1b78de66931aa416fda81f136b584f515561afd (commit)
via d003ebd54cb9b0454e60af6270177e202c766892 (commit)
via 36f7b7fa7bddd07da90c9346d5d62dbed77c2a49 (commit)
via 9b930275a2a875d91aa425cf8bcbe552c5bee453 (commit)
via f7cfb3332667e260a60660af23290087fd0c4fbc (commit)
via aedbd11c6780fb36fbaa8c6c8c4521caf24fa5b4 (commit)
via c519aa88c8cf4edb25a53ec761364b076a6ae497 (commit)
via 8aa47f6c343499103fcf86bdbe4c3918253a810a (commit)
via d7eecc60198a4bdd4cdf23dda9663f9d0d803f9b (commit)
via 1fd14dfefcd2339d9dea25b30dc9204bd10548f4 (commit)
via 39fba2a15cb200dbe77ae3c9a4d894496f9eee66 (commit)
via 557794895c01555c32906ad8422d5d6fbb878fe8 (commit)
via 3af2603f0f08ad709590766051eea370c470642f (commit)
via 03ce449ee10c5a8d8f60a68cb42839b9ae63ec00 (commit)
via 9784a1841ff7a5f08c92a0c646bbc4ff9a0b4ccb (commit)
via 5082c255bbeb8c83f9ab08d4e28429cc271b2fec (commit)
via 3599c07417b1bcf40ae8e59959278b4a59ede827 (commit)
via 043b3d2952712cd61e7ffb4f73f3253d11ce6bd2 (commit)
via b5cd040334461d9f5ab0f1267d505bc7ac3e0ca9 (commit)
via 54a1d02e8816b53e0d4ab77817380946ddf156d4 (commit)
via 7d1a2ca88d0daac2779ecccf50c4308eb0914ebd (commit)
via ad233427c14503e34980780761700596f13e14f4 (commit)
via 6713fdc5fb825c90598bbc48029027ed3f2515e0 (commit)
via 6dd94680bed03c6423d1674bc4193c64a7fed4a1 (commit)
via 9377868e58e5303d06688010224ae325972a1547 (commit)
via 8efaec345efaf37272cd7098177b60ea787e3ee7 (commit)
via 48c33c6c5adbe7e46d5a7853c1ee0f7085fcab6f (commit)
via 74c2ed970512562b5ea53004d3e152e2d8519329 (commit)
via 2d3ed739129c40c2473a927f51e37a445b47fee5 (commit)
via 3e678f3bda8a502c8a4fe483e12166bf1c9077d3 (commit)
via 73e1a3cf8920bfb4e7cd8f7672b3f82600f2766f (commit)
via 31075ad95c5c27f30d2b9812ead3dd8b6edff236 (commit)
via 721708db656444bf46451aeed3500bf3580b3466 (commit)
via f18722f2e329c939584ff1b45936eed172fb16e3 (commit)
via f3259cf183a8bea65acad690b50982057f24abe5 (commit)
via 0dccb67c0e49f3448e7fc3da7a5e414113bf36dc (commit)
via 622d3e7123555d0115149e3f4527dbe5f11da864 (commit)
via 6bb2e2de9bc899e31c8233c1ef74e8ff79c1bccc (commit)
via 366cf13a9ca23ea0ace26677754d1968106c277b (commit)
via 186195174b0d8256da12f91395f8e57c17ea6a7c (commit)
via 6548cfede8e4f8fb9457eb62b0c458969a358155 (commit)
via 42a5a7952572019c06a8dddec9994cdbe54e0d4a (commit)
via bc8cc9cf605cfbe065d0e27e8a081e5f54c6a4cd (commit)
via 02c2f7f80e6d858233a595cae04842666fbd29c8 (commit)
via ca15d37a2e8d6eff63b52eb5fef0e8d3bdb3e2c2 (commit)
via 10386454c422828a5d46a3b927b1c284e6216372 (commit)
via f41e4c3104480b6a8a6aa08cc5c28cf71a13a3bc (commit)
via b30eb9348603cb2181794ca00acbc1bcffa54db7 (commit)
via ac473294894ef65cd01174114183a42657d6151d (commit)
via 3456e30bce45c286b120d176a517ca88e9e21b1c (commit)
via 4325f3dfd81569be4ef09a84cf0fb74246a5b666 (commit)
via 74bad1ad71292fa998f14ad3953236f7e979efac (commit)
via 035fbe1da0e77a45d108046e5399c5a676e098a8 (commit)
via 5d65c74c127243790200028375786fced2bd1ad7 (commit)
via 2b96d4487c517ab209a1ca5fce4af877fecae0a7 (commit)
via 79fe8bd49de676e941f804350bde4115ab454124 (commit)
via 36d8374051ef3df79035debd0bcdf63016d0aa85 (commit)
via 69abb657226d2002827b238503ef0f31a81d66c0 (commit)
via 1815740f4154dea79aa15ec64cffe2f5d010769d (commit)
via a752e1aeacc9470ff3dfb172bf3f9064eefc3161 (commit)
via 0b7af64f6b3a567371a38ceb83001008dcc8f4b2 (commit)
via a1d81937904fd5f646f40c655975855aea5985d4 (commit)
via 26326a8f0780a2603c7ff7268998fbcce7bc5dbc (commit)
via d42136528ea0edc6c7ea45c3c4d4c542217f9d33 (commit)
via 7a49270e4d0117b5432dfedec70e41a9c399745a (commit)
via c45efc1b129fc4b64d79040e0bec33f08658b170 (commit)
via 4a5506ec8f3474513c7de10969628385cec138b2 (commit)
via e646d8af58ff6b9863a89ae65b6244275405bce0 (commit)
via 1024b43c68506682dd55cf1cf9c5fc9dbc8cd96b (commit)
via 68343c845a46a9be3f38c38388ec02a8bb4ef884 (commit)
via 31df868ff405b57c6767fad73a7ba82ac0544e4a (commit)
via 51c2df1a2974002d93335c5d762353795ba8ac92 (commit)
via 10f2ae4625a238a4899372432df13d29eca9f9b3 (commit)
via bd7e56727b6d8228f2fdc685bff3a18349415c69 (commit)
via 85873bf8615dc643db8619414e417dffef827c4f (commit)
via 909a05ac4185589eb2304d815b5ccd108f92f4b7 (commit)
via 1453084c9e0ecc7c3663678031f3d19c5f91adff (commit)
via fa4c5b912c4e4deab0e42af46eaeb6ef3e41df72 (commit)
via 702512a6bbb48421042b69fba312bb41296eab61 (commit)
via 53a1410acc5726fe8ed83ded19b65be7c9002ee1 (commit)
via 832eddac5d9724af4b228cb123a84ed7ddae0473 (commit)
via c055314da48eaadb3f88b64eda17263fc6b387fa (commit)
via d86119932d1b6f2674f3685515c35be147f2d327 (commit)
via 846996720639e0a37682312641bd6ed504870488 (commit)
via 9c2f41d0ce4250623b36966eb04698d55eac7509 (commit)
via 97e41248dcb4ec3285d1e5f6d0f7d922952c63a6 (commit)
via 8f8bfc92e622717d75684b8bd36181b525a22309 (commit)
via 1d139b0a312bae07267448c6e679b50ce24e5fc2 (commit)
via 6e966c8f89b249a3f1c1a87201f24db656a06720 (commit)
via 882b1c68c77a1e3e8261fd7a8ff9d654313963b2 (commit)
via 4cef89808213a50c9da3aa469d6253ff939f254b (commit)
via 58a2bdc6b084fdb4ac57726b305f49463370cc2c (commit)
via 08f6a3e96b00395c6132b7558ca8c3e0764e7adf (commit)
via 8f4d5d803a3fad8019f8b4b638731d533c6dae16 (commit)
via 4f47c644ff2ba7b3d062e51d4b7eef4f87417301 (commit)
via 3c9911b4fde96eeec42bdf951ac6beb003eb0184 (commit)
via eecd53050bae78469caad312532cbf404244f1e2 (commit)
via cb324f11a8b5ac623b9c4ed491d7ec01551531cf (commit)
via d20b1f2ce6030846d6de80f6addf2c5ce53d564f (commit)
via b9be6a9538f9d11946c5291e7063aaa0db990261 (commit)
via 39aa6489f86c5aa33e07da05c0f0be4ca2d9d9ae (commit)
via 85d28a85b04c5c6a6905c6e018dc9a899afc4494 (commit)
via 80f01bc608cbd779c76a6e5d5ca8dade8b37dbdd (commit)
via 1ca6a3ac00cdf22e6fea1157bc887dd44e1f87a7 (commit)
via 8b0cb0ded8035fcfb9cdd483ab3936db6690b97e (commit)
via fa9feb90e8588508daabccf397cc3fd4751a0e70 (commit)
via ae0620edd4c9cb477066e5f29f1d983b79554f4a (commit)
via 757a5b14ef9101d99c47e7155a1b19b526d09389 (commit)
via c6302d0c4226e13753428df87e7d7864e86ed778 (commit)
via b91e4297ed4ba93adcb9af382f00c301876bf8de (commit)
via f5b9768aba32bb7673458eeeb14f0f56734427bc (commit)
via decee38f00f3efac436fa899bf6160fff787e318 (commit)
via 35b62ac905d85ea7216a2d61b1cfb62a890bdea6 (commit)
via d8595ab6bd85996c00019da0b086fdb40374e1f9 (commit)
via edd0b0623481ea70b4af23a0cbd2bb4ef797d10f (commit)
via 45e29ee0eeeced40dd047ec9da32da674124c55e (commit)
via df3ee4b9e04459e2ba6dbc50702b3c7f29c0ceb7 (commit)
via b82db9d608868c11c68d692458769e9afb47f871 (commit)
via 53ceb71e36d310a2a4cd5fef7ae9fa5d410f078a (commit)
via bc9125aec50bb75fcf601eb2e0c9bcb5dd3a1a1b (commit)
via 9869caead03573be43747c30c4f38929c975b409 (commit)
via a756e890b5e9dee98694bb88d3714613c0af686e (commit)
via e9799d37bb8a0d145d0080c8ac93781c08f150e6 (commit)
via a713b09fa81eb688de94ca720a304138ed48f96b (commit)
via fb3a565eeded13fcb8f746229c6a55aa7f1d000c (commit)
via 38293a1f2ca44708f117d870ec1c6e50600fc2c4 (commit)
via 70cc3db9edd5e1b30d8efb8107e57de021fdaac3 (commit)
via 5d221a4027cdd09be11a45cd30bea2ef5b67bbed (commit)
via 07738f9d7122d58dd7e42b7fc414f46c3f5ede35 (commit)
from 99b7eba1c65fbf8c6c0f71fd2f930fd480fd0b03 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit bd923e85fd2f9bd6ad4dbfbbadae4fb43ff79210
Merge: 99b7eba 9046019
Author: Jeremy C. Reed <jreed at isc.org>
Date: Tue Jan 21 15:22:49 2014 -0600
[2945]Merge branch 'master' into trac2945
fix merge conflicts
-----------------------------------------------------------------------
Summary of changes:
.gitignore | 4 +
ChangeLog | 855 +++++-
Makefile.am | 13 +-
configure.ac | 697 +++--
doc/Doxyfile | 42 +-
doc/Doxyfile-xml | 7 +
doc/Makefile.am | 4 +-
doc/design/Makefile.am | 1 +
doc/design/datasrc/.gitignore | 3 +
doc/design/datasrc/Makefile.am | 30 +
doc/design/datasrc/auth-local.txt | 142 +
doc/design/datasrc/auth-mapped.txt | 99 +
doc/design/datasrc/data-source-classes.txt | 366 +++
doc/design/datasrc/memmgr-mapped-init.txt | 137 +
doc/design/datasrc/memmgr-mapped-reload.txt | 94 +
doc/design/datasrc/overview.txt | 68 +
doc/design/ipc-high.txt | 4 +-
doc/design/resolver/01-scaling-across-cores | 347 +++
.../resolver/02-mixed-recursive-authority-setup | 150 +
doc/design/resolver/03-cache-algorithm | 256 ++
doc/design/resolver/README | 5 +
doc/devel/contribute.dox | 162 ++
doc/devel/mainpage.dox | 72 +-
doc/guide/.gitignore | 1 +
doc/guide/Makefile.am | 8 +-
doc/guide/bind10-guide.xml | 1014 +++++--
ext/Makefile.am | 8 +
ext/asio/Makefile.am | 6 +
ext/asio/README | 11 +
ext/asio/asio/Makefile.am | 310 ++
m4macros/ax_boost_for_bind10.m4 | 61 +-
m4macros/ax_python_sqlite3.m4 | 17 +
m4macros/ax_sqlite3_for_bind10.m4 | 21 +-
src/Makefile.am | 2 +
src/bin/Makefile.am | 15 +-
src/bin/auth/.gitignore | 2 +
src/bin/auth/Makefile.am | 14 +-
src/bin/auth/auth_messages.mes | 28 +
src/bin/auth/auth_srv.cc | 98 +-
src/bin/auth/auth_srv.h | 15 +-
src/bin/auth/b10-auth.xml.pre | 27 +-
src/bin/auth/datasrc_clients_mgr.h | 303 +-
src/bin/auth/main.cc | 9 +-
src/bin/auth/query.cc | 8 +-
src/bin/auth/statistics.cc.pre | 15 +-
src/bin/auth/statistics.h | 18 +
src/bin/auth/statistics_msg_items.def | 1 +
src/bin/auth/tests/auth_srv_unittest.cc | 82 +-
.../auth/tests/datasrc_clients_builder_unittest.cc | 298 +-
src/bin/auth/tests/datasrc_clients_mgr_unittest.cc | 87 +-
src/bin/auth/tests/datasrc_config_unittest.cc | 9 +-
src/bin/auth/tests/query_unittest.cc | 113 +-
src/bin/auth/tests/statistics_unittest.cc.pre | 58 +
src/bin/auth/tests/test_datasrc_clients_mgr.cc | 15 +-
src/bin/auth/tests/test_datasrc_clients_mgr.h | 30 +-
src/bin/bind10/init.py.in | 127 +-
src/bin/bind10/init_messages.mes | 7 +
src/bin/bind10/run_bind10.sh.in | 2 +-
src/bin/bind10/tests/init_test.py.in | 57 +-
src/bin/bindctl/bindcmd.py | 5 +
src/bin/bindctl/bindctl_main.py.in | 6 +-
src/bin/cfgmgr/b10-cfgmgr.py.in | 3 +-
src/bin/cfgmgr/plugins/tests/datasrc_test.py | 9 -
src/bin/cmdctl/Makefile.am | 15 +-
src/bin/cmdctl/b10-cmdctl.xml | 2 -
src/bin/cmdctl/cmdctl.py.in | 31 +-
src/bin/cmdctl/cmdctl.spec.pre.in | 5 -
src/bin/cmdctl/tests/b10-certgen_test.py | 2 +
src/bin/cmdctl/tests/cmdctl_test.py | 53 +-
src/bin/d2/.gitignore | 7 +
src/bin/d2/Makefile.am | 89 +
src/bin/d2/b10-dhcp-ddns.xml | 121 +
src/bin/d2/d2_asio.h | 31 +
src/bin/d2/d2_cfg_mgr.cc | 213 ++
src/bin/d2/d2_cfg_mgr.h | 237 ++
src/bin/d2/d2_config.cc | 649 +++++
src/bin/d2/d2_config.h | 959 +++++++
src/bin/d2/d2_controller.cc | 68 +
src/bin/d2/d2_controller.h | 72 +
src/bin/d2/d2_log.cc | 27 +
src/bin/d2/d2_log.h | 32 +
src/bin/d2/d2_messages.mes | 449 +++
src/bin/d2/d2_process.cc | 394 +++
src/bin/d2/d2_process.h | 333 +++
src/bin/d2/d2_queue_mgr.cc | 263 ++
src/bin/d2/d2_queue_mgr.h | 354 +++
src/bin/d2/d2_update_message.cc | 221 ++
src/bin/d2/d2_update_message.h | 341 +++
src/bin/d2/d2_update_mgr.cc | 249 ++
src/bin/d2/d2_update_mgr.h | 256 ++
src/bin/d2/d2_zone.cc | 36 +
src/bin/d2/d2_zone.h | 117 +
src/bin/d2/d_cfg_mgr.cc | 240 ++
src/bin/d2/d_cfg_mgr.h | 331 +++
src/bin/d2/d_controller.cc | 423 +++
src/bin/d2/d_controller.h | 557 ++++
src/bin/d2/d_process.h | 217 ++
src/bin/d2/dhcp-ddns.spec | 212 ++
src/bin/d2/dns_client.cc | 262 ++
src/bin/d2/dns_client.h | 192 ++
src/bin/d2/labeled_value.cc | 123 +
src/bin/d2/labeled_value.h | 184 ++
src/bin/d2/main.cc | 49 +
src/bin/d2/nc_add.cc | 697 +++++
src/bin/d2/nc_add.h | 451 +++
src/bin/d2/nc_remove.cc | 695 +++++
src/bin/d2/nc_remove.h | 435 +++
src/bin/d2/nc_trans.cc | 440 +++
src/bin/d2/nc_trans.h | 560 ++++
src/bin/d2/spec_config.h.pre.in | 15 +
src/bin/d2/state_model.cc | 387 +++
src/bin/d2/state_model.h | 679 +++++
src/bin/d2/tests/.gitignore | 2 +
src/bin/d2/tests/Makefile.am | 111 +
src/bin/d2/tests/d2_cfg_mgr_unittests.cc | 1260 ++++++++
src/bin/d2/tests/d2_controller_unittests.cc | 225 ++
src/bin/d2/tests/d2_process_unittests.cc | 605 ++++
src/bin/d2/tests/d2_queue_mgr_unittests.cc | 445 +++
src/bin/d2/tests/d2_test.py | 168 ++
src/bin/d2/tests/d2_unittests.cc | 31 +
src/bin/d2/tests/d2_update_message_unittests.cc | 591 ++++
src/bin/d2/tests/d2_update_mgr_unittests.cc | 772 +++++
src/bin/d2/tests/d2_zone_unittests.cc | 75 +
src/bin/d2/tests/d_cfg_mgr_unittests.cc | 386 +++
src/bin/d2/tests/d_controller_unittests.cc | 364 +++
src/bin/d2/tests/d_test_stubs.cc | 319 +++
src/bin/d2/tests/d_test_stubs.h | 667 +++++
src/bin/d2/tests/dns_client_unittests.cc | 425 +++
src/bin/d2/tests/labeled_value_unittests.cc | 107 +
src/bin/d2/tests/nc_add_unittests.cc | 1790 ++++++++++++
src/bin/d2/tests/nc_remove_unittests.cc | 1872 ++++++++++++
src/bin/d2/tests/nc_test_utils.cc | 702 +++++
src/bin/d2/tests/nc_test_utils.h | 372 +++
src/bin/d2/tests/nc_trans_unittests.cc | 1010 +++++++
src/bin/d2/tests/state_model_unittests.cc | 839 ++++++
src/bin/d2/tests/test_data_files_config.h.in | 17 +
src/bin/dbutil/b10-dbutil.xml | 2 +-
src/bin/dbutil/dbutil.py.in | 8 +-
src/bin/dbutil/tests/dbutil_test.sh.in | 17 +
src/bin/ddns/ddns.py.in | 8 +-
src/bin/ddns/ddns_messages.mes | 5 -
src/bin/ddns/tests/ddns_test.py | 1 -
src/bin/dhcp4/.gitignore | 1 +
src/bin/dhcp4/Makefile.am | 9 +-
src/bin/dhcp4/config_parser.cc | 1815 ++----------
src/bin/dhcp4/config_parser.h | 25 +-
src/bin/dhcp4/ctrl_dhcp4_srv.cc | 57 +-
src/bin/dhcp4/ctrl_dhcp4_srv.h | 3 +-
src/bin/dhcp4/dhcp4.dox | 112 +-
src/bin/dhcp4/dhcp4.spec | 144 +-
src/bin/dhcp4/dhcp4_hooks.dox | 216 ++
src/bin/dhcp4/dhcp4_log.h | 3 +
src/bin/dhcp4/dhcp4_messages.mes | 147 +-
src/bin/dhcp4/dhcp4_srv.cc | 1622 +++++++++--
src/bin/dhcp4/dhcp4_srv.h | 394 ++-
src/bin/dhcp4/tests/.gitignore | 3 +
src/bin/dhcp4/tests/Makefile.am | 37 +-
src/bin/dhcp4/tests/callout_library_1.cc | 22 +
src/bin/dhcp4/tests/callout_library_2.cc | 22 +
src/bin/dhcp4/tests/callout_library_common.h | 82 +
src/bin/dhcp4/tests/config_parser_unittest.cc | 970 ++++++-
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc | 70 +-
src/bin/dhcp4/tests/dhcp4_srv_unittest.cc | 3015 ++++++++++++++++----
src/bin/dhcp4/tests/dhcp4_test_utils.cc | 613 ++++
src/bin/dhcp4/tests/dhcp4_test_utils.h | 461 +++
src/bin/dhcp4/tests/fqdn_unittest.cc | 781 +++++
src/bin/dhcp4/tests/marker_file.cc | 66 +
src/bin/dhcp4/tests/marker_file.h.in | 69 +
src/bin/dhcp4/tests/test_data_files_config.h.in | 17 +
src/bin/dhcp4/tests/test_libraries.h.in | 39 +
src/bin/dhcp4/tests/wireshark.cc | 190 ++
src/bin/dhcp6/.gitignore | 1 +
src/bin/dhcp6/Makefile.am | 9 +-
src/bin/dhcp6/config_parser.cc | 1937 +++----------
src/bin/dhcp6/config_parser.h | 13 +-
src/bin/dhcp6/ctrl_dhcp6_srv.cc | 52 +-
src/bin/dhcp6/dhcp6.dox | 131 +-
src/bin/dhcp6/dhcp6.spec | 64 +-
src/bin/dhcp6/dhcp6_hooks.dox | 239 ++
src/bin/dhcp6/dhcp6_log.h | 3 +
src/bin/dhcp6/dhcp6_messages.mes | 288 +-
src/bin/dhcp6/dhcp6_srv.cc | 1639 ++++++++++-
src/bin/dhcp6/dhcp6_srv.h | 269 +-
src/bin/dhcp6/tests/.gitignore | 3 +
src/bin/dhcp6/tests/Makefile.am | 43 +-
src/bin/dhcp6/tests/callout_library_1.cc | 22 +
src/bin/dhcp6/tests/callout_library_2.cc | 22 +
src/bin/dhcp6/tests/callout_library_common.h | 82 +
src/bin/dhcp6/tests/config_parser_unittest.cc | 1081 ++++++-
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc | 84 +-
src/bin/dhcp6/tests/dhcp6_srv_unittest.cc | 1419 ++++-----
src/bin/dhcp6/tests/dhcp6_test_utils.cc | 557 ++++
src/bin/dhcp6/tests/dhcp6_test_utils.h | 503 ++++
src/bin/dhcp6/tests/fqdn_unittest.cc | 778 +++++
src/bin/dhcp6/tests/hooks_unittest.cc | 1434 ++++++++++
src/bin/dhcp6/tests/marker_file.cc | 66 +
src/bin/dhcp6/tests/marker_file.h.in | 69 +
src/bin/dhcp6/tests/test_data_files_config.h.in | 23 +
src/bin/dhcp6/tests/test_libraries.h.in | 37 +
src/bin/dhcp6/tests/wireshark.cc | 303 ++
src/bin/loadzone/.gitignore | 1 -
src/bin/loadzone/b10-loadzone.xml | 33 +
src/bin/loadzone/loadzone.py.in | 70 +-
src/bin/loadzone/loadzone_messages.mes | 5 +
src/bin/loadzone/tests/loadzone_test.py | 37 +-
src/bin/memmgr/.gitignore | 5 +
src/bin/memmgr/Makefile.am | 62 +
src/bin/memmgr/b10-memmgr.xml | 109 +
src/bin/memmgr/memmgr.py.in | 261 ++
src/bin/memmgr/memmgr.spec.pre.in | 25 +
src/bin/memmgr/memmgr_messages.mes | 51 +
src/bin/memmgr/tests/Makefile.am | 30 +
src/bin/memmgr/tests/memmgr_test.py | 341 +++
src/bin/msgq/msgq.py.in | 312 +-
src/bin/msgq/msgq.spec | 14 +-
src/bin/msgq/msgq_messages.mes | 26 +-
src/bin/msgq/tests/msgq_run_test.py | 72 +
src/bin/msgq/tests/msgq_test.py | 255 +-
src/bin/resolver/.gitignore | 1 +
src/bin/resolver/Makefile.am | 10 +-
src/bin/resolver/bench/.gitignore | 1 +
src/bin/resolver/bench/Makefile.am | 3 +-
src/bin/resolver/main.cc | 3 +-
src/bin/resolver/resolver.cc | 21 +-
src/bin/resolver/resolver.h | 4 -
src/bin/resolver/tests/Makefile.am | 1 -
src/bin/stats/stats.py.in | 90 +-
src/bin/stats/stats_httpd.py.in | 6 +-
src/bin/stats/stats_messages.mes | 11 +
src/bin/stats/tests/stats-httpd_test.py | 1 +
src/bin/stats/tests/stats_test.py | 297 +-
src/bin/stats/tests/test_utils.py | 25 +-
src/bin/tests/process_rename_test.py.in | 5 +
src/bin/usermgr/b10-cmdctl-usermgr.py.in | 2 +-
src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py | 34 +-
src/bin/xfrin/b10-xfrin.xml | 262 +-
src/bin/xfrin/tests/xfrin_test.py | 815 ++++--
src/bin/xfrin/xfrin.py.in | 811 +++---
src/bin/xfrin/xfrin.spec | 483 +++-
src/bin/xfrin/xfrin_messages.mes | 77 +-
src/bin/xfrout/b10-xfrout.xml | 70 +-
src/bin/xfrout/tests/xfrout_test.py.in | 106 +-
src/bin/xfrout/xfrout.py.in | 68 +-
src/bin/xfrout/xfrout.spec.pre.in | 98 +-
src/bin/zonemgr/tests/zonemgr_test.py | 137 +-
src/bin/zonemgr/zonemgr.py.in | 259 +-
src/bin/zonemgr/zonemgr_messages.mes | 12 +-
src/hooks/Makefile.am | 1 +
src/hooks/dhcp/Makefile.am | 1 +
src/hooks/dhcp/user_chk/Makefile.am | 68 +
src/hooks/dhcp/user_chk/libdhcp_user_chk.dox | 207 ++
src/hooks/dhcp/user_chk/load_unload.cc | 132 +
src/hooks/dhcp/user_chk/pkt_receive_co.cc | 144 +
src/hooks/dhcp/user_chk/pkt_send_co.cc | 541 ++++
src/hooks/dhcp/user_chk/subnet_select_co.cc | 144 +
src/hooks/dhcp/user_chk/tests/.gitignore | 2 +
src/hooks/dhcp/user_chk/tests/Makefile.am | 77 +
src/hooks/dhcp/user_chk/tests/run_unittests.cc | 27 +
.../user_chk/tests/test_data_files_config.h.in | 16 +
src/hooks/dhcp/user_chk/tests/test_users_1.txt | 4 +
src/hooks/dhcp/user_chk/tests/test_users_err.txt | 2 +
.../dhcp/user_chk/tests/user_file_unittests.cc | 171 ++
.../dhcp/user_chk/tests/user_registry_unittests.cc | 217 ++
src/hooks/dhcp/user_chk/tests/user_unittests.cc | 97 +
src/hooks/dhcp/user_chk/tests/userid_unittests.cc | 149 +
src/hooks/dhcp/user_chk/user.cc | 221 ++
src/hooks/dhcp/user_chk/user.h | 253 ++
src/hooks/dhcp/user_chk/user_chk.h | 51 +
src/hooks/dhcp/user_chk/user_chk_log.cc | 22 +
src/hooks/dhcp/user_chk/user_chk_log.h | 29 +
src/hooks/dhcp/user_chk/user_chk_messages.mes | 43 +
src/hooks/dhcp/user_chk/user_data_source.h | 79 +
src/hooks/dhcp/user_chk/user_file.cc | 162 ++
src/hooks/dhcp/user_chk/user_file.h | 140 +
src/hooks/dhcp/user_chk/user_registry.cc | 125 +
src/hooks/dhcp/user_chk/user_registry.h | 132 +
src/hooks/dhcp/user_chk/version.cc | 25 +
src/lib/Makefile.am | 11 +-
src/lib/asiodns/.gitignore | 1 +
src/lib/asiodns/Makefile.am | 14 +-
src/lib/asiodns/README | 5 +-
src/lib/asiodns/asiodns_messages.mes | 6 +
src/lib/asiodns/dns_service.cc | 16 +-
src/lib/asiodns/dns_service.h | 6 +-
src/lib/asiodns/io_fetch.cc | 3 +-
src/lib/asiodns/sync_udp_server.cc | 24 +-
src/lib/asiodns/sync_udp_server.h | 50 +-
src/lib/asiodns/tcp_server.cc | 60 +-
src/lib/asiodns/tcp_server.h | 3 -
src/lib/asiodns/tests/Makefile.am | 4 -
src/lib/asiodns/tests/dns_server_unittest.cc | 167 +-
src/lib/asiodns/tests/dns_service_unittest.cc | 2 +-
src/lib/asiodns/udp_server.cc | 36 +-
src/lib/asiodns/udp_server.h | 2 -
src/lib/asiolink/Makefile.am | 11 +-
src/lib/asiolink/io_address.cc | 6 +
src/lib/asiolink/io_address.h | 16 +
src/lib/asiolink/io_asio_socket.h | 8 +-
src/lib/asiolink/io_endpoint.cc | 4 +-
src/lib/asiolink/local_socket.cc | 102 +
src/lib/asiolink/local_socket.h | 132 +
src/lib/asiolink/tcp_socket.h | 1 -
src/lib/asiolink/tests/Makefile.am | 5 +-
src/lib/asiolink/tests/io_address_unittest.cc | 12 +-
src/lib/asiolink/tests/io_endpoint_unittest.cc | 8 +-
src/lib/asiolink/tests/io_socket_unittest.cc | 6 +-
src/lib/asiolink/tests/local_socket_unittest.cc | 253 ++
src/lib/asiolink/tests/tcp_endpoint_unittest.cc | 4 +-
src/lib/asiolink/tests/udp_endpoint_unittest.cc | 4 +-
src/lib/asiolink/udp_socket.h | 1 -
src/lib/cache/.gitignore | 1 +
src/lib/cache/Makefile.am | 7 +-
src/lib/cc/.gitignore | 1 +
src/lib/cc/Makefile.am | 13 +-
src/lib/cc/data.cc | 81 +-
src/lib/cc/data.h | 43 +-
src/lib/cc/proto_defs.cc | 3 +
src/lib/cc/session.cc | 2 +-
src/lib/cc/tests/Makefile.am | 3 -
src/lib/cc/tests/data_unittests.cc | 100 +-
src/lib/config/.gitignore | 1 +
src/lib/config/Makefile.am | 7 +-
src/lib/config/ccsession.cc | 104 +-
src/lib/config/ccsession.h | 126 +
src/lib/config/config_data.h | 2 +-
src/lib/config/tests/ccsession_unittests.cc | 136 +
src/lib/config/tests/fake_session.cc | 8 +-
src/lib/datasrc/.gitignore | 2 +
src/lib/datasrc/Makefile.am | 12 +-
src/lib/datasrc/cache_config.cc | 2 +-
src/lib/datasrc/cache_config.h | 12 +-
src/lib/datasrc/client.h | 28 +-
src/lib/datasrc/client_list.cc | 224 +-
src/lib/datasrc/client_list.h | 170 +-
src/lib/datasrc/database.cc | 2 +-
src/lib/datasrc/datasrc_messages.mes | 28 +-
src/lib/datasrc/exceptions.h | 31 +-
src/lib/datasrc/factory.cc | 4 +-
src/lib/datasrc/factory.h | 15 +-
src/lib/datasrc/memory/.gitignore | 1 +
src/lib/datasrc/memory/Makefile.am | 16 +-
src/lib/datasrc/memory/domaintree.h | 1315 +++++++--
src/lib/datasrc/memory/memory_client.cc | 11 +-
src/lib/datasrc/memory/memory_messages.mes | 6 +
src/lib/datasrc/memory/rdataset.cc | 166 +-
src/lib/datasrc/memory/rdataset.h | 62 +
src/lib/datasrc/memory/segment_object_holder.cc | 41 +
src/lib/datasrc/memory/segment_object_holder.h | 82 +-
src/lib/datasrc/memory/treenode_rrset.cc | 5 -
src/lib/datasrc/memory/treenode_rrset.h | 7 +-
src/lib/datasrc/memory/zone_data.cc | 19 +-
src/lib/datasrc/memory/zone_data.h | 63 +-
src/lib/datasrc/memory/zone_data_loader.cc | 70 +-
src/lib/datasrc/memory/zone_data_loader.h | 2 +-
src/lib/datasrc/memory/zone_data_updater.cc | 74 +-
src/lib/datasrc/memory/zone_data_updater.h | 43 +-
src/lib/datasrc/memory/zone_finder.cc | 2 +-
src/lib/datasrc/memory/zone_table.cc | 72 +-
src/lib/datasrc/memory/zone_table.h | 109 +-
src/lib/datasrc/memory/zone_table_segment.cc | 10 +
src/lib/datasrc/memory/zone_table_segment.h | 257 +-
src/lib/datasrc/memory/zone_table_segment_local.cc | 32 +-
src/lib/datasrc/memory/zone_table_segment_local.h | 65 +-
.../datasrc/memory/zone_table_segment_mapped.cc | 392 +++
src/lib/datasrc/memory/zone_table_segment_mapped.h | 144 +
src/lib/datasrc/memory/zone_writer.cc | 175 ++
src/lib/datasrc/memory/zone_writer.h | 91 +-
src/lib/datasrc/memory/zone_writer_local.cc | 93 -
src/lib/datasrc/result.h | 25 +-
src/lib/datasrc/sqlite3_accessor.cc | 3 +-
src/lib/datasrc/sqlite3_datasrc_messages.mes | 5 +-
src/lib/datasrc/static.zone.pre | 12 +-
src/lib/datasrc/tests/cache_config_unittest.cc | 2 +-
src/lib/datasrc/tests/client_list_unittest.cc | 723 +++--
src/lib/datasrc/tests/database_unittest.cc | 7 +-
src/lib/datasrc/tests/factory_unittest.cc | 2 +
src/lib/datasrc/tests/memory/Makefile.am | 18 +-
.../datasrc/tests/memory/domaintree_unittest.cc | 570 +++-
.../datasrc/tests/memory/memory_client_unittest.cc | 52 +-
src/lib/datasrc/tests/memory/memory_segment_mock.h | 62 +
src/lib/datasrc/tests/memory/memory_segment_test.h | 62 -
.../tests/memory/rdata_serialization_unittest.cc | 2 +-
src/lib/datasrc/tests/memory/rdataset_unittest.cc | 219 +-
.../tests/memory/rrset_collection_unittest.cc | 24 +-
.../tests/memory/segment_object_holder_unittest.cc | 78 +-
src/lib/datasrc/tests/memory/testdata/Makefile.am | 1 +
.../datasrc/tests/memory/testdata/template.zone | 4 +
.../tests/memory/treenode_rrset_unittest.cc | 1 -
.../tests/memory/zone_data_loader_unittest.cc | 48 +-
src/lib/datasrc/tests/memory/zone_data_unittest.cc | 17 +-
.../tests/memory/zone_data_updater_unittest.cc | 207 +-
.../datasrc/tests/memory/zone_finder_unittest.cc | 71 +-
src/lib/datasrc/tests/memory/zone_loader_util.cc | 28 +-
src/lib/datasrc/tests/memory/zone_loader_util.h | 7 +-
.../memory/zone_table_segment_mapped_unittest.cc | 566 ++++
.../datasrc/tests/memory/zone_table_segment_mock.h | 94 +
.../datasrc/tests/memory/zone_table_segment_test.h | 116 -
.../tests/memory/zone_table_segment_unittest.cc | 41 +-
.../datasrc/tests/memory/zone_table_unittest.cc | 124 +-
.../datasrc/tests/memory/zone_writer_unittest.cc | 226 +-
src/lib/datasrc/tests/mock_client.cc | 2 +-
src/lib/datasrc/tests/mock_client.h | 7 +
src/lib/datasrc/tests/test_client.cc | 10 +-
.../datasrc/tests/zone_finder_context_unittest.cc | 23 +-
src/lib/datasrc/tests/zone_loader_unittest.cc | 7 +-
src/lib/datasrc/zone_table_accessor.h | 3 +
src/lib/dhcp/Makefile.am | 45 +-
src/lib/dhcp/dhcp4.h | 25 +-
src/lib/dhcp/docsis3_option_defs.h | 71 +
src/lib/dhcp/duid.cc | 6 +-
src/lib/dhcp/duid.h | 26 +-
src/lib/dhcp/hwaddr.h | 6 +-
src/lib/dhcp/iface_mgr.cc | 719 ++---
src/lib/dhcp/iface_mgr.h | 515 +++-
src/lib/dhcp/iface_mgr_bsd.cc | 108 +-
src/lib/dhcp/iface_mgr_linux.cc | 19 +-
src/lib/dhcp/iface_mgr_sun.cc | 113 +-
src/lib/dhcp/libdhcp++.cc | 419 ++-
src/lib/dhcp/libdhcp++.dox | 176 +-
src/lib/dhcp/libdhcp++.h | 76 +-
src/lib/dhcp/option.cc | 30 +-
src/lib/dhcp/option.h | 71 +-
src/lib/dhcp/option4_client_fqdn.cc | 525 ++++
src/lib/dhcp/option4_client_fqdn.h | 370 +++
src/lib/dhcp/option6_addrlst.cc | 6 +-
src/lib/dhcp/option6_client_fqdn.cc | 455 +++
src/lib/dhcp/option6_client_fqdn.h | 271 ++
src/lib/dhcp/option6_ia.cc | 24 +-
src/lib/dhcp/option6_ia.h | 9 +-
src/lib/dhcp/option6_iaaddr.cc | 14 +-
src/lib/dhcp/option6_iaaddr.h | 14 +-
src/lib/dhcp/option6_iaprefix.cc | 126 +
src/lib/dhcp/option6_iaprefix.h | 110 +
src/lib/dhcp/option_custom.cc | 66 +-
src/lib/dhcp/option_custom.h | 11 +-
src/lib/dhcp/option_data_types.cc | 25 +-
src/lib/dhcp/option_data_types.h | 16 +-
src/lib/dhcp/option_definition.cc | 289 +-
src/lib/dhcp/option_definition.h | 130 +-
src/lib/dhcp/option_int.h | 8 +-
src/lib/dhcp/option_int_array.h | 4 +-
src/lib/dhcp/option_string.cc | 86 +
src/lib/dhcp/option_string.h | 114 +
src/lib/dhcp/option_vendor.cc | 85 +
src/lib/dhcp/option_vendor.h | 106 +
src/lib/dhcp/pkt4.cc | 260 +-
src/lib/dhcp/pkt4.h | 238 +-
src/lib/dhcp/pkt6.cc | 195 +-
src/lib/dhcp/pkt6.h | 150 +-
src/lib/dhcp/pkt_filter.cc | 66 +
src/lib/dhcp/pkt_filter.h | 87 +-
src/lib/dhcp/pkt_filter6.cc | 44 +
src/lib/dhcp/pkt_filter6.h | 150 +
src/lib/dhcp/pkt_filter_inet.cc | 46 +-
src/lib/dhcp/pkt_filter_inet.h | 48 +-
src/lib/dhcp/pkt_filter_inet6.cc | 286 ++
src/lib/dhcp/pkt_filter_inet6.h | 98 +
src/lib/dhcp/pkt_filter_lpf.cc | 264 +-
src/lib/dhcp/pkt_filter_lpf.h | 49 +-
src/lib/dhcp/protocol_util.cc | 243 ++
src/lib/dhcp/protocol_util.h | 153 +
src/lib/dhcp/std_option_defs.h | 22 +-
src/lib/dhcp/tests/Makefile.am | 16 +
src/lib/dhcp/tests/iface_mgr_unittest.cc | 1591 ++++++++++-
src/lib/dhcp/tests/libdhcp++_unittest.cc | 362 ++-
src/lib/dhcp/tests/option4_client_fqdn_unittest.cc | 959 +++++++
src/lib/dhcp/tests/option6_client_fqdn_unittest.cc | 809 ++++++
src/lib/dhcp/tests/option6_ia_unittest.cc | 186 +-
src/lib/dhcp/tests/option6_iaaddr_unittest.cc | 15 +
src/lib/dhcp/tests/option6_iaprefix_unittest.cc | 188 ++
src/lib/dhcp/tests/option_custom_unittest.cc | 214 +-
src/lib/dhcp/tests/option_data_types_unittest.cc | 20 +-
src/lib/dhcp/tests/option_definition_unittest.cc | 78 +-
src/lib/dhcp/tests/option_string_unittest.cc | 160 ++
src/lib/dhcp/tests/option_unittest.cc | 133 +
src/lib/dhcp/tests/option_vendor_unittest.cc | 240 ++
src/lib/dhcp/tests/pkt4_unittest.cc | 559 ++--
src/lib/dhcp/tests/pkt6_unittest.cc | 328 ++-
src/lib/dhcp/tests/pkt_filter6_test_utils.cc | 212 ++
src/lib/dhcp/tests/pkt_filter6_test_utils.h | 160 ++
src/lib/dhcp/tests/pkt_filter_inet6_unittest.cc | 140 +
src/lib/dhcp/tests/pkt_filter_inet_unittest.cc | 153 +
src/lib/dhcp/tests/pkt_filter_lpf_unittest.cc | 192 ++
src/lib/dhcp/tests/pkt_filter_test_utils.cc | 194 ++
src/lib/dhcp/tests/pkt_filter_test_utils.h | 167 ++
src/lib/dhcp/tests/pkt_filter_unittest.cc | 74 +
src/lib/dhcp/tests/protocol_util_unittest.cc | 392 +++
src/lib/dhcp_ddns/.gitignore | 3 +
src/lib/dhcp_ddns/Makefile.am | 60 +
src/lib/dhcp_ddns/dhcp_ddns_log.cc | 27 +
src/lib/dhcp_ddns/dhcp_ddns_log.h | 31 +
src/lib/dhcp_ddns/dhcp_ddns_messages.mes | 76 +
src/lib/dhcp_ddns/libdhcp_ddns.dox | 47 +
src/lib/dhcp_ddns/ncr_io.cc | 322 +++
src/lib/dhcp_ddns/ncr_io.h | 700 +++++
src/lib/dhcp_ddns/ncr_msg.cc | 641 +++++
src/lib/dhcp_ddns/ncr_msg.h | 638 +++++
src/lib/dhcp_ddns/ncr_udp.cc | 328 +++
src/lib/dhcp_ddns/ncr_udp.h | 562 ++++
src/lib/dhcp_ddns/tests/.gitignore | 1 +
src/lib/dhcp_ddns/tests/Makefile.am | 58 +
src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc | 518 ++++
src/lib/dhcp_ddns/tests/ncr_unittests.cc | 633 ++++
src/lib/{dhcp => dhcp_ddns}/tests/run_unittests.cc | 0
src/lib/dhcpsrv/.gitignore | 1 +
src/lib/dhcpsrv/Makefile.am | 17 +-
src/lib/dhcpsrv/alloc_engine.cc | 597 +++-
src/lib/dhcpsrv/alloc_engine.h | 271 +-
src/lib/dhcpsrv/callout_handle_store.h | 91 +
src/lib/dhcpsrv/cfgmgr.cc | 131 +-
src/lib/dhcpsrv/cfgmgr.h | 153 +-
src/lib/dhcpsrv/d2_client.cc | 185 ++
src/lib/dhcpsrv/d2_client.h | 278 ++
src/lib/dhcpsrv/database_backends.dox | 45 +-
src/lib/dhcpsrv/dhcp_config_parser.h | 73 -
src/lib/dhcpsrv/dhcp_parsers.cc | 1271 +++++++++
src/lib/dhcpsrv/dhcp_parsers.h | 960 +++++++
src/lib/dhcpsrv/dhcpdb_create.mysql | 15 +-
src/lib/dhcpsrv/dhcpsrv_log.h | 3 +
src/lib/dhcpsrv/dhcpsrv_messages.mes | 59 +-
src/lib/dhcpsrv/lease.cc | 257 ++
src/lib/dhcpsrv/lease.h | 402 +++
src/lib/dhcpsrv/lease_mgr.cc | 131 +-
src/lib/dhcpsrv/lease_mgr.h | 327 +--
src/lib/dhcpsrv/libdhcpsrv.dox | 54 +-
src/lib/dhcpsrv/memfile_lease_mgr.cc | 148 +-
src/lib/dhcpsrv/memfile_lease_mgr.h | 124 +-
src/lib/dhcpsrv/mysql_lease_mgr.cc | 312 +-
src/lib/dhcpsrv/mysql_lease_mgr.h | 31 +-
src/lib/dhcpsrv/option_space_container.h | 19 +-
src/lib/dhcpsrv/pool.cc | 68 +-
src/lib/dhcpsrv/pool.h | 108 +-
src/lib/dhcpsrv/subnet.cc | 246 +-
src/lib/dhcpsrv/subnet.h | 250 +-
src/lib/dhcpsrv/tests/.gitignore | 1 +
src/lib/dhcpsrv/tests/Makefile.am | 38 +-
src/lib/dhcpsrv/tests/alloc_engine_unittest.cc | 1357 +++++++--
.../dhcpsrv/tests/callout_handle_store_unittest.cc | 122 +
src/lib/dhcpsrv/tests/callout_library.cc | 31 +
src/lib/dhcpsrv/tests/cfgmgr_unittest.cc | 287 +-
src/lib/dhcpsrv/tests/d2_client_unittest.cc | 294 ++
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc | 1269 ++++++++
src/lib/dhcpsrv/tests/lease_mgr_unittest.cc | 359 ++-
src/lib/dhcpsrv/tests/lease_unittest.cc | 71 +
.../dhcpsrv/tests/memfile_lease_mgr_unittest.cc | 66 +-
src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc | 608 ++--
src/lib/dhcpsrv/tests/pool_unittest.cc | 114 +-
src/lib/dhcpsrv/tests/schema_copy.h | 20 +-
src/lib/dhcpsrv/tests/subnet_unittest.cc | 338 ++-
src/lib/dhcpsrv/tests/test_get_callout_handle.cc | 31 +
src/lib/dhcpsrv/tests/test_get_callout_handle.h | 46 +
src/lib/dhcpsrv/tests/test_libraries.h.in | 37 +
src/lib/dhcpsrv/tests/test_utils.cc | 515 +++-
src/lib/dhcpsrv/tests/test_utils.h | 88 +-
src/lib/dns/.gitignore | 1 +
src/lib/dns/Makefile.am | 8 +-
src/lib/dns/gen-rdatacode.py.in | 52 +-
src/lib/dns/master_lexer.h | 4 +-
src/lib/dns/master_loader.cc | 44 +-
src/lib/dns/masterload.cc | 205 +-
src/lib/dns/masterload.h | 88 +-
src/lib/dns/message.cc | 30 +-
src/lib/dns/message.h | 12 +-
src/lib/dns/messagerenderer.h | 1 -
src/lib/dns/python/.gitignore | 1 +
src/lib/dns/python/Makefile.am | 3 +-
src/lib/dns/python/pydnspp_common.cc | 3 +-
src/lib/dns/python/pydnspp_common.h | 7 +-
src/lib/dns/python/pydnspp_config.h.in | 25 +
src/lib/dns/python/rrset_python.cc | 13 -
src/lib/dns/python/tests/question_python_test.py | 6 +-
src/lib/dns/python/tests/rrset_python_test.py | 5 -
src/lib/dns/question.cc | 13 +-
src/lib/dns/question.h | 12 +-
src/lib/dns/rdata.cc | 3 +
src/lib/dns/rdata/any_255/tsig_250.cc | 273 +-
src/lib/dns/rdata/any_255/tsig_250.h | 14 +-
src/lib/dns/rdata/ch_3/a_1.cc | 8 +-
src/lib/dns/rdata/generic/detail/ds_like.h | 12 +-
src/lib/dns/rdata/generic/dlv_32769.cc | 2 +-
src/lib/dns/rdata/generic/dnskey_48.cc | 2 +-
src/lib/dns/rdata/generic/ds_43.cc | 2 +-
src/lib/dns/rdata/generic/minfo_14.cc | 73 +-
src/lib/dns/rdata/generic/minfo_14.h | 6 +-
src/lib/dns/rdata/generic/nsec3_50.cc | 2 +-
src/lib/dns/rdata/generic/nsec3param_51.cc | 2 +-
src/lib/dns/rdata/generic/nsec_47.cc | 2 +-
src/lib/dns/rdata/generic/rp_17.cc | 86 +-
src/lib/dns/rdata/generic/rp_17.h | 12 +-
src/lib/dns/rdata/generic/rrsig_46.cc | 2 +-
src/lib/dns/rdata/generic/spf_99.cc | 2 +-
src/lib/dns/rdata/generic/sshfp_44.cc | 239 +-
src/lib/dns/rdata/generic/sshfp_44.h | 17 +-
src/lib/dns/rdata/generic/txt_16.cc | 2 +-
src/lib/dns/rdata/hs_4/a_1.cc | 8 +-
src/lib/dns/rdata/in_1/srv_33.cc | 2 +-
src/lib/dns/rrclass-placeholder.h | 24 +-
src/lib/dns/rrclass.cc | 6 +-
src/lib/dns/rrparamregistry-placeholder.cc | 44 +-
src/lib/dns/rrparamregistry.h | 17 +-
src/lib/dns/rrset.cc | 99 +-
src/lib/dns/rrset.h | 25 -
src/lib/dns/rrttl.cc | 12 +-
src/lib/dns/rrttl.h | 24 +-
src/lib/dns/serial.h | 5 +-
src/lib/dns/tests/masterload_unittest.cc | 14 +-
src/lib/dns/tests/question_unittest.cc | 10 +-
src/lib/dns/tests/rdata_ds_like_unittest.cc | 53 +-
src/lib/dns/tests/rdata_minfo_unittest.cc | 113 +-
src/lib/dns/tests/rdata_rp_unittest.cc | 72 +-
src/lib/dns/tests/rdata_sshfp_unittest.cc | 97 +-
src/lib/dns/tests/rdata_tsig_unittest.cc | 202 +-
src/lib/dns/tests/rrclass_unittest.cc | 14 +-
src/lib/dns/tests/rrparamregistry_unittest.cc | 7 +-
src/lib/dns/tests/rrset_unittest.cc | 38 +-
src/lib/dns/tests/rrttl_unittest.cc | 19 +-
src/lib/dns/tests/tsigkey_unittest.cc | 11 +
src/lib/dns/tsigkey.cc | 14 +-
src/lib/dns/tsigkey.h | 1 +
src/lib/hooks/.gitignore | 3 +
src/lib/hooks/Makefile.am | 67 +
src/lib/hooks/callout_handle.cc | 163 ++
src/lib/hooks/callout_handle.h | 390 +++
src/lib/hooks/callout_manager.cc | 259 ++
src/lib/hooks/callout_manager.h | 384 +++
src/lib/hooks/hooks.cc | 34 +
src/lib/hooks/hooks.h | 77 +
src/lib/hooks/hooks_component_developer.dox | 483 ++++
src/lib/hooks/hooks_log.cc | 26 +
src/lib/hooks/hooks_log.h | 50 +
src/lib/hooks/hooks_maintenance.dox | 382 +++
src/lib/hooks/hooks_manager.cc | 200 ++
src/lib/hooks/hooks_manager.h | 316 ++
src/lib/hooks/hooks_messages.mes | 177 ++
src/lib/hooks/hooks_user.dox | 1075 +++++++
src/lib/hooks/images/DataScopeArgument.dia | Bin 0 -> 1887 bytes
src/lib/hooks/images/DataScopeArgument.png | Bin 0 -> 11672 bytes
src/lib/hooks/images/DataScopeContext.dia | Bin 0 -> 2161 bytes
src/lib/hooks/images/DataScopeContext.png | Bin 0 -> 14180 bytes
src/lib/hooks/images/HooksUml.dia | Bin 0 -> 2354 bytes
src/lib/hooks/images/HooksUml.png | Bin 0 -> 13841 bytes
src/lib/hooks/library_handle.cc | 76 +
src/lib/hooks/library_handle.h | 149 +
src/lib/hooks/library_manager.cc | 362 +++
src/lib/hooks/library_manager.h | 231 ++
src/lib/hooks/library_manager_collection.cc | 129 +
src/lib/hooks/library_manager_collection.h | 170 ++
src/lib/hooks/pointer_converter.h | 121 +
src/lib/hooks/server_hooks.cc | 159 ++
src/lib/hooks/server_hooks.h | 183 ++
src/lib/hooks/tests/.gitignore | 4 +
src/lib/hooks/tests/Makefile.am | 146 +
src/lib/hooks/tests/basic_callout_library.cc | 124 +
src/lib/hooks/tests/callout_handle_unittest.cc | 329 +++
src/lib/hooks/tests/callout_manager_unittest.cc | 882 ++++++
src/lib/hooks/tests/common_test_class.h | 142 +
src/lib/hooks/tests/framework_exception_library.cc | 47 +
src/lib/hooks/tests/full_callout_library.cc | 141 +
src/lib/hooks/tests/handles_unittest.cc | 974 +++++++
src/lib/hooks/tests/hooks_manager_unittest.cc | 524 ++++
src/lib/hooks/tests/incorrect_version_library.cc | 33 +
.../tests/library_manager_collection_unittest.cc | 251 ++
src/lib/hooks/tests/library_manager_unittest.cc | 569 ++++
src/lib/hooks/tests/load_callout_library.cc | 123 +
src/lib/hooks/tests/load_error_callout_library.cc | 49 +
src/lib/hooks/tests/marker_file.h.in | 27 +
src/lib/hooks/tests/no_version_library.cc | 30 +
src/lib/hooks/tests/run_unittests.cc | 25 +
src/lib/hooks/tests/server_hooks_unittest.cc | 178 ++
src/lib/hooks/tests/test_libraries.h.in | 60 +
src/lib/hooks/tests/unload_callout_library.cc | 52 +
src/lib/log/Makefile.am | 20 +-
src/lib/log/dummylog.cc | 37 -
src/lib/log/dummylog.h | 61 -
src/lib/log/interprocess/Makefile.am | 21 +
src/lib/log/interprocess/README | 13 +
src/lib/log/interprocess/interprocess_sync.h | 151 +
src/lib/log/interprocess/interprocess_sync_file.cc | 134 +
src/lib/log/interprocess/interprocess_sync_file.h | 93 +
src/lib/log/interprocess/interprocess_sync_null.cc | 44 +
src/lib/log/interprocess/interprocess_sync_null.h | 66 +
.../log/interprocess}/tests/.gitignore | 0
src/lib/log/interprocess/tests/Makefile.am | 37 +
.../tests/interprocess_sync_file_unittest.cc | 151 +
.../tests/interprocess_sync_null_unittest.cc | 75 +
src/lib/log/interprocess/tests/run_unittests.cc | 25 +
src/lib/log/logger.cc | 7 +-
src/lib/log/logger.h | 14 +-
src/lib/log/logger_impl.cc | 10 +-
src/lib/log/logger_impl.h | 10 +-
src/lib/log/logger_manager.cc | 5 +-
src/lib/log/logger_manager.h | 4 +-
src/lib/log/logger_manager_impl.cc | 23 +-
src/lib/log/message_exception.h | 4 +-
src/lib/log/message_reader.cc | 2 +-
src/lib/log/tests/.gitignore | 1 +
src/lib/log/tests/Makefile.am | 15 +-
src/lib/log/tests/buffer_logger_test.cc | 5 +-
src/lib/log/tests/logger_example.cc | 10 +-
src/lib/log/tests/logger_lock_test.cc | 6 +-
src/lib/log/tests/logger_manager_unittest.cc | 87 +-
src/lib/log/tests/logger_unittest.cc | 5 +-
src/lib/nsas/.gitignore | 1 +
src/lib/nsas/Makefile.am | 8 +-
src/lib/nsas/README | 5 -
src/lib/nsas/address_request_callback.h | 1 -
src/lib/nsas/asiolink.h | 21 -
src/lib/nsas/nameserver_address.h | 1 -
src/lib/nsas/nameserver_entry.h | 1 -
.../tests/nameserver_address_store_unittest.cc | 20 +-
src/lib/nsas/tests/nameserver_entry_unittest.cc | 1 -
src/lib/nsas/tests/zone_entry_unittest.cc | 15 +-
src/lib/nsas/zone_entry.h | 1 -
src/lib/python/bind10_config.py.in | 25 +
src/lib/python/isc/Makefile.am | 4 +
src/lib/python/isc/config/ccsession.py | 120 +-
src/lib/python/isc/config/config_data.py | 15 +-
src/lib/python/isc/config/tests/ccsession_test.py | 97 +
.../python/isc/config/tests/config_data_test.py | 2 +
src/lib/python/isc/datasrc/Makefile.am | 6 +
.../isc/datasrc/configurableclientlist_inc.cc | 134 +
.../isc/datasrc/configurableclientlist_python.cc | 295 +-
src/lib/python/isc/datasrc/datasrc.cc | 55 +
src/lib/python/isc/datasrc/finder_python.cc | 4 +-
src/lib/python/isc/datasrc/iterator_python.cc | 2 +-
src/lib/python/isc/datasrc/tests/Makefile.am | 18 +-
.../python/isc/datasrc/tests/clientlist_test.py | 330 ++-
.../python/isc/datasrc/tests/testdata/Makefile.am | 12 +
.../datasrc/tests/testdata/example.com-broken.zone | 6 +
.../isc/datasrc/zonetable_accessor_python.cc | 182 ++
.../python/isc/datasrc/zonetable_accessor_python.h | 43 +
.../isc/datasrc/zonetable_iterator_python.cc | 197 ++
.../python/isc/datasrc/zonetable_iterator_python.h | 44 +
src/lib/python/isc/datasrc/zonewriter_inc.cc | 103 +
src/lib/python/isc/datasrc/zonewriter_python.cc | 253 ++
src/lib/python/isc/datasrc/zonewriter_python.h | 50 +
src/lib/python/isc/ddns/libddns_messages.mes | 10 +
src/lib/python/isc/ddns/session.py | 23 +
src/lib/python/isc/ddns/tests/session_tests.py | 10 +
src/lib/python/isc/log_messages/Makefile.am | 6 +
.../python/isc/log_messages/libmemmgr_messages.py | 1 +
src/lib/python/isc/log_messages/memmgr_messages.py | 1 +
src/lib/python/isc/log_messages/util_messages.py | 1 +
src/lib/python/isc/log_messages/work/Makefile.am | 2 +
src/lib/python/isc/log_messages/work/README | 5 +
src/lib/python/isc/memmgr/Makefile.am | 25 +
src/lib/python/isc/{bind10 => memmgr}/__init__.py | 0
src/lib/python/isc/memmgr/builder.py | 185 ++
src/lib/python/isc/memmgr/datasrc_info.py | 413 +++
src/lib/python/isc/memmgr/libmemmgr_messages.mes | 35 +
src/lib/python/isc/memmgr/logger.py | 20 +
src/lib/python/isc/memmgr/tests/Makefile.am | 36 +
src/lib/python/isc/memmgr/tests/builder_tests.py | 240 ++
.../python/isc/memmgr/tests/datasrc_info_tests.py | 469 +++
.../python/isc/memmgr/tests/testdata/Makefile.am | 2 +
.../tests/testdata/example.com.zone} | 0
src/lib/python/isc/notify/notify_out.py | 23 +-
src/lib/python/isc/notify/tests/Makefile.am | 2 +-
src/lib/python/isc/notify/tests/notify_out_test.py | 28 +-
.../isc/notify/tests/testdata/test_spec1.spec | 57 +
src/lib/python/isc/server_common/.gitignore | 1 +
src/lib/python/isc/server_common/Makefile.am | 2 +
src/lib/python/isc/server_common/bind10_server.py | 251 ++
.../isc/server_common/datasrc_clients_mgr.py | 173 ++
.../isc/server_common/server_common_messages.mes | 20 +
src/lib/python/isc/server_common/tests/.gitignore | 2 +
src/lib/python/isc/server_common/tests/Makefile.am | 13 +-
.../isc/server_common/tests/bind10_server_test.py | 294 ++
.../tests/datasrc_clients_mgr_test.py | 142 +
src/lib/python/isc/statistics/Makefile.am | 2 +-
src/lib/python/isc/statistics/counters.py | 272 +-
src/lib/python/isc/statistics/dns.py | 166 ++
src/lib/python/isc/statistics/tests/Makefile.am | 2 +-
.../python/isc/statistics/tests/counters_test.py | 262 +-
src/lib/python/isc/statistics/tests/dns_test.py | 224 ++
.../isc/statistics/tests/testdata/test_spec2.spec | 96 +-
.../isc/statistics/tests/testdata/test_spec3.spec | 204 +-
src/lib/python/isc/util/Makefile.am | 17 +-
src/lib/python/isc/util/tests/Makefile.am | 2 +-
.../isc/util/tests/address_formatter_test.py | 2 -
.../isc/util/tests/traceback_handler_test.py | 101 +
src/lib/python/isc/util/traceback_handler.py | 39 +
src/lib/python/isc/util/util_messages.mes | 26 +
src/lib/resolve/.gitignore | 1 +
src/lib/resolve/Makefile.am | 13 +-
src/lib/resolve/recursive_query.cc | 5 -
src/lib/resolve/tests/recursive_query_unittest.cc | 10 +-
.../resolve/tests/recursive_query_unittest_2.cc | 2 +-
.../resolve/tests/recursive_query_unittest_3.cc | 16 +-
src/lib/server_common/.gitignore | 1 +
src/lib/server_common/Makefile.am | 7 +-
src/lib/server_common/portconfig.cc | 3 +
src/lib/server_common/tests/client_unittest.cc | 6 +-
src/lib/statistics/counter.h | 8 +-
src/lib/statistics/tests/Makefile.am | 4 -
src/lib/statistics/tests/counter_unittest.cc | 5 +
src/lib/testutils/Makefile.am | 3 +-
src/lib/testutils/mockups.h | 2 +
src/lib/util/Makefile.am | 6 +-
src/lib/util/buffer.h | 18 +-
src/lib/util/interprocess_sync.h | 149 -
src/lib/util/interprocess_sync_file.cc | 132 -
src/lib/util/interprocess_sync_file.h | 91 -
src/lib/util/interprocess_sync_null.cc | 42 -
src/lib/util/interprocess_sync_null.h | 64 -
src/lib/util/memory_segment.h | 71 +-
src/lib/util/memory_segment_local.cc | 11 +-
src/lib/util/memory_segment_local.h | 2 +-
src/lib/util/memory_segment_mapped.cc | 114 +-
src/lib/util/memory_segment_mapped.h | 11 +-
src/lib/util/python/.gitignore | 1 +
src/lib/util/python/Makefile.am | 2 +-
src/lib/util/python/doxygen2pydoc.py.in | 680 +++++
src/lib/util/random/qid_gen.h | 2 +
src/lib/util/random/random_number_generator.h | 1 +
src/lib/util/range_utilities.h | 1 +
src/lib/util/tests/Makefile.am | 6 +-
src/lib/util/tests/buffer_unittest.cc | 5 +
.../util/tests/interprocess_sync_file_unittest.cc | 155 -
.../util/tests/interprocess_sync_null_unittest.cc | 76 -
src/lib/util/tests/interprocess_util.cc | 48 -
src/lib/util/tests/interprocess_util.h | 31 -
.../util/tests/memory_segment_common_unittest.cc | 36 +-
.../util/tests/memory_segment_mapped_unittest.cc | 86 +-
.../util/tests/random_number_generator_unittest.cc | 16 +-
src/lib/util/tests/run_unittests.cc | 1 -
src/lib/util/threads/tests/Makefile.am | 3 +-
src/lib/util/threads/tests/condvar_unittest.cc | 10 +-
src/lib/util/threads/tests/lock_unittest.cc | 10 +-
src/lib/util/unittests/Makefile.am | 1 +
src/lib/util/unittests/interprocess_util.cc | 48 +
src/lib/util/unittests/interprocess_util.h | 31 +
src/lib/util/unittests/mock_socketsession.h | 7 +-
src/lib/xfr/Makefile.am | 6 +-
tests/Makefile.am | 2 +-
tests/lettuce/Makefile.am | 1 +
tests/lettuce/README | 7 +-
.../configurations/example.org.inmem.config | 6 +
.../configurations/ixfr-out/testset1-config.db | 1 -
.../xfrin/retransfer_slave.conf.orig | 1 -
.../xfrin/retransfer_slave_diffs.conf | 4 +-
.../xfrin/retransfer_slave_notify.conf.orig | 4 +-
.../xfrin/retransfer_slave_notify_v4.conf | 4 +-
tests/lettuce/configurations/xfrout_master.conf | 41 +
tests/lettuce/features/.gitignore | 1 +
tests/lettuce/features/auth_badzone.feature | 10 +-
tests/lettuce/features/ddns_system.feature | 36 +
tests/lettuce/features/example.feature | 32 +-
tests/lettuce/features/nsec3_auth.feature | 28 +-
tests/lettuce/features/queries.feature | 17 +-
tests/lettuce/features/resolver_basic.feature | 36 -
.../features/resolver_basic.feature.disabled | 36 +
tests/lettuce/features/terrain/loadzone.py | 106 +
tests/lettuce/features/terrain/querying.py | 25 +-
tests/lettuce/features/terrain/terrain.py | 6 +-
tests/lettuce/features/terrain/transfer.py | 71 +-
tests/lettuce/features/xfrin_bind10.feature | 19 +-
.../lettuce/features/xfrin_notify_handling.feature | 203 +-
tests/lettuce/features/xfrout_bind10.feature | 39 +
tests/lettuce/run_python-tool.sh | 23 +
tests/lettuce/setup_intree_bind10.sh.in | 7 +-
tests/lettuce/tools/xfr-client.py | 103 +
tests/tools/badpacket/Makefile.am | 3 -
tests/tools/perfdhcp/Makefile.am | 4 +-
tests/tools/perfdhcp/command_options.cc | 206 +-
tests/tools/perfdhcp/command_options.h | 110 +-
tests/tools/perfdhcp/packet_storage.h | 161 ++
tests/tools/perfdhcp/perf_pkt4.cc | 2 +-
tests/tools/perfdhcp/perf_pkt6.cc | 2 +-
tests/tools/perfdhcp/pkt_transform.cc | 12 +-
tests/tools/perfdhcp/pkt_transform.h | 8 +-
tests/tools/perfdhcp/rate_control.cc | 158 +
tests/tools/perfdhcp/rate_control.h | 180 ++
tests/tools/perfdhcp/stats_mgr.h | 67 +-
tests/tools/perfdhcp/test_control.cc | 443 ++-
tests/tools/perfdhcp/test_control.h | 175 +-
tests/tools/perfdhcp/tests/Makefile.am | 4 +
.../tools/perfdhcp/tests/command_options_helper.h | 10 +-
.../perfdhcp/tests/command_options_unittest.cc | 230 +-
.../perfdhcp/tests/packet_storage_unittest.cc | 205 ++
tests/tools/perfdhcp/tests/perf_pkt6_unittest.cc | 10 +-
.../tools/perfdhcp/tests/rate_control_unittest.cc | 207 ++
tests/tools/perfdhcp/tests/stats_mgr_unittest.cc | 27 +-
.../tools/perfdhcp/tests/test_control_unittest.cc | 624 +++-
884 files changed, 114864 insertions(+), 15197 deletions(-)
create mode 100644 doc/Doxyfile-xml
create mode 100644 doc/design/Makefile.am
create mode 100644 doc/design/datasrc/.gitignore
create mode 100644 doc/design/datasrc/Makefile.am
create mode 100644 doc/design/datasrc/auth-local.txt
create mode 100644 doc/design/datasrc/auth-mapped.txt
create mode 100644 doc/design/datasrc/data-source-classes.txt
create mode 100644 doc/design/datasrc/memmgr-mapped-init.txt
create mode 100644 doc/design/datasrc/memmgr-mapped-reload.txt
create mode 100644 doc/design/datasrc/overview.txt
create mode 100644 doc/design/resolver/01-scaling-across-cores
create mode 100644 doc/design/resolver/02-mixed-recursive-authority-setup
create mode 100644 doc/design/resolver/03-cache-algorithm
create mode 100644 doc/design/resolver/README
create mode 100644 doc/devel/contribute.dox
create mode 100644 ext/Makefile.am
create mode 100644 ext/asio/Makefile.am
create mode 100644 ext/asio/asio/Makefile.am
create mode 100644 m4macros/ax_python_sqlite3.m4
create mode 100644 src/bin/d2/.gitignore
create mode 100644 src/bin/d2/Makefile.am
create mode 100644 src/bin/d2/b10-dhcp-ddns.xml
create mode 100644 src/bin/d2/d2_asio.h
create mode 100644 src/bin/d2/d2_cfg_mgr.cc
create mode 100644 src/bin/d2/d2_cfg_mgr.h
create mode 100644 src/bin/d2/d2_config.cc
create mode 100644 src/bin/d2/d2_config.h
create mode 100644 src/bin/d2/d2_controller.cc
create mode 100644 src/bin/d2/d2_controller.h
create mode 100644 src/bin/d2/d2_log.cc
create mode 100644 src/bin/d2/d2_log.h
create mode 100644 src/bin/d2/d2_messages.mes
create mode 100644 src/bin/d2/d2_process.cc
create mode 100644 src/bin/d2/d2_process.h
create mode 100644 src/bin/d2/d2_queue_mgr.cc
create mode 100644 src/bin/d2/d2_queue_mgr.h
create mode 100644 src/bin/d2/d2_update_message.cc
create mode 100644 src/bin/d2/d2_update_message.h
create mode 100644 src/bin/d2/d2_update_mgr.cc
create mode 100644 src/bin/d2/d2_update_mgr.h
create mode 100644 src/bin/d2/d2_zone.cc
create mode 100644 src/bin/d2/d2_zone.h
create mode 100644 src/bin/d2/d_cfg_mgr.cc
create mode 100644 src/bin/d2/d_cfg_mgr.h
create mode 100644 src/bin/d2/d_controller.cc
create mode 100644 src/bin/d2/d_controller.h
create mode 100644 src/bin/d2/d_process.h
create mode 100644 src/bin/d2/dhcp-ddns.spec
create mode 100644 src/bin/d2/dns_client.cc
create mode 100644 src/bin/d2/dns_client.h
create mode 100644 src/bin/d2/labeled_value.cc
create mode 100644 src/bin/d2/labeled_value.h
create mode 100644 src/bin/d2/main.cc
create mode 100644 src/bin/d2/nc_add.cc
create mode 100644 src/bin/d2/nc_add.h
create mode 100644 src/bin/d2/nc_remove.cc
create mode 100644 src/bin/d2/nc_remove.h
create mode 100644 src/bin/d2/nc_trans.cc
create mode 100644 src/bin/d2/nc_trans.h
create mode 100644 src/bin/d2/spec_config.h.pre.in
create mode 100644 src/bin/d2/state_model.cc
create mode 100644 src/bin/d2/state_model.h
create mode 100644 src/bin/d2/tests/.gitignore
create mode 100644 src/bin/d2/tests/Makefile.am
create mode 100644 src/bin/d2/tests/d2_cfg_mgr_unittests.cc
create mode 100644 src/bin/d2/tests/d2_controller_unittests.cc
create mode 100644 src/bin/d2/tests/d2_process_unittests.cc
create mode 100644 src/bin/d2/tests/d2_queue_mgr_unittests.cc
create mode 100644 src/bin/d2/tests/d2_test.py
create mode 100644 src/bin/d2/tests/d2_unittests.cc
create mode 100644 src/bin/d2/tests/d2_update_message_unittests.cc
create mode 100644 src/bin/d2/tests/d2_update_mgr_unittests.cc
create mode 100644 src/bin/d2/tests/d2_zone_unittests.cc
create mode 100644 src/bin/d2/tests/d_cfg_mgr_unittests.cc
create mode 100644 src/bin/d2/tests/d_controller_unittests.cc
create mode 100644 src/bin/d2/tests/d_test_stubs.cc
create mode 100644 src/bin/d2/tests/d_test_stubs.h
create mode 100644 src/bin/d2/tests/dns_client_unittests.cc
create mode 100644 src/bin/d2/tests/labeled_value_unittests.cc
create mode 100644 src/bin/d2/tests/nc_add_unittests.cc
create mode 100644 src/bin/d2/tests/nc_remove_unittests.cc
create mode 100644 src/bin/d2/tests/nc_test_utils.cc
create mode 100644 src/bin/d2/tests/nc_test_utils.h
create mode 100644 src/bin/d2/tests/nc_trans_unittests.cc
create mode 100644 src/bin/d2/tests/state_model_unittests.cc
create mode 100644 src/bin/d2/tests/test_data_files_config.h.in
create mode 100644 src/bin/dhcp4/dhcp4_hooks.dox
create mode 100644 src/bin/dhcp4/tests/callout_library_1.cc
create mode 100644 src/bin/dhcp4/tests/callout_library_2.cc
create mode 100644 src/bin/dhcp4/tests/callout_library_common.h
create mode 100644 src/bin/dhcp4/tests/dhcp4_test_utils.cc
create mode 100644 src/bin/dhcp4/tests/dhcp4_test_utils.h
create mode 100644 src/bin/dhcp4/tests/fqdn_unittest.cc
create mode 100644 src/bin/dhcp4/tests/marker_file.cc
create mode 100644 src/bin/dhcp4/tests/marker_file.h.in
create mode 100644 src/bin/dhcp4/tests/test_data_files_config.h.in
create mode 100644 src/bin/dhcp4/tests/test_libraries.h.in
create mode 100644 src/bin/dhcp4/tests/wireshark.cc
create mode 100644 src/bin/dhcp6/dhcp6_hooks.dox
create mode 100644 src/bin/dhcp6/tests/callout_library_1.cc
create mode 100644 src/bin/dhcp6/tests/callout_library_2.cc
create mode 100644 src/bin/dhcp6/tests/callout_library_common.h
create mode 100644 src/bin/dhcp6/tests/dhcp6_test_utils.cc
create mode 100644 src/bin/dhcp6/tests/dhcp6_test_utils.h
create mode 100644 src/bin/dhcp6/tests/fqdn_unittest.cc
create mode 100644 src/bin/dhcp6/tests/hooks_unittest.cc
create mode 100644 src/bin/dhcp6/tests/marker_file.cc
create mode 100644 src/bin/dhcp6/tests/marker_file.h.in
create mode 100644 src/bin/dhcp6/tests/test_data_files_config.h.in
create mode 100644 src/bin/dhcp6/tests/test_libraries.h.in
create mode 100644 src/bin/dhcp6/tests/wireshark.cc
create mode 100644 src/bin/memmgr/.gitignore
create mode 100644 src/bin/memmgr/Makefile.am
create mode 100644 src/bin/memmgr/b10-memmgr.xml
create mode 100755 src/bin/memmgr/memmgr.py.in
create mode 100644 src/bin/memmgr/memmgr.spec.pre.in
create mode 100644 src/bin/memmgr/memmgr_messages.mes
create mode 100644 src/bin/memmgr/tests/Makefile.am
create mode 100755 src/bin/memmgr/tests/memmgr_test.py
create mode 100644 src/bin/resolver/bench/.gitignore
create mode 100644 src/hooks/Makefile.am
create mode 100644 src/hooks/dhcp/Makefile.am
create mode 100644 src/hooks/dhcp/user_chk/Makefile.am
create mode 100644 src/hooks/dhcp/user_chk/libdhcp_user_chk.dox
create mode 100644 src/hooks/dhcp/user_chk/load_unload.cc
create mode 100644 src/hooks/dhcp/user_chk/pkt_receive_co.cc
create mode 100644 src/hooks/dhcp/user_chk/pkt_send_co.cc
create mode 100644 src/hooks/dhcp/user_chk/subnet_select_co.cc
create mode 100644 src/hooks/dhcp/user_chk/tests/.gitignore
create mode 100644 src/hooks/dhcp/user_chk/tests/Makefile.am
create mode 100644 src/hooks/dhcp/user_chk/tests/run_unittests.cc
create mode 100644 src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in
create mode 100644 src/hooks/dhcp/user_chk/tests/test_users_1.txt
create mode 100644 src/hooks/dhcp/user_chk/tests/test_users_err.txt
create mode 100644 src/hooks/dhcp/user_chk/tests/user_file_unittests.cc
create mode 100644 src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc
create mode 100644 src/hooks/dhcp/user_chk/tests/user_unittests.cc
create mode 100644 src/hooks/dhcp/user_chk/tests/userid_unittests.cc
create mode 100644 src/hooks/dhcp/user_chk/user.cc
create mode 100644 src/hooks/dhcp/user_chk/user.h
create mode 100644 src/hooks/dhcp/user_chk/user_chk.h
create mode 100644 src/hooks/dhcp/user_chk/user_chk_log.cc
create mode 100644 src/hooks/dhcp/user_chk/user_chk_log.h
create mode 100644 src/hooks/dhcp/user_chk/user_chk_messages.mes
create mode 100644 src/hooks/dhcp/user_chk/user_data_source.h
create mode 100644 src/hooks/dhcp/user_chk/user_file.cc
create mode 100644 src/hooks/dhcp/user_chk/user_file.h
create mode 100644 src/hooks/dhcp/user_chk/user_registry.cc
create mode 100644 src/hooks/dhcp/user_chk/user_registry.h
create mode 100644 src/hooks/dhcp/user_chk/version.cc
create mode 100644 src/lib/asiolink/local_socket.cc
create mode 100644 src/lib/asiolink/local_socket.h
create mode 100644 src/lib/asiolink/tests/local_socket_unittest.cc
create mode 100644 src/lib/datasrc/memory/segment_object_holder.cc
create mode 100644 src/lib/datasrc/memory/zone_table_segment_mapped.cc
create mode 100644 src/lib/datasrc/memory/zone_table_segment_mapped.h
create mode 100644 src/lib/datasrc/memory/zone_writer.cc
delete mode 100644 src/lib/datasrc/memory/zone_writer_local.cc
create mode 100644 src/lib/datasrc/tests/memory/memory_segment_mock.h
delete mode 100644 src/lib/datasrc/tests/memory/memory_segment_test.h
create mode 100644 src/lib/datasrc/tests/memory/testdata/template.zone
create mode 100644 src/lib/datasrc/tests/memory/zone_table_segment_mapped_unittest.cc
create mode 100644 src/lib/datasrc/tests/memory/zone_table_segment_mock.h
delete mode 100644 src/lib/datasrc/tests/memory/zone_table_segment_test.h
create mode 100644 src/lib/dhcp/docsis3_option_defs.h
create mode 100644 src/lib/dhcp/option4_client_fqdn.cc
create mode 100644 src/lib/dhcp/option4_client_fqdn.h
create mode 100644 src/lib/dhcp/option6_client_fqdn.cc
create mode 100644 src/lib/dhcp/option6_client_fqdn.h
create mode 100644 src/lib/dhcp/option6_iaprefix.cc
create mode 100644 src/lib/dhcp/option6_iaprefix.h
create mode 100644 src/lib/dhcp/option_string.cc
create mode 100644 src/lib/dhcp/option_string.h
create mode 100644 src/lib/dhcp/option_vendor.cc
create mode 100644 src/lib/dhcp/option_vendor.h
create mode 100644 src/lib/dhcp/pkt_filter.cc
create mode 100644 src/lib/dhcp/pkt_filter6.cc
create mode 100644 src/lib/dhcp/pkt_filter6.h
create mode 100644 src/lib/dhcp/pkt_filter_inet6.cc
create mode 100644 src/lib/dhcp/pkt_filter_inet6.h
create mode 100644 src/lib/dhcp/protocol_util.cc
create mode 100644 src/lib/dhcp/protocol_util.h
create mode 100644 src/lib/dhcp/tests/option4_client_fqdn_unittest.cc
create mode 100644 src/lib/dhcp/tests/option6_client_fqdn_unittest.cc
create mode 100644 src/lib/dhcp/tests/option6_iaprefix_unittest.cc
create mode 100644 src/lib/dhcp/tests/option_string_unittest.cc
create mode 100644 src/lib/dhcp/tests/option_vendor_unittest.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter6_test_utils.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter6_test_utils.h
create mode 100644 src/lib/dhcp/tests/pkt_filter_inet6_unittest.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter_inet_unittest.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter_lpf_unittest.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter_test_utils.cc
create mode 100644 src/lib/dhcp/tests/pkt_filter_test_utils.h
create mode 100644 src/lib/dhcp/tests/pkt_filter_unittest.cc
create mode 100644 src/lib/dhcp/tests/protocol_util_unittest.cc
create mode 100644 src/lib/dhcp_ddns/.gitignore
create mode 100644 src/lib/dhcp_ddns/Makefile.am
create mode 100644 src/lib/dhcp_ddns/dhcp_ddns_log.cc
create mode 100644 src/lib/dhcp_ddns/dhcp_ddns_log.h
create mode 100644 src/lib/dhcp_ddns/dhcp_ddns_messages.mes
create mode 100644 src/lib/dhcp_ddns/libdhcp_ddns.dox
create mode 100644 src/lib/dhcp_ddns/ncr_io.cc
create mode 100644 src/lib/dhcp_ddns/ncr_io.h
create mode 100644 src/lib/dhcp_ddns/ncr_msg.cc
create mode 100644 src/lib/dhcp_ddns/ncr_msg.h
create mode 100644 src/lib/dhcp_ddns/ncr_udp.cc
create mode 100644 src/lib/dhcp_ddns/ncr_udp.h
create mode 100644 src/lib/dhcp_ddns/tests/.gitignore
create mode 100644 src/lib/dhcp_ddns/tests/Makefile.am
create mode 100644 src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc
create mode 100644 src/lib/dhcp_ddns/tests/ncr_unittests.cc
copy src/lib/{dhcp => dhcp_ddns}/tests/run_unittests.cc (100%)
create mode 100644 src/lib/dhcpsrv/callout_handle_store.h
create mode 100644 src/lib/dhcpsrv/d2_client.cc
create mode 100644 src/lib/dhcpsrv/d2_client.h
create mode 100644 src/lib/dhcpsrv/dhcp_parsers.cc
create mode 100644 src/lib/dhcpsrv/dhcp_parsers.h
create mode 100644 src/lib/dhcpsrv/lease.cc
create mode 100644 src/lib/dhcpsrv/lease.h
create mode 100644 src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc
create mode 100644 src/lib/dhcpsrv/tests/callout_library.cc
create mode 100644 src/lib/dhcpsrv/tests/d2_client_unittest.cc
create mode 100644 src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
create mode 100644 src/lib/dhcpsrv/tests/lease_unittest.cc
create mode 100644 src/lib/dhcpsrv/tests/test_get_callout_handle.cc
create mode 100644 src/lib/dhcpsrv/tests/test_get_callout_handle.h
create mode 100644 src/lib/dhcpsrv/tests/test_libraries.h.in
create mode 100644 src/lib/dns/python/pydnspp_config.h.in
create mode 100644 src/lib/hooks/.gitignore
create mode 100644 src/lib/hooks/Makefile.am
create mode 100644 src/lib/hooks/callout_handle.cc
create mode 100644 src/lib/hooks/callout_handle.h
create mode 100644 src/lib/hooks/callout_manager.cc
create mode 100644 src/lib/hooks/callout_manager.h
create mode 100644 src/lib/hooks/hooks.cc
create mode 100644 src/lib/hooks/hooks.h
create mode 100644 src/lib/hooks/hooks_component_developer.dox
create mode 100644 src/lib/hooks/hooks_log.cc
create mode 100644 src/lib/hooks/hooks_log.h
create mode 100644 src/lib/hooks/hooks_maintenance.dox
create mode 100644 src/lib/hooks/hooks_manager.cc
create mode 100644 src/lib/hooks/hooks_manager.h
create mode 100644 src/lib/hooks/hooks_messages.mes
create mode 100644 src/lib/hooks/hooks_user.dox
create mode 100644 src/lib/hooks/images/DataScopeArgument.dia
create mode 100644 src/lib/hooks/images/DataScopeArgument.png
create mode 100644 src/lib/hooks/images/DataScopeContext.dia
create mode 100644 src/lib/hooks/images/DataScopeContext.png
create mode 100644 src/lib/hooks/images/HooksUml.dia
create mode 100644 src/lib/hooks/images/HooksUml.png
create mode 100644 src/lib/hooks/library_handle.cc
create mode 100644 src/lib/hooks/library_handle.h
create mode 100644 src/lib/hooks/library_manager.cc
create mode 100644 src/lib/hooks/library_manager.h
create mode 100644 src/lib/hooks/library_manager_collection.cc
create mode 100644 src/lib/hooks/library_manager_collection.h
create mode 100644 src/lib/hooks/pointer_converter.h
create mode 100644 src/lib/hooks/server_hooks.cc
create mode 100644 src/lib/hooks/server_hooks.h
create mode 100644 src/lib/hooks/tests/.gitignore
create mode 100644 src/lib/hooks/tests/Makefile.am
create mode 100644 src/lib/hooks/tests/basic_callout_library.cc
create mode 100644 src/lib/hooks/tests/callout_handle_unittest.cc
create mode 100644 src/lib/hooks/tests/callout_manager_unittest.cc
create mode 100644 src/lib/hooks/tests/common_test_class.h
create mode 100644 src/lib/hooks/tests/framework_exception_library.cc
create mode 100644 src/lib/hooks/tests/full_callout_library.cc
create mode 100644 src/lib/hooks/tests/handles_unittest.cc
create mode 100644 src/lib/hooks/tests/hooks_manager_unittest.cc
create mode 100644 src/lib/hooks/tests/incorrect_version_library.cc
create mode 100644 src/lib/hooks/tests/library_manager_collection_unittest.cc
create mode 100644 src/lib/hooks/tests/library_manager_unittest.cc
create mode 100644 src/lib/hooks/tests/load_callout_library.cc
create mode 100644 src/lib/hooks/tests/load_error_callout_library.cc
create mode 100644 src/lib/hooks/tests/marker_file.h.in
create mode 100644 src/lib/hooks/tests/no_version_library.cc
create mode 100644 src/lib/hooks/tests/run_unittests.cc
create mode 100644 src/lib/hooks/tests/server_hooks_unittest.cc
create mode 100644 src/lib/hooks/tests/test_libraries.h.in
create mode 100644 src/lib/hooks/tests/unload_callout_library.cc
delete mode 100644 src/lib/log/dummylog.cc
delete mode 100644 src/lib/log/dummylog.h
create mode 100644 src/lib/log/interprocess/Makefile.am
create mode 100644 src/lib/log/interprocess/README
create mode 100644 src/lib/log/interprocess/interprocess_sync.h
create mode 100644 src/lib/log/interprocess/interprocess_sync_file.cc
create mode 100644 src/lib/log/interprocess/interprocess_sync_file.h
create mode 100644 src/lib/log/interprocess/interprocess_sync_null.cc
create mode 100644 src/lib/log/interprocess/interprocess_sync_null.h
copy src/{bin/resolver => lib/log/interprocess}/tests/.gitignore (100%)
create mode 100644 src/lib/log/interprocess/tests/Makefile.am
create mode 100644 src/lib/log/interprocess/tests/interprocess_sync_file_unittest.cc
create mode 100644 src/lib/log/interprocess/tests/interprocess_sync_null_unittest.cc
create mode 100644 src/lib/log/interprocess/tests/run_unittests.cc
delete mode 100644 src/lib/nsas/asiolink.h
create mode 100644 src/lib/python/isc/datasrc/configurableclientlist_inc.cc
create mode 100644 src/lib/python/isc/datasrc/tests/testdata/Makefile.am
create mode 100644 src/lib/python/isc/datasrc/tests/testdata/example.com-broken.zone
create mode 100644 src/lib/python/isc/datasrc/zonetable_accessor_python.cc
create mode 100644 src/lib/python/isc/datasrc/zonetable_accessor_python.h
create mode 100644 src/lib/python/isc/datasrc/zonetable_iterator_python.cc
create mode 100644 src/lib/python/isc/datasrc/zonetable_iterator_python.h
create mode 100644 src/lib/python/isc/datasrc/zonewriter_inc.cc
create mode 100644 src/lib/python/isc/datasrc/zonewriter_python.cc
create mode 100644 src/lib/python/isc/datasrc/zonewriter_python.h
create mode 100644 src/lib/python/isc/log_messages/libmemmgr_messages.py
create mode 100644 src/lib/python/isc/log_messages/memmgr_messages.py
create mode 100644 src/lib/python/isc/log_messages/util_messages.py
create mode 100644 src/lib/python/isc/log_messages/work/README
create mode 100644 src/lib/python/isc/memmgr/Makefile.am
copy src/lib/python/isc/{bind10 => memmgr}/__init__.py (100%)
create mode 100644 src/lib/python/isc/memmgr/builder.py
create mode 100644 src/lib/python/isc/memmgr/datasrc_info.py
create mode 100644 src/lib/python/isc/memmgr/libmemmgr_messages.mes
create mode 100644 src/lib/python/isc/memmgr/logger.py
create mode 100644 src/lib/python/isc/memmgr/tests/Makefile.am
create mode 100644 src/lib/python/isc/memmgr/tests/builder_tests.py
create mode 100644 src/lib/python/isc/memmgr/tests/datasrc_info_tests.py
create mode 100644 src/lib/python/isc/memmgr/tests/testdata/Makefile.am
copy src/lib/python/isc/{datasrc/tests/testdata/example.com => memmgr/tests/testdata/example.com.zone} (100%)
create mode 100644 src/lib/python/isc/notify/tests/testdata/test_spec1.spec
create mode 100644 src/lib/python/isc/server_common/.gitignore
create mode 100644 src/lib/python/isc/server_common/bind10_server.py
create mode 100644 src/lib/python/isc/server_common/datasrc_clients_mgr.py
create mode 100644 src/lib/python/isc/server_common/tests/.gitignore
create mode 100755 src/lib/python/isc/server_common/tests/bind10_server_test.py
create mode 100644 src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py
create mode 100644 src/lib/python/isc/statistics/dns.py
create mode 100644 src/lib/python/isc/statistics/tests/dns_test.py
create mode 100644 src/lib/python/isc/util/tests/traceback_handler_test.py
create mode 100644 src/lib/python/isc/util/traceback_handler.py
create mode 100644 src/lib/python/isc/util/util_messages.mes
delete mode 100644 src/lib/util/interprocess_sync.h
delete mode 100644 src/lib/util/interprocess_sync_file.cc
delete mode 100644 src/lib/util/interprocess_sync_file.h
delete mode 100644 src/lib/util/interprocess_sync_null.cc
delete mode 100644 src/lib/util/interprocess_sync_null.h
create mode 100755 src/lib/util/python/doxygen2pydoc.py.in
delete mode 100644 src/lib/util/tests/interprocess_sync_file_unittest.cc
delete mode 100644 src/lib/util/tests/interprocess_sync_null_unittest.cc
delete mode 100644 src/lib/util/tests/interprocess_util.cc
delete mode 100644 src/lib/util/tests/interprocess_util.h
create mode 100644 src/lib/util/unittests/interprocess_util.cc
create mode 100644 src/lib/util/unittests/interprocess_util.h
create mode 100644 tests/lettuce/Makefile.am
create mode 100644 tests/lettuce/configurations/xfrout_master.conf
create mode 100644 tests/lettuce/features/.gitignore
delete mode 100644 tests/lettuce/features/resolver_basic.feature
create mode 100644 tests/lettuce/features/resolver_basic.feature.disabled
create mode 100644 tests/lettuce/features/terrain/loadzone.py
create mode 100644 tests/lettuce/features/xfrout_bind10.feature
create mode 100755 tests/lettuce/run_python-tool.sh
mode change 100644 => 100755 tests/lettuce/setup_intree_bind10.sh.in
create mode 100755 tests/lettuce/tools/xfr-client.py
create mode 100644 tests/tools/perfdhcp/packet_storage.h
create mode 100644 tests/tools/perfdhcp/rate_control.cc
create mode 100644 tests/tools/perfdhcp/rate_control.h
create mode 100644 tests/tools/perfdhcp/tests/packet_storage_unittest.cc
create mode 100644 tests/tools/perfdhcp/tests/rate_control_unittest.cc
-----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 7bc41b4..65efde8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,9 @@ Makefile
Makefile.in
TAGS
+*.log
+*.trs
+config.h.in~
/aclocal.m4
/autom4te.cache/
/config.guess
@@ -30,6 +33,7 @@ TAGS
/missing
/py-compile
/stamp-h1
+/test-driver
/all.info
/coverage-cpp-html
diff --git a/ChangeLog b/ChangeLog
index b2717a8..83e6f3f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,807 @@
+732. [func] tomek
+ b10-dhcp4, b10-dhcp6: Support for simplified client classification
+ added. Incoming packets are now assigned to a client class based on
+ the content of the packet's user class option (DHCPv4) or vendor class
+ option (DHCPv6). Two classes (docsis3.0 and eRouter1.0) have class
+ specific behavior in b10-dhcp4. See DHCPv4 Client Classification and
+ DHCPv6 Client Classification in BIND10 Developer's Guide for details.
+ This is a first ticket in a series of planned at least three tickets.
+ (Trac #3203, git afea612c23143f81a4201e39ba793bc837c5c9f1)
+
+731. [func] tmark
+ b10-dhcp4 now parses parameters which support DHCP-DDNS updates via
+ the DHCP-DDNS module, b10-dhcp-ddns. These parameters are part of new
+ configuration element, dhcp-ddns, defined in dhcp4.spec. The parameters
+ parse, store and retrieve but do not yet govern behavior. That will be
+ provided under separate ticket.
+ (Trac# 3033, git 0ba859834503f2b9b908cd7bc572e0286ca9201f)
+
+730. [bug] tomek
+ b10-dhcp4, b10-dhcp6: Both servers used to unnecessarily increase
+ subnet-id values after reconfiguration. The subnet-ids are now reset
+ to 1 every time a server is reconfigured.
+ (Trac #3234, git 31e416087685a6dadc3047fdbb0927bbf60095aa)
+
+729. [bug] marcin
+ b10-dhcp4 discards DHCPv4 messages carrying server identifiers
+ which don't match server identifiers used by the server.
+ (Trac #3279, git 805d2b269c6bf3e7be68c13f1da1709d8150a666)
+
+728. [func] marcin
+ b10-dhcp6: If server fails to open a socket on one interface it
+ will log a warning and continue to open sockets on other interfaces.
+ The warning message is communicated from the libdhcp++ via the
+ error handler function supplied by the DHCPv6 server.
+ (Trac #3252, git af5eada1bba906697ee92df3fcc25cc0e3979221)
+
+727. [func] muks
+ RRset::setName() has now been removed.
+ (Trac #2335, git c918027a387da8514acf7e125fd52c8378113662)
+
+726. [bug] muks
+ Don't print trailing newlines in Question::toText() output by
+ default. This fixes some logging that were split with a line
+ feed. It is possible to get the old behavior by passing
+ toText(true). Message::toText() output is unchanged.
+ (Trac #571, git 7286499d5206c6d2aa8a59a5247c3841a772a43e)
+
+725. [func] tmark
+ b10-dhcp-ddns D2UpdateMgr now uses the newly implemented
+ NameAddTransaction and NameRemoveTransaction classes. This allows
+ it to conduct actual DNS update exchanges based upon queued
+ NameChangeRequests.
+ (Trac# 3089, git 9ff948a169e1c1f3ad9e1bad1568375590a3ef42)
+
+724. [bug] marcin
+ b10-dhcp4: Different server identifiers are used for the packets
+ being sent through different interfaces. The server uses IPv4 address
+ assigned to the particular interface as a server identifier. This
+ guarantees that the unicast packet sent by a relay or a client, to
+ the address being a server identifier, will reach the server.
+ (Trac #3231, git c7a229f15089670d2bfde6e9f0530c30ce6f8cf8)
+
+723. [bug] marcin
+ libdhcp++: Implemented unit tests for the IfaceMgr's routine
+ which opens IPv6 sockets on detected interfaces. The IfaceMgr
+ logic performing low level operations on sockets has been
+ moved to a separate class. By providing a custom implementation
+ of this class, the unit tests may use fake interfaces with
+ custom configuration and thus cover wide range of test
+ scenarios for the function.
+ (Trac #3251, git 21d2f7ec425f8461b545687104cd76a42da61b2e)
+
+722. [bug] muks
+ b10-cmdctl now prints a more operator-friendly message when the
+ address+port that b10-cmdctl listens on is already in use.
+ (Trac #3227, git 5ec35e37dbb46f66ff0f6a9d9a6a87a393b37934)
+
+721. [func] tmark
+ Updates the user_chk example hooks shared library with callouts
+ for packet receive and packet send. Decision outcome now includes
+ the lease or prefix assigned. The user registry now supports a
+ default user entry.
+ (Trac #3207, git 34fddf2e75b80d9e517a8f9c3321aa4878cda795)
+
+720. [func] tmark
+ Added the initial implementation of the class, NameAddTransaction,
+ to b10-dhcp-ddns. This class provides a state machine which
+ implements the logic required to remove forward and reverse DNS
+ entries as described in RFC 4703, section 5.5. This includes the
+ ability to construct the necessary DNS requests.
+ (Trac# 3088, git ca58ac00fce4cb5f46e534d7ffadb2db4e4ffaf3)
+
+719. [func] tomek
+ b10-dhcp4: Support for sending back client-id (RFC6842) has been
+ added now. Also a configuration parameter (echo-client-id) has
+ been added, so it is possible to enable backward compatibility
+ ("echo-client-id false").
+ (Trac #3210, git 88a4858db206dfcd53a227562198f308f7779a72)
+
+718. [func] dclink, tomek
+ libdhcp++: Interface detection implemented for FreeBSD, NetBSD,
+ OpenBSD, Mac OS X and Solaris 11. Thanks to David Carlier for
+ contributing a patch.
+ (Trac #2246, git d8045b5e1580a1d0b89a232fd61c10d25a95e769)
+
+717. [bug] marcin
+ Fixed the bug which incorrectly treated DHCPv4 option codes 224-254 as
+ standard options, barring them from being used as custom options.
+ (Trac #2772, git c6158690c389d75686545459618ae0bf16f2cdb8)
+
+716. [func] marcin
+ perfdhcp: added support for sending DHCPv6 Release messages
+ at the specified rate and measure performance. The orphan
+ messages counters are not displayed for individual exchanges
+ anymore. The following ticket: #3261 has been submitted to
+ implement global orphan counting for all exchange types.
+ (Trac #3181, git 684524bc130080e4fa31b65edfd14d58eec37e50)
+
+715. [bug] marcin
+ libdhcp++: Used the CMSG_SPACE instead of CMSG_LEN macro to calculate
+ msg_controllen field of the DHCPv6 message. Use of CMSG_LEN causes
+ sendmsg failures on OpenBSD due to the bug kernel/6080 on OpenBSD.
+ (Trac #1824, git 39c9499d001a98c8d2f5792563c28a5eb2cc5fcb)
+
+714. [doc] tomek
+ BIND10 Contributor's Guide added.
+ (Trac #3109, git 016bfae00460b4f88adbfd07ed26759eb294ef10)
+
+713. [func] tmark
+ Added DNS update request construction to d2::NameAddTransaction
+ in b10-dhcp-ddns. The class now generates all DNS update
+ request variations needed to fulfill it's state machine in
+ compliance with RFC 4703, sections 5.3 and 5.4.
+ (Trac# 3241, git dceca9554cb9410dd8d12371b68198b797cb6cfb)
+
+712. [func] marcin, dclink
+ b10-dhcp4: If server fails to open a socket on one interface it
+ will log a warning and continue to open sockets on other interfaces.
+ The warning message is communicated from the libdhcp++ via the
+ error handler function supplied by the DHCPv4 server. Thanks to
+ David Carlier for providing a patch.
+ (Trac #2765, git f49c4b8942cdbafb85414a1925ff6ca1d381f498)
+
+711. [func] tmark
+ Added the initial implementation of the class, NameAddTransaction,
+ to b10-dhcp-ddns. This class provides the state model logic
+ described in the DHCP_DDNS design to add or replace forward and
+ reverse DNS entries for a given FQDN. It does not yet construct
+ the actual DNS update requests, this will be added under Trac#
+ 3241.
+ (Trac# 3087, git 8f99da735a9f39d514c40d0a295f751dc8edfbcd)
+
+710. [build] jinmei
+ Fixed various build time issues for MacOS X 10.9. Those include
+ some general fixes and improvements:
+ - (libdns++) masterLoad() functions now use the generic MasterLoader
+ class as backend, eliminating the restrictions of the previous
+ versions.
+ - (libcc) fixed a minor portability bug in the JSON parser. Although
+ the only known affected system is OS X 10.9 at the moment, that
+ could potentially cause disruption on other existing and future
+ systems.
+ Other notes:
+ - if built with googletest, gtest 1.7 (and possibly higher) is
+ required.
+ - many older versions of Boost don't work. A known workable version
+ is 1.54.
+ (Trac #3213, git d4e570f097fe0eb9009b177a4af285cde0c636cc)
+
+709. [bug] marcin
+ b10-dhcp6: Server crashed when the client sent FQDN option and did
+ not request FQDN option to be returned.
+ (Trac #3220, git 0f1ed4205a46eb42ef728ba6b0955c9af384e0be)
+
+708. [bug] dclink, marcin
+ libdhcpsrv: Fixed a bug in Memfile lease database backend which
+ caused DHCPv4 server crashes when leases with NULL client id
+ were present. Thanks to David Carlier for submitting the patch.
+ (Trac #2940, git a232f3d7d92ebcfb7793dc6b67914299c45c715b)
+
+707. [bug] muks
+ Using very large numbers (out of bounds) in config values caused
+ BIND 10 to throw an exception. This has been fixed in a patch
+ contributed by David Carlier.
+ (Trac #3114, git 9bd776e36b7f53a6ee2e4d5a2ea79722ba5fe13b)
+
+706. [func] marcin
+ b10-dhcp4: Server processes the DHCPv4 Client FQDN and Host Name
+ options sent by a client and generates the response. As a result
+ of processing, the server generates NameChangeRequests which
+ represent changes to DNS mappings for a particular lease (addition
+ or removal of DNS mappings).
+ Currently all generated NameChangeRequests are dropped. Sending
+ them to b10-dhcp-ddns will be implemented with the future tickets.
+ (Trac #3035, git f617e6af8cdf068320d14626ecbe14a73a6da22)
+
+705. [bug] kean
+ When commands are piped into bindctl, no longer attempt to query the
+ user name and password if no default user name and password file is
+ present, or it contains no valid entries.
+ (Trac #264, git 4921d7de6b5623c7e85d2baf8bc978686877345b)
+
+704. [func] naokikambe
+ New statistics items related to IP sockets added into b10-xfrin:
+ open, openfail, close, connfail, conn, senderr, and recverr.
+ Their values can be obtained by invoking "Stats show Xfrin" via
+ bindctl while b10-xfrin is running.
+ (Trac #2300, git 4655c110afa0ec6f5669bf53245bffe6b30ece4b)
+
+703. [bug] kean
+ A bug in b10-msgq was fixed where it would remove the socket file if
+ there was an existing copy of b10-msgq running. It now correctly
+ detects and reports this without removing the socket file.
+ (Trac #433, git c18a49b0435c656669e6f87ef65d44dc98e0e726)
+
+702. [func] marcin
+ perfdhcp: support for sending DHCPv6 Renew messages at the specified
+ rate and measure performance.
+ (Trac #3183, git 66f2939830926f4337623b159210103b5a8e2434)
+
+701. [bug] tomek
+ libdhcp++: Incoming DHCPv6 IAPREFIX option is now parsed properly.
+ (Trac #3211, git ed43618a2c7b2387d76f99a5a4b1a3e05ac70f5e)
+
+700. [func] tomek, marcin
+ b10-dhcp4, b10-dhcp6: Support for vendor options has been added. It
+ is now possible to configure vendor options. Server is able to
+ parse some CableLabs vendor options and send configured vendor
+ options in response. The support is not complete.
+ (Trac #3194, git 243ded15bbed0d35e230d00f4e3ee42c3609616c)
+
+699. [bug] marcin
+ libdhcp++: Options with defined suboptions are now handled properly.
+ In particular, Relay Agent Info options is now echoed back properly.
+ (Trac #3102, git 6f6251bbd761809634aa470f36480d046b4d2a20)
+
+698. [bug] muks
+ A bug was fixed in the interaction between b10-init and b10-msgq
+ that caused BIND 10 failures after repeated start/stop of
+ components.
+ (Trac #3094, git ed672a898d28d6249ff0c96df12384b0aee403c8
+
+697. [func] tmark
+ Implements "user_check" hooks shared library which supports subnet
+ selection based upon the contents of a list of known DHCP lease users
+ (i.e. clients). Adds the following subdirectories to the bind10 src
+ directory for maintaining hooks shared libraries:
+ bind10/src/hooks - base directory for hooks shared libraries;
+ bind10/src/hooks/dhcp - base directory for all hooks libs
+ pertaining to DHCP (Kea);
+ bind10/src/hooks/dhcp/user_check - directory containing the
+ user_check hooks library.
+ (Trac #3186, git f36aab92c85498f8511fbbe19fad5e3f787aef68)
+
+696. [func] tomek
+ b10-dhcp4: It is now possible to specify value of siaddr field
+ in DHCPv4 responses. It is used to point out to the next
+ server in the boot process (that typically is TFTP server).
+ (Trac #3191, git 541922b5300904a5de2eaeddc3666fc4b654ffba)
+
+695. [func] tomek
+ b10-dhcp6 is now able to listen on global IPv6 unicast addresses.
+ (Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+694. [bug] tomek
+ b10-dhcp6 now handles exceptions better when processing initial
+ configuration. In particular, errors with socket binding do not
+ prevent b10-dhcp6 from establishing configuration session anymore.
+ (Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+693. [bug] tomek
+ b10-dhcp6 now handles IPv6 interface enabling correctly.
+ (Trac #3195, git 72e601f2a57ab70b25d50877c8e49242739d1c9f)
+
+692. [bug] marcin
+ b10-dhcp4: Fix a bug whereby the Parameter Request List was not parsed
+ by the server and requested DHCPv4 options were not returned to the
+ client. Options are not sent back to the client if server failed to
+ assign a lease.
+ (Trac #3200, git 50d91e4c069c6de13680bfaaee3c56b68d6e4ab1)
+
+691. [bug] marcin
+ libdhcp++: Created definitions for standard DHCPv4 options:
+ tftp-server-name (66) and boot-file-name (67). Also, fixed definition
+ of DHCPv4 option time-offset (2).
+ (Trac #3199, git 6e171110c4dd9ae3b1be828b9516efc65c33460b)
+
+690. [bug] tomek
+ b10-dhcp4: Relay Agent Info option is now echoed back in
+ DHCPv4 responses.
+ (Trac #3184, git 287389c049518bff66bdf6a5a49bb8768be02d8e)
+
+689. [func]* marcin
+ b10-dhcp4 and b10-dhcp6 install callback functions which parse options
+ in the received DHCP packets.
+ (Trac #3180, git f73fba3cde9421acbeb9486c615900b0af58fa25)
+
+688. [func] tomek
+ b10-dhcp6: Prefix Delegation support is now extended to
+ Renew and Release messages.
+ (Trac #3153, #3154, git 3207932815f58045acea84ae092e0a5aa7c4bfd7)
+
+687. [func] tomek
+ b10-dhcp6: Prefix Delegation (IA_PD and IAPREFIX options) is now
+ supported in Solicit and Request messages.
+ (Trac #3152, git a0e73dd74658f2deb22fad2c7a1f56d122aa9021)
+
+686. [bug] tomek
+ b10-dhcp6 now sends back relayed traffic to proper port.
+ (Trac #3177, git 6b33de4bea92eecb64b6c673bf1b8ae51f8edcf1)
+
+685. [func] tomek
+ libdhcpsrv: Allocation Engine is now able to handle IPv6 prefixes.
+ This will be used in Prefix Delegation.
+ (Trac #3171, git 7d1431b4c887f0c7ee1b26b9b82d3d3b8464b34f)
+
+684. [func] muks, vorner
+ API support to delete zone data has been added. With this,
+ DomainTree and RdataSet which form the central zone data
+ structures of b10-auth allow deletion of names and RR data
+ respectively.
+ (Trac #2750, git d3dbe8e1643358d4f88cdbb7a16a32fd384b85b1)
+ (Trac #2751, git 7430591b4ae4c7052cab86ed17d0221db3b524a8)
+
+683. [bug] stephen
+ Modifications to fix problems running unit tests if they
+ are statically linked. This includes provision of an
+ initialization function that must be called by user-written
+ hooks libraries if they are loaded by a statically-linked
+ image.
+ (Trac #3113, git 3d19eee4dbfabc7cf7ae528351ee9e3a334cae92)
+
+682. [func] naokikambe
+ New statistics items added into b10-xfrin : ixfr_running,
+ axfr_running, and soa_in_progress. Their values can be
+ obtained by invoking "Stats show Xfrin" via bindctl when
+ b10-xfrin is running.
+ (Trac #2274, git ca691626a2be16f08754177bb27983a9f4984702)
+
+681. [func] tmark
+ Added support for prefix delegation configuration to b10-dhcp6
+ subnets.
+ (Trac# 3151, git 79a22be33825bafa1a0cdfa24d5cb751ab1ae2d3)
+
+680. [func] marcin
+ perfdhcp: Added support for requesting IPv6 prefixes using IA_PD
+ option being sent to the server.
+ (Trac #3173, git 4cc844f7cc82c8bd749296a2709ef67af8d9ba87)
+
+679. [func] tmark
+ b10-dhcp-ddns: Finite state machine logic was refactored
+ into its own class, StateModel.
+ (Trac# 3156, git 6e9227b1b15448e834d1f60dd655e5633ff9745c)
+
+678. [func] tmark
+ MySQL backend used by b10-dhcp6 now uses lease type as a
+ filtering parameter in all IPv6 lease queries.
+ (Trac# 3147, git 65b6372b783cb1361fd56efe2b3247bfdbdc47ea)
+
+677. [func] tomek
+ libdhcpsrv: CfgMgr is now able to store IA, TA and PD pools in
+ Subnet6 structures.
+ (Trac #3150, git e6f0e89162bac0adae3ce3141437a282d5183162)
+
+676. [bug] muks
+ We now also allow the short name ("hmac-md5"), along with the long
+ name ("hmac-md5.sig-alg.reg.int") that was allowed before for
+ HMAC-MD5, so that it is more convenient to configure TSIG keys
+ using it.
+ (Trac #2762, git c543008573eba65567e9c189824322954c6dd43b)
+
+675. [func] vorner
+ If there's an exception not handled in a Python BIND10 component,
+ it is now stored in a temporary file and properly logged, instead
+ of dumping to stderr.
+ (Trac #3095, git 18cf54ed89dee1dd1847053c5210f0ca220590c2)
+
+674. [func] tomek
+ Preparatory work for prefix delegation in LeaseMgr. getLease6()
+ renamed to getLeases6(). It now can return more than one lease.
+ (Trac #3146, git 05a05d810be754e7a4d8ca181550867febf6dcc6)
+
+673. [func] tomek
+ libdhcp: Added support for IA_PD and IAPREFIX options. New class
+ for IAPREFIX (Option6_IAPrefix) has been added.
+ (Trac #3145, git 3a844e85ecc3067ccd1c01841f4a61366cb278f4)
+
+672. [func] tmark
+ Added b10-dhcp-ddnsupdate transaction base class,
+ NameChangeTransaction. This class provides the common
+ structure and methods to implement the state models described
+ in the DHCP_DDNS design, plus integration with DNSClient
+ and its callback mechanism for asynchronous IO with the
+ DNS servers.
+ (Trac #3086, git 079b862c9eb21056fdf957e560b8fe7b218441b6)
+
+671. [func] dclink, tomek
+ The memfile backend now supports getLease4(hwaddr) and
+ getLease4(client-id) methods. Thanks to David Carlier for
+ contributing a patch.
+ (Trac #2592, git a11683be53db2f9f8f9b71c1d1c163511e0319b3)
+
+670. [func] marcin
+ libdhcpsrv: Added support to MySQL lease database backend to
+ store FQDN data for the lease.
+ (Trac #3084, git 79b7d8ee017b57a81cec5099bc028e1494d7e2e9)
+
+669. [func] tmark
+ Added main process event loop to D2Process which is the primary
+ application object in b10-dhcp-ddns. This allows DHCP-DDNS
+ to queue requests received from clients for processing while
+ listening for command control events.
+ (Trac #3075 git e2f9d2e4c1b36f01eb5bfa2c4f8d55cf139c7e02)
+
+668. [func] marcin
+ libdhcpsrv: Implemented changes to lease allocation engine to
+ propagate information about client's FQDN.
+ (Trac #3083, git 37af28303d1cd61f675faea969cd1159df65bf9d)
+
+667. [func] tomek
+ Additional hooks (buffer4_receive, lease4_renew,
+ lease4_release, buffer4_send) added to the DHCPv4 server.
+ (Trac #2983, git fd47f18f898695b98623a63a0a1c68d2e4b37568)
+
+666. [func] vorner
+ The CmdCtl's command "print_settings" was removed. It served no real
+ purpose and was just experimental leftover from early development.
+ (Trac #3028, git 0d22246092ad4822d48f5a52af5f644f5ae2f5e2)
+
+665. [doc] stephen
+ Added the "Hook's Maintenance Guide" to the BIND 10 developer
+ documentation.
+ (Trac #3063, git 5d1ee7b7470fc644b798ac47db1811c829f5ac24)
+
+664. [bug] tmark
+ Corrects a bug in Hooks processing that was improperly
+ creating a new callout handle on every call, rather
+ than maintaining it throughout the context of the
+ packet being processed.
+ (Trac #3062, git 28684bcfe5e54ad0421d75d4445a04b75358ce77)
+
+663. [func] marcin
+ b10-dhcp6: Server processes the DHCPv6 Client FQDN Option
+ sent by a client and generates the response. The DHCPv6 Client
+ FQDN Option is represented by the new class in the libdhcp++.
+ As a result of FQDN Option processing, the server generates
+ NameChangeRequests which represent changes to DNS mappings for
+ a particular lease (addition or removal of DNS mappings).
+ Currently all generated NameChangeRequests are dropped. Sending
+ them to b10-dhcp-ddns will be implemented with the future tickets.
+ (Trac #3036, git 209f3964b9f12afbf36f3fa6b62964e03049ec6e)
+
+662. [func] marcin
+ libdhcp++: Implemented an Option4ClientFqdn class which represents
+ DHCPv4 Client FQDN Option (code 81) defined in RFC4702. This class
+ supports the domain name encoding in canonical FQDN format as well
+ as in deprecated ASCII format.
+ (Trac# 3082, git 1b434debfbf4a43070eb480fa0975a6eff6429d4)
+
+661. [func] stephen
+ Copy additional header files to the BIND 10 installation directory
+ to allow the building of DHCP hooks libraries against an installed
+ version of BIND 10.
+ (Trac #3092, git e9beef0b435ba108af9e5979476bd2928808b342)
+
+660. [func] fujiwara
+ src/lib/cc: Integer size of C++ CC library is changed to int64_t.
+ b10-auth: The size of statistics counters is changed to uint64_t.
+ b10-auth sends lower 63 bit of counter values to b10-stats.
+ (Trac #3015, git e5b3471d579937f19e446f8a380464e0fc059567
+ and Trac #3016, git ffbcf9833ebd2f1952664cc0498608b988628d53)
+
+659. [func] stephen
+ Added capability to configure the hooks libraries for the
+ b10-dhcp4 and b10-dhcp6 servers through the BIND 10
+ configuration mechanism.
+ (Trac #2981, git aff6b06b2490fe4fa6568e7575a9a9105cfd7fae)
+
+658. [func]* vorner
+ The resolver, being experimental, is no longer installed by default.
+ If you really want to use it, even when it is known to be buggy, use
+ the ./configure --enable-experimental-resolver option.
+ (Trac #3064, git f5f07c976d2d42bdf80fea4433202ecf1f260648)
+
+657. [bug] vorner
+ Due to various problems with older versions of boost and
+ shared memory, the server rejects to compile with combination
+ of boost < 1.48 and shared memory enabled. Most users don't
+ need shared memory, admins of large servers are asked to
+ upgrade boost.
+ (Trac #3025, git 598e458c7af7d5bb81131112396e4c5845060ecd)
+
+656. [func] tomek
+ Additional hooks (buffer6_receive, lease6_renew,
+ lease6_release, buffer6_send) added to the DHCPv6 server.
+ (Trac #2984, git 540dd0449121094a56f294c500c2ed811f6016b6)
+
+655. [func] tmark
+ Added D2UpdateMgr class to b10-dhcp-ddns. This class is
+ the b10-dhcp-ddns task master, instantiating and supervising
+ transactions that carry out the DNS updates needed to
+ fulfill the requests (NameChangeRequests) received from
+ b10-dhcp-ddns clients (e.g. DHCP servers).
+ (Trac #3059 git d72675617d6b60e3eb6160305738771f015849ba)
+
+654. [bug] stephen
+ Always clear "skip" flag before calling any callouts on a hook.
+ (Trac# 3050, git ff0b9b45869b1d9a4b99e785fbce421e184c2e93)
+
+653. [func] tmark
+ Added initial implementation of D2QueueMgr to
+ b10-dhcp-ddns. This class manages the receipt and
+ queueing of requests received by b10-dhcp-ddns from
+ its clients (e.g. DHCP servers)
+ (Trac# 3052, git a970f6c5255e000c053a2dc47926cea7cec2761c)
+
+652. [doc] stephen
+ Added the "Hook Developer's Guide" to the BIND 10 developer
+ documentation.
+ (Trac# 2982, git 26a805c7e49a9ec85ee825f179cda41a2358f4c6)
+
+651. [bug] muks
+ A race condition when creating cmdctl certificates caused corruption
+ of these certificates in rare cases. This has now been fixed.
+ (Trac# 2962, git 09f557d871faef090ed444ebeee7f13e142184a0)
+
+650. [func] muks
+ The DomainTree rebalancing code has been updated to be more
+ understandable. This ChangeLog entry is made just to make a note
+ of this change. The change should not cause any observable
+ difference whatsoever.
+ (Trac# 2811, git 7c0bad1643af13dedf9356e9fb3a51264b7481de)
+
+649. [func] muks
+ The default b10-xfrout also_notify port has been changed from
+ 0 to 53.
+ (Trac# 2925, git 8acbf043daf590a9f2ad003e715cd4ffb0b3f979)
+
+648. [func] tmark
+ Moved classes pertaining to sending and receiving
+ NameChangeRequests from src/bin/d2 into their own library,
+ libdhcp_ddns, in src/lib/dhcp_ddns. This allows the
+ classes to be shared between DHDCP-DDNS and its clients,
+ such as the DHCP servers.
+ (Trac# 3065, git 3d39bccaf3f0565152ef73ec3e2cd03e77572c56)
+
+647. [func] tmark
+ Added initial implementation of classes for sending
+ and receiving NameChangeRequests between DHCP-DDNS
+ and its clients such as DHCP. This includes both
+ abstract classes and a derivation which traffics
+ requests across UDP sockets.
+ (Trac #3008, git b54530b4539cec4476986442e72c047dddba7b48)
+
+646. [func] stephen
+ Extended the hooks framework to add a "validate libraries" function.
+ This will be used to check libraries specified during BIND 10
+ configuration.
+ (Trac #3054, git 0f845ed94f462dee85b67f056656b2a197878b04)
+
+645. [func] tomek
+ Added initial set of hooks (pkt4_receive, subnet4_select,
+ lease4_select, pkt4_send) to the DHCPv6 server.
+ (Trac #2994, git be65cfba939a6a7abd3c93931ce35c33d3e8247b)
+
+644. [func] marcin
+ b10-dhcp4, b10-dhcp6: Implemented selection of the interfaces
+ that server listens on, using Configuration Manager. It is
+ possible to specify interface names explicitly or use asterisk
+ to specify that server should listen on all available interfaces.
+ Sockets are reopened according to the new configuration as
+ soon as it is committed.
+ (Trac #1555, git f48a3bff3fbbd15584d788a264d5966154394f04)
+
+643. [bug] muks
+ When running some unittests as root that depended on insufficient
+ file permissions, the tests used to fail because the root user
+ could still access such files. Such tests are now skipped when
+ they are run as the root user.
+ (Trac #3056, git 92ebabdbcf6168666b03d7f7fbb31f899be39322)
+
+642. [func] tomek
+ Added initial set of hooks (pkt6_receive, subnet6_select,
+ lease6_select, pkt6_send) to the DHCPv6 server.
+ (Trac #2995, git d6de376f97313ba40fef989e4a437d184fdf70cc)
+
+641. [func] stephen
+ Added the hooks framework. This allows shared libraries of
+ user-written functions to be loaded at run-time and the
+ functions called during packet processing.
+ (Trac #2980, git 82c997a72890a12af135ace5b9ee100e41c5534e)
+
+640. [func] marcin
+ b10-dhcp-ddns: Implemented DNSClient class which implements
+ asynchronous DNS updates using UDP. The TCP and TSIG support
+ will be implemented at later time. Nevertheless, class API
+ accommodates the use of TCP and TSIG.
+ (Trac #2977, git 5a67a8982baa1fd6b796c063eeb13850c633702c)
+
+639. [bug] muks
+ Added workaround for build failure on Fedora 19 between GCC 4.8.x
+ and boost versions less than 1.54. Fedora 19 currently ships
+ boost-1.53.
+ (Trac #3039, git 4ef6830ed357ceb859ebb3e5e821a064bd8797bb)
+
+638. [bug]* naokikambe
+ Per-zone statistics counters are distinguished by zone class,
+ e.g. IN, CH, and HS. A class name is added onto a zone name in
+ structure of per-zone statistics.
+ (Trac #2884, git c0153581c3533ef045a92e68e0464aab00947cbb)
+
+637. [func] tmark
+ Added initial implementation of NameChangeRequest,
+ which embodies DNS update requests sent to DHCP-DDNS
+ by its clients.
+ (trac3007 git f33bdd59c6a8c8ea883f11578b463277d01c2b70)
+
+636. [func] tmark
+ Added the initial implementation of configuration parsing for
+ DHCP-DDNS.
+ (Trac #2957, git c04fb71fa44c2a458aac57ae54eeb1711c017a49)
+
+635. [func] marcin
+ b10-dhcp-ddns: Implemented DNS Update message construction.
+ (Trac #2796, git eac5e751473e238dee1ebf16491634a1fbea25e2)
+
+634. [bug] muks
+ When processing DDNS updates, we now check the zone more
+ thoroughly with the received zone data updates to check if it is
+ valid. If the zone fails validation, we reply with SERVFAIL
+ rcode. So, while previously we may have allowed more zone data
+ cases without checking which resulted in invalid zones, such
+ update requests are now rejected.
+ (Trac #2759, git d8991bf8ed720a316f7506c1dd9db7de5c57ad4d)
+
+633. [func] jinmei
+ b10-memmgr: a new BIND 10 module that manages shared memory
+ segments for DNS zone data. At this point it's runnable but does
+ nothing really meaningful for end users; it was added to the
+ master branch for further development.
+ (Trac #2854, git d05d7aa36d0f8f87b94dba114134b50ca37eabff)
+
+632. [bug] marcin
+ perfdhcp: Fixed a bug in whereby the application was sporadically
+ crashing when timed out packets were garbage collected.
+ (Trac #2979, git 6d42b333f446eccc9d0204bcc04df38fed0c31db)
+
+631. [bug] muks
+ Applied a patch by Tomas Hozza to fix a couple of compile errors
+ on Fedora 19 development release.
+ (Trac #3001, git 6e42b90971b377261c72d51c38bf4a8dc336664a)
+
+630. [bug] muks
+ If there is a problem loading the backend module for a type of
+ data source, b10-auth would not serve any zones. This behaviour
+ has been changed now so that it serves zones from all other usable
+ data sources that were configured.
+ (Trac #2947, git 9a3ddf1e2bfa2546bfcc7df6d9b11bfbdb5cf35f)
+
+629. [func] stephen
+ Added first part of the hooks framework.
+ (Trac #2794, git d2b107586db7c2deaecba212c891d231d7e54a07)
+
+628. [func] y-aharen
+ b10-auth: A new statistics item 'qryrecursion' has been introduced.
+ The counter is for the number of queries (OpCode=Query) with Recursion
+ Desired (RD) bit on.
+ (Trac #2796, git 3d291f42cdb186682983aa833a1a67cb9e6a8434)
+
+627. [func] tmark
+ Logger name for DHCP-DDNS has been changed from "d2_logger" to
+ "dhcpddns". In addition, its log messages now use two suffixes,
+ DCTL_ for logs the emanate from the underlying base classes, and
+ DHCP_DDNS_ for logs which emanate from DHCP-DDNS specific code
+ (Trac #2978, git 5aec5fb20b0486574226f89bd877267cb9116921)
+
+626. [func] tmark
+ Created the initial implementation of DHCP-DDNS service
+ controller class, D2Controller, and the abstract class from
+ which it derives, DControllerBase. D2Controller manages the
+ lifecycle and BIND10 integration of the DHCP-DDNS application
+ process, D2Process. Also note, module name is now
+ b10-dhcp-ddns.
+ (Trac #2956, git a41cac582e46213c120b19928e4162535ba5fe76)
+
+625. [bug]* jinmei
+ b10-xfrin/b10-loadzone: b10-xfrin now refers to the unified
+ "data_sources" module configuration instead of almost-deprecated
+ the Auth/database_file configuration (Note: zonemgr still uses the
+ latter, so a secondary server would still need it for the moment).
+ Due to this change, b10-xfrin does not auto-generate an initial
+ zone for the very first transfer anymore; b10-loadzone has been
+ extended with a new -e option for the initial setup.
+ (Trac #2946, git 8191aec04c5279c199909f00f0a0b2b8f7bede94)
+
+624. [bug] jinmei
+ logging: prevented multiple BIND 10 processes from generating
+ multiple small log files when they dumped logs to files and try
+ to roll over them simultaneously. This fix relies on a feature of
+ underling logging library (log4cplus) version 1.1.0 or higher,
+ so the problem can still happen if BIND 10 is built with an older
+ version of log4cplus. (But this is expected to happen rarely in
+ any case unless a verbose debug level is specified).
+ (Trac #1622, git 5da8f8131b1224c99603852e1574b2a1adace236)
+
+623. [func] tmark
+ Created the initial, bare-bones implementation of DHCP-DDNS
+ service process class, D2Process, and the abstract class
+ from which it derives, DProcessBase. D2Process will provide
+ the DHCP-DDNS specific event loop and business logic.
+ (Trac #2955, git dbe4772246039a1257b6492936fda2a8600cd245)
+
+622. [func]* jinmei
+ b10-xfrin now has tighter control on the choice of IXFR or AXFR
+ through zones/request_ixfr configuration item. It includes
+ the new "IXFR only" behavior for some special cases. b10-xfrin
+ now also uses AXFR whenever necessary, so it is now safe to try
+ IXFR by default and it's made the default. The previous
+ use_ixfr configuration item was deprecated and triggers startup
+ failure if specified; configuration using use_ixfr should be
+ updated.
+ (Trac #2911, git 8118f8e4e9c0ad3e7b690bbce265a163e4f8767a)
+
+621. [func] team
+ libdns++: All Rdata classes now use the generic lexer in
+ constructors from text. This means that the name fields in such
+ RRs in a zone file can now be non-absolute (the origin name in that
+ context will be used), e.g., when loaded by b10-loadzone. Note
+ that the existing string constructors for these Rdata classes also
+ use the generic lexer, and they now expect an absolute name (with
+ the trailing '.') in the name fields.
+ (Trac #2522, git ea97070cf6b41299351fc29af66fa39c6465d56a)
+ (Trac #2521, git c6603decaadcd33ccf9aee4a7b22447acec4b7f6)
+ (See also ChangeLog 594, 564, 545)
+
+620. [bug] jinmei
+ b10-auth now returns SERVFAIL to queries for a zone that is
+ configured to be loaded in-memory but isn't due to load time
+ errors (missing zone file or errors in the zone file, etc).
+ Such zones were previously treated as non existent and would
+ result in REFUSED or unintentional match against less specific
+ zones. The revised behavior is also compatible with BIND 9.
+ (Trac #2905, git 56ee9810fdfb5f86bd6948e6bf26545ac714edd8)
+
+619. [bug] jinmei
+ b10-xfrout now uses blocking send for xfr response messages
+ to prevent abrupt termination of the stream due to a slower
+ client or narrower network bandwidth.
+ (Trac #2934, git bde0e94518469557c8b455ccbecc079a38382afd)
+
+618. [func]* marcin
+ b10-dhcp4: Added the ability for the server to respond to a
+ directly connected client which does not yet have an IP address.
+ On Linux, the server will unicast the response to the client's
+ hardware address and the 'yiaddr' (the client's new IP
+ address). Sending a response to the unicast address prevents other
+ (not interested) hosts from receiving the server response. This
+ capability is not yet implemented on non-Linux Operating Systems
+ where, in all cases, the server responds to the broadcast
+ address. The logic conforms to section 4.1 of RFC 2131.
+ (Trac #2902, git c2d40e3d425f1e51647be6a717c4a97d7ca3c29c)
+
+617. [bug] marcin
+ b10-dhcp4: Fixed a bug whereby the domain-name option was encoded
+ as FQDN (using technique described in RFC1035) instead of a string.
+ Also, created new class which represents an option carrying a single
+ string value. This class is now used for all standard options of
+ this kind.
+ (Trac #2786, git 96b1a7eb31b16bf9b270ad3d82873c0bd86a3530)
+
+616. [doc] stephen
+ Added description to the DHCP "Database Back-Ends" section of the
+ BIND 10 Developer's Guide about how to set up a MySQL database for
+ testing the DHCP MySQL backend.
+ (Trac #2653, git da3579feea036aa2b7d094b1c260a80a69d2f9aa)
+
+615. [bug] jinmei
+ b10-auth: Avoid referencing to a freed object when authoritative
+ server addresses are reconfigured. It caused a crash on a busy
+ server during initial startup time, and the same crash could also
+ happen if listen_on parameters are reconfigured at run time.
+ (Trac #2946, git d5f2a0d0954acd8bc33aabb220fab31652394fcd)
+
+614. [func] tmark
+ b10-d2: Initial DHCP-DDNS (a.k.a. D2) module implemented.
+ Currently it does nothing useful, except for providing the
+ skeleton implementation to be expanded in the future.
+ (Trac #2954, git 392c5ec5d15cd8c809bc9c6096b9f2bfe7b8c66a)
+
+613. [func] jinmei
+ datasrc: Error handling in loading zones into memory is now more
+ consistent and convenient: data source configuration does not fail
+ due to zones configured to be loaded into memory but not available
+ in the data source, just like the case of missing zone file for
+ the MasterFiles type of data source. Also, zones that aren't
+ loaded into memory due to errors can now be reloaded for b10-auth
+ using the bindctl Auth loadzone command after fixing the error,
+ without reconfiguring the entire data source.
+ (Trac #2851, git a3d4fe8a32003534150ed076ea0bbf80e1fcc43c)
+
+612. [func] tomek
+ b10-dhcp6: Support for relayed DHCPv6 traffic has been added.
+ (Trac #2898, git c3f6b67fa16a07f7f7ede24dd85feaa7c157e1cb)
+
611. [func] naokikambe
Added Xfrin statistics items such as the number of successful
transfers. These are per-zone type counters. Their values can be
@@ -5,7 +809,7 @@
while Xfrin is running.
(Trac #2252, git e1a0ea8ef5c51b9b25afa111fbfe9347afbe5413)
-bind10-1.0.0beta2 released on May 3, 2013
+bind10-1.1.0beta2 released on May 10, 2013
610. [bug] muks
When the sqlite3 program is not available on the system (in
@@ -51,8 +855,8 @@ bind10-1.0.0beta2 released on May 3, 2013
(Trac #2879, git 4c45f29f28ae766a9f7dc3142859f1d0000284e1)
605. [bug] tmark
- Modified perfdhcp to calculate the times displayed for packet sent
- and received as time elapsed since perfdhcp process start time.
+ Modified perfdhcp to calculate the times displayed for packet sent
+ and received as time elapsed since perfdhcp process start time.
Previously these were times since the start of the epoch.
However the large numbers involved caused loss of precision
in the calculation of the test statistics.
@@ -68,7 +872,7 @@ bind10-1.0.0beta2 released on May 3, 2013
(Trac #991, git 33ffc9a750cd3fb34158ef676aab6b05df0302e2)
603. [func] tmark
- The directory in which the b10-dchp4 and b10-dhcp6 server id files has
+ The directory in which the b10-dhcp4 and b10-dhcp6 server id files has
been changed from the local state directory (set by the "configure"
--localstatedir switch) to the "bind10" subdirectory of it. After an
upgrade, server id files in the former location will be orphaned and
@@ -108,7 +912,7 @@ bind10-1.0.0beta2 released on May 3, 2013
messages.
(Trac #2827, git 29c3f7f4e82d7e85f0f5fb692345fd55092796b4)
-bind10-1.0.0beta1 released on April 4, 2013
+bind10-1.1.0beta1 released on April 4, 2013
598. [func]* jinmei
The separate "static" data source is now deprecated as it can be
@@ -194,15 +998,15 @@ bind10-1.0.0beta1 released on April 4, 2013
587. [bug] jelte
When used from python, the dynamic datasource factory now
- explicitely loads the logging messages dictionary, so that correct
+ explicitly loads the logging messages dictionary, so that correct
logging messages does not depend on incidental earlier import
statements. Also, the sqlite3-specific log messages have been moved
from the general datasource library to the sqlite3 datasource
- (which also explicitely loads its messages).
+ (which also explicitly loads its messages).
(Trac #2746, git 1c004d95a8b715500af448683e4a07e9b66ea926)
586. [func] marcin
- libdhcp++: Removed unnecesary calls to the function which
+ libdhcp++: Removed unnecessary calls to the function which
validates option definitions used to create instances of options
being decoded in the received packets. Eliminating these calls
lowered the CPU utilization by the server by approximately 10%.
@@ -308,7 +1112,7 @@ bind10-1.0.0-rc released on February 14, 2013
The lease6 now has the one additional index: iaid/subnet_id/duid.
Adding these indexes significantly improves lease acquisition
performance.
- (Trac #2699,#2703, git 54bbed5fcbe237c5a49b515ae4c55148723406ce)
+ (Trac #2699, #2703, git 54bbed5fcbe237c5a49b515ae4c55148723406ce)
573. [bug] stephen
Fixed problem whereby the DHCP server crashed if it ran out of
@@ -534,7 +1338,7 @@ bind10-1.0.0-rc released on February 14, 2013
542. [func] marcin
Created OptionSpace and OptionSpace6 classes to represent DHCP
option spaces. The option spaces are used to group instances
- and definitions of options having uniqe codes. A special type
+ and definitions of options having unique codes. A special type
of option space is the so-called "vendor specific option space"
which groups sub-options sent within Vendor Encapsulated Options.
The new classes are not used yet but they will be used once
@@ -622,14 +1426,14 @@ bind10-1.0.0-beta released on December 20, 2012
531. [func] tomek
b10-dhcp6: Added support for expired leases. Leases for IPv6
addresses that are past their valid lifetime may be recycled, i.e.
- rellocated to other clients if needed.
+ relocated to other clients if needed.
(Trac #2327, git 62a23854f619349d319d02c3a385d9bc55442d5e)
530. [func]* team
b10-loadzone was fully overhauled. It now uses C++-based zone
parser and loader library, performing stricter checks, having
more complete support for master file formats, producing more
- helpful logs, is more extendable for various types of data
+ helpful logs, is more extendible for various types of data
sources, and yet much faster than the old version. In
functionality the new version should be generally backwards
compatible to the old version, but there are some
@@ -994,7 +1798,7 @@ bind10-devel-20121115 released on November 15, 2012
A new library (libb10-dhcpsrv) has been created. At present, it
only holds the code for the DHCP Configuration Manager. Currently
this object only supports basic configuration storage for the DHCPv6
- server, but that capability will be expanded.
+ server, but that capability will be expanded.
(Trac #2238, git 6f29861b92742da34be9ae76968e82222b5bfd7d)
bind10-devel-20120927 released on September 27, 2012
@@ -1325,7 +2129,7 @@ bind10-devel-20120517 released on May 17, 2012
The in-memory data source can now load zones from the
sqlite3 data source, so that zones stored in the database
(and updated for example by xfrin) can be served from memory.
- (Trac #1789,#1790,#1792,#1793,#1911,
+ (Trac #1789, #1790, #1792, #1793, #1911,
git 93f11d2a96ce4dba9308889bdb9be6be4a765b27)
438. [bug] naokikambe
@@ -1395,7 +2199,7 @@ bind10-devel-20120517 released on May 17, 2012
now manipulates them in the separate table for the NSEC3 namespace.
As a result b10-xfrin now correctly updates NSEC3-signed zones by
inbound zone transfers.
- (Trac #1781,#1788,#1891, git 672f129700dae33b701bb02069cf276238d66be3)
+ (Trac #1781, #1788, #1891, git 672f129700dae33b701bb02069cf276238d66be3)
426. [bug] vorner
The NSEC3 records are now included when transferring a
@@ -1855,11 +2659,11 @@ bind10-devel-20120119 released on January 19, 2012
(Trac #1508, #1509, #1510,
git edc5b3c12eb45437361484c843794416ad86bb00)
-361. [func] vorner,jelte,jinmei
+361. [func] vorner, jelte, jinmei
The socket creator is now used to provide sockets. It means you can
reconfigure the ports and addresses at runtime even when the rest
of the bind10 runs as non root user.
- (Trac #805,#1522, git 1830215f884e3b5efda52bd4dbb120bdca863a6a)
+ (Trac #805, #1522, git 1830215f884e3b5efda52bd4dbb120bdca863a6a)
360. [bug] vorner
Fixed problem where bindctl crashed when a duplicate non-string
@@ -1939,7 +2743,7 @@ bind10-devel-20120119 released on January 19, 2012
interface was awkward. To get all the RRsets of a single domain, use
the new findAll method (the same applies to python version, the method
is named find_all).
- (Trac #1483,#1484, git 0020456f8d118c9f3fd6fc585757c822b79a96f6)
+ (Trac #1483, #1484, git 0020456f8d118c9f3fd6fc585757c822b79a96f6)
349. [bug] dvv
resolver: If an upstream server responds with FORMERR to an EDNS
@@ -2357,7 +3161,7 @@ bind10-devel-20111014 released on October 14, 2011
Stats module can read these through the config manager. Stats
module and stats httpd report statistics data and statistics
schema by each module via both bindctl and HTTP/XML.
- (Trac #928,#929,#930,#1175,
+ (Trac #928, #929, #930, #1175,
git 054699635affd9c9ecbe7a108d880829f3ba229e)
290. [func] jinmei
@@ -2575,7 +3379,7 @@ bind10-devel-20110705 released on July 05, 2011
Logging now correctly initialized in b10-auth. Also, fixed
bug whereby querying for "version.bind txt ch" would cause
b10-auth to crash if BIND 10 was started with the "-v" switch.
- (Trac #1022,#1023, git 926a65fa08617be677a93e9e388df0f229b01067)
+ (Trac #1022, #1023, git 926a65fa08617be677a93e9e388df0f229b01067)
258. [build] jelte
Now builds and runs with Python 3.2
@@ -2742,7 +3546,7 @@ bind10-devel-20110519 released on May 19, 2011
(Trac #900, git b395258c708b49a5da8d0cffcb48d83294354ba3)
231. [func]* vorner
- The logging interface changed slightly. We use
+ The logging interface changed slightly. We use
logger.foo(MESSAGE_ID).arg(bar); instead of logger.foo(MESSAGE_ID,
bar); internally. The message definitions use '%1,%2,...'
instead of '%s,%d', which allows us to cope better with
@@ -3044,7 +3848,7 @@ bind10-devel-20110322 released on March 22, 2011
183. [bug] jerry
src/bin/xfrout: Enable parallel sessions between xfrout server and
- muti-Auth. The session needs to be created only on the first time
+ multi-Auth. The session needs to be created only on the first time
or if an error occur.
(Trac #419, git 1d60afb59e9606f312caef352ecb2fe488c4e751)
@@ -3837,7 +4641,7 @@ bind10-devel-20100701 released on July 1, 2010
(Trac #251, svn r2310)
59. [bug] jinmei
- lib/datasrc,bin/auth: The authoritative server could return a
+ lib/datasrc, bin/auth: The authoritative server could return a
SERVFAIL with a partial answer if it finds a data source broken
while looking for an answer. This can happen, for example, if a
zone that doesn't have an NS RR is configured and loaded as a
@@ -4093,9 +4897,10 @@ bind10-devel-20100421 released on April 21, 2010
bind10-devel-20100319 released on March 19, 2010
-For complete code revision history, see http://bind10.isc.org/browser
+For complete code revision history, see
+ http://git.bind10.isc.org/cgi-bin/cgit.cgi/bind10
Specific git changesets can be accessed at:
- http://bind10.isc.org/changeset/?reponame=&old=rrrr^&new=rrrr
+ http://git.bind10.isc.org/cgi-bin/cgit.cgi/bind10/commit/?id=rrr
or after cloning the original git repository by executing:
% git diff rrrr^ rrrr
Subversion changesets are not accessible any more. The subversion
diff --git a/Makefile.am b/Makefile.am
index 10ef321..6fd0200 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,7 @@ ACLOCAL_AMFLAGS = -I m4macros -I examples/m4 ${ACLOCAL_FLAGS}
# ^^^^^^^^ This has to be the first line and cannot come later in this
# Makefile.am due to some bork in some versions of autotools.
-SUBDIRS = compatcheck doc . src tests m4macros
+SUBDIRS = compatcheck doc . src tests m4macros ext
USE_LCOV=@USE_LCOV@
LCOV=@LCOV@
GENHTML=@GENHTML@
@@ -21,11 +21,11 @@ dist_doc_DATA = AUTHORS COPYING ChangeLog README
.PHONY: check-valgrind check-valgrind-suppress
install-exec-hook:
- - at echo -e "\033[1;33m" # switch to yellow color text
+ - at tput smso # Start standout mode
@echo "NOTE: BIND 10 does not automatically start DNS services when it is run"
@echo " in its default configuration. Please see the Guide for information"
@echo " on how to configure these services to be started automatically."
- - at echo -e "\033[m" # switch back to normal
+ - at tput rmso # End standout mode
check-valgrind:
if HAVE_VALGRIND
@@ -110,7 +110,8 @@ report-coverage: report-cpp-coverage report-python-coverage
# for static C++ check using cppcheck (when available)
cppcheck:
- cppcheck --enable=all --suppressions src/cppcheck-suppress.lst --inline-suppr \
+ cppcheck -I./src/lib -I./src/bin --enable=all --suppressions \
+ src/cppcheck-suppress.lst --inline-suppr \
--quiet --error-exitcode=1 \
--template '{file}:{line}: check_fail: {message} ({severity},{id})' \
src
@@ -434,6 +435,10 @@ pkgconfig_DATA = dns++.pc
CLEANFILES = $(abs_top_builddir)/logger_lockfile
+# config.h may be included by headers supplied for building user-written
+# hooks libraries, so we need to include it in the distribution.
+pkginclude_HEADERS = config.h
+
if HAVE_GTEST_SOURCE
noinst_LIBRARIES = libgtest.a
libgtest_a_CXXFLAGS = $(GTEST_INCLUDES) $(AM_CXXFLAGS)
diff --git a/configure.ac b/configure.ac
index af63958..1c239a3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,15 +2,21 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.59])
-AC_INIT(bind10, 20130503, bind10-dev at isc.org)
+AC_INIT(bind10, 20130529, bind10-dev at isc.org)
AC_CONFIG_SRCDIR(README)
-# serial-tests is not available in automake version before 1.13. In
-# automake 1.13 and higher, AM_PROG_INSTALL is undefined, so we'll check
-# that and conditionally use serial-tests.
-AM_INIT_AUTOMAKE(
- [foreign]
- m4_ifndef([AM_PROG_INSTALL], [serial-tests])
-)
+
+# serial-tests is not available in automake version before 1.13, so
+# we'll check that and conditionally use serial-tests. This check is
+# adopted from code by Richard W.M. Jones:
+# https://www.redhat.com/archives/libguestfs/2013-February/msg00102.html
+m4_define([serial_tests], [
+ m4_esyscmd([automake --version |
+ head -1 |
+ awk '{split ($NF,a,"."); if (a[1] == 1 && a[2] >= 12) { print "serial-tests" }}'
+ ])
+])
+AM_INIT_AUTOMAKE(foreign serial_tests)
+
m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])dnl be backward compatible
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIR([m4macros])
@@ -68,6 +74,13 @@ AC_CHECK_DECL([__SUNPRO_CC], [SUNCXX="yes"], [SUNCXX="no"])
AC_CHECK_DECL([__clang__], [CLANGPP="yes"], [CLANGPP="no"])
AM_CONDITIONAL(USE_CLANGPP, test "X${CLANGPP}" = "Xyes")
+dnl Determine if weare using GNU sed
+GNU_SED=no
+$SED --version 2> /dev/null | grep GNU > /dev/null 2>&1
+if test $? -eq 0; then
+ GNU_SED=yes
+fi
+
# Linker options
# check -R, "-Wl,-R" or -rpath (we share the AX function defined in
@@ -105,9 +118,12 @@ AC_DEFUN([BIND10_CXX_TRY_FLAG], [
AC_MSG_RESULT([$bind10_cxx_flag])
])
+CXX_VERSION="unknown"
+
# SunStudio compiler requires special compiler options for boost
# (http://blogs.sun.com/sga/entry/boost_mini_howto)
if test "$SUNCXX" = "yes"; then
+CXX_VERSION=`$CXX -V 2> /dev/null | head -1`
CXXFLAGS="$CXXFLAGS -library=stlport4 -features=tmplife -features=tmplrefstatic"
MULTITHREADING_FLAG="-mt"
fi
@@ -120,7 +136,8 @@ fi
# we suppress this particular warning. Note that it doesn't weaken checks
# on the source code.
if test "$CLANGPP" = "yes"; then
- B10_CXXFLAGS="$B10_CXXFLAGS -Qunused-arguments"
+CXX_VERSION=`$CXX --version 2> /dev/null | head -1`
+B10_CXXFLAGS="$B10_CXXFLAGS -Qunused-arguments"
fi
BIND10_CXX_TRY_FLAG([-Wno-missing-field-initializers],
@@ -129,7 +146,8 @@ AC_SUBST(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
# gcc specific settings:
if test "X$GXX" = "Xyes"; then
-B10_CXXFLAGS="$B10_CXXFLAGS -Wall -Wextra -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare"
+CXX_VERSION=`$CXX --version 2> /dev/null | head -1`
+B10_CXXFLAGS="$B10_CXXFLAGS -Wall -Wextra -Wnon-virtual-dtor -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare"
case "$host" in
*-solaris*)
MULTITHREADING_FLAG=-pthreads
@@ -181,6 +199,7 @@ AC_HELP_STRING([--enable-static-link],
[build programs with static link [[default=no]]]),
[enable_static_link=yes], [enable_static_link=no])
AM_CONDITIONAL(USE_STATIC_LINK, test $enable_static_link = yes)
+AM_COND_IF([USE_STATIC_LINK], [AC_DEFINE([USE_STATIC_LINK], [1], [BIND 10 was statically linked?])])
# Check validity about some libtool options
if test $enable_static_link = yes -a $enable_static = no; then
@@ -199,6 +218,7 @@ AC_HELP_STRING([--disable-setproctitle-check],
# OS dependent configuration
SET_ENV_LIBRARY_PATH=no
ENV_LIBRARY_PATH=LD_LIBRARY_PATH
+bind10_undefined_pthread_behavior=no
case "$host" in
*-solaris*)
@@ -210,13 +230,25 @@ case "$host" in
# Destroying locked mutexes, condition variables being waited
# on, etc. are undefined behavior on Solaris, so we set it as
# such here.
- AC_DEFINE([HAS_UNDEFINED_PTHREAD_BEHAVIOR], [1], [Does this platform have some undefined pthreads behavior?])
+ bind10_undefined_pthread_behavior=yes
;;
*-apple-darwin*)
# Starting with OSX 10.7 (Lion) we must choose which IPv6 API to use
# (RFC2292 or RFC3542).
CPPFLAGS="$CPPFLAGS -D__APPLE_USE_RFC_3542"
+ # In OS X 10.9 (and possibly any future versions?) pthread_cond_destroy
+ # doesn't work as documented, which makes some of unit tests fail.
+ # Testing a specific system and version is not a good practice, but
+ # identifying this behavior would be too heavy (running a program
+ # with multiple threads), so this is a compromise. In general,
+ # it should be avoided to rely on 'osx_version' unless there's no
+ # viable alternative.
+ osx_version=`/usr/bin/sw_vers -productVersion`
+ if [ test $osx_version = "10.9" ]; then
+ bind10_undefined_pthread_behavior=yes
+ fi
+
# libtool doesn't work perfectly with Darwin: libtool embeds the
# final install path in dynamic libraries and our loadable python
# modules always refer to that path even if it's loaded within the
@@ -239,6 +271,9 @@ esac
AM_CONDITIONAL(SET_ENV_LIBRARY_PATH, test $SET_ENV_LIBRARY_PATH = yes)
AC_SUBST(SET_ENV_LIBRARY_PATH)
AC_SUBST(ENV_LIBRARY_PATH)
+if [ test $bind10_undefined_pthread_behavior = "yes" ]; then
+ AC_DEFINE([HAS_UNDEFINED_PTHREAD_BEHAVIOR], [1], [Does this platform have some undefined pthreads behavior?])
+fi
# Our experiments have shown Solaris 10 has broken support for the
# IPV6_USE_MIN_MTU socket option for getsockopt(); it doesn't return the value
@@ -375,6 +410,24 @@ fi
AC_SUBST(PYTHON_LIB)
LDFLAGS=$LDFLAGS_SAVED
+# Python 3.2 changed the return type of internal hash function to
+# Py_hash_t and some platforms (such as Solaris) strictly check for long
+# vs Py_hash_t. So we detect and use the appropriate return type.
+# Remove this test (and associated changes in pydnspp_config.h.in) when
+# we require Python 3.2.
+have_py_hash_t=0
+CPPFLAGS_SAVED="$CPPFLAGS"
+CPPFLAGS=${PYTHON_INCLUDES}
+AC_MSG_CHECKING(for Py_hash_t)
+AC_TRY_COMPILE([#include <Python.h>
+ Py_hash_t h;],,
+ [AC_MSG_RESULT(yes)
+ have_py_hash_t=1],
+ [AC_MSG_RESULT(no)])
+CPPFLAGS="$CPPFLAGS_SAVED"
+HAVE_PY_HASH_T=$have_py_hash_t
+AC_SUBST(HAVE_PY_HASH_T)
+
# Check for the setproctitle module
if test "$setproctitle_check" = "yes" ; then
AC_MSG_CHECKING(for setproctitle module)
@@ -392,7 +445,7 @@ fi
# Python 3.2 has an unused parameter in one of its headers. This
# has been reported, but not fixed as of yet, so we check if we need
# to set -Wno-unused-parameter.
-if test "X$GXX" = "Xyes" -a $werror_ok = 1; then
+if test "X$GXX" = "Xyes" -a "$werror_ok" = 1; then
CPPFLAGS_SAVED="$CPPFLAGS"
CPPFLAGS=${PYTHON_INCLUDES}
CXXFLAGS_SAVED="$CXXFLAGS"
@@ -430,6 +483,7 @@ AC_SUBST(B10_CXXFLAGS)
AC_SEARCH_LIBS(inet_pton, [nsl])
AC_SEARCH_LIBS(recvfrom, [socket])
AC_SEARCH_LIBS(nanosleep, [rt])
+AC_SEARCH_LIBS(dlsym, [dl])
# Checks for header files.
@@ -469,6 +523,17 @@ AM_COND_IF([OS_BSD], [AC_DEFINE([OS_BSD], [1], [Running on BSD?])])
AM_CONDITIONAL(OS_SOLARIS, test $OS_TYPE = Solaris)
AM_COND_IF([OS_SOLARIS], [AC_DEFINE([OS_SOLARIS], [1], [Running on Solaris?])])
+# Deal with variants
+AM_CONDITIONAL(OS_FREEBSD, test $system = FreeBSD)
+AM_COND_IF([OS_FREEBSD], [AC_DEFINE([OS_FREEBSD], [1], [Running on FreeBSD?])])
+AM_CONDITIONAL(OS_NETBSD, test $system = NetBSD)
+AM_COND_IF([OS_NETBSD], [AC_DEFINE([OS_NETBSD], [1], [Running on NetBSD?])])
+AM_CONDITIONAL(OS_OPENBSD, test $system = OpenBSD)
+AM_COND_IF([OS_OPENBSD], [AC_DEFINE([OS_OPENBSD], [1], [Running on OpenBSD?])])
+AM_CONDITIONAL(OS_OSX, test $system = Darwin)
+AM_COND_IF([OS_OSX], [AC_DEFINE([OS_OSX], [1], [Running on OSX?])])
+
+
AC_MSG_CHECKING(for sa_len in struct sockaddr)
AC_TRY_COMPILE([
#include <sys/types.h>
@@ -705,6 +770,21 @@ then
BOTAN_INCLUDES="-I`${BOTAN_CONFIG} --prefix`/include ${BOTAN_INCLUDES}"
fi
fi
+
+dnl Determine the Botan version
+AC_MSG_CHECKING([Botan version])
+cat > conftest.cpp << EOF
+#include <botan/version.h>
+AUTOCONF_BOTAN_VERSION=BOTAN_VERSION_MAJOR . BOTAN_VERSION_MINOR . BOTAN_VERSION_PATCH
+EOF
+
+BOTAN_VERSION=`$CPP $CPPFLAGS $BOTAN_INCLUDES conftest.cpp | grep '^AUTOCONF_BOTAN_VERSION=' | $SED -e 's/^AUTOCONF_BOTAN_VERSION=//' -e 's/[[ ]]//g' -e 's/"//g' 2> /dev/null`
+if test -z "$BOTAN_VERSION"; then
+ BOTAN_VERSION="unknown"
+fi
+$RM -f conftest.cpp
+AC_MSG_RESULT([$BOTAN_VERSION])
+
# botan-config script (and the way we call pkg-config) returns -L and -l
# as one string, but we need them in separate values
BOTAN_LDFLAGS=
@@ -742,7 +822,24 @@ CPPFLAGS_SAVED=$CPPFLAGS
CPPFLAGS="$BOTAN_INCLUDES $CPPFLAGS"
LIBS_SAVED="$LIBS"
LIBS="$LIBS $BOTAN_LIBS"
-AC_CHECK_HEADERS([botan/botan.h],,AC_MSG_ERROR([Missing required header files.]))
+
+# ac_header_preproc is an autoconf symbol (undocumented but stable) that
+# is set if the pre-processor phase passes. Thus by adding a custom
+# failure handler we can detect the difference between a header not existing
+# (or not even passing the pre-processor phase) and a header file resulting
+# in compilation failures.
+AC_CHECK_HEADERS([botan/botan.h],,[
+ if test "x$ac_header_preproc" = "xyes"; then
+ AC_MSG_ERROR([
+botan/botan.h was found but is unusable. The most common cause of this problem
+is attempting to use an updated C++ compiler with older C++ libraries, such as
+the version of Botan that comes with your distribution. If you have updated
+your C++ compiler we highly recommend that you use support libraries such as
+Boost and Botan that were compiled with the same compiler version.])
+ else
+ AC_MSG_ERROR([Missing required header files.])
+ fi]
+)
AC_LINK_IFELSE(
[AC_LANG_PROGRAM([#include <botan/botan.h>
#include <botan/hash.h>
@@ -784,6 +881,7 @@ if test "$MYSQL_CONFIG" != "" ; then
MYSQL_CPPFLAGS=`$MYSQL_CONFIG --cflags`
MYSQL_LIBS=`$MYSQL_CONFIG --libs`
+ MYSQL_VERSION=`$MYSQL_CONFIG --version`
AC_SUBST(MYSQL_CPPFLAGS)
AC_SUBST(MYSQL_LIBS)
@@ -862,6 +960,20 @@ AC_LINK_IFELSE(
AC_MSG_ERROR([Needs log4cplus library])]
)
+dnl Determine the log4cplus version, used mainly for config.report.
+AC_MSG_CHECKING([log4cplus version])
+cat > conftest.cpp << EOF
+#include <log4cplus/version.h>
+AUTOCONF_LOG4CPLUS_VERSION=LOG4CPLUS_VERSION_STR
+EOF
+
+LOG4CPLUS_VERSION=`$CPP $CPPFLAGS conftest.cpp | grep '^AUTOCONF_LOG4CPLUS_VERSION=' | $SED -e 's/^AUTOCONF_LOG4CPLUS_VERSION=//' -e 's/[[ ]]//g' -e 's/"//g' 2> /dev/null`
+if test -z "$LOG4CPLUS_VERSION"; then
+ LOG4CPLUS_VERSION="unknown"
+fi
+$RM -f conftest.cpp
+AC_MSG_RESULT([$LOG4CPLUS_VERSION])
+
CPPFLAGS=$CPPFLAGS_SAVED
LIBS=$LIBS_SAVED
@@ -871,10 +983,14 @@ LIBS=$LIBS_SAVED
AX_BOOST_FOR_BIND10
# Boost offset_ptr is required in one library and not optional right now, so
# we unconditionally fail here if it doesn't work.
-if test "$BOOST_OFFSET_PTR_FAILURE" = "yes"; then
+if test "$BOOST_OFFSET_PTR_WOULDFAIL" = "yes" -a "$werror_ok" = 1; then
AC_MSG_ERROR([Failed to compile a required header file. Try upgrading Boost to 1.44 or higher (when using clang++) or specifying --without-werror. See the ChangeLog entry for Trac no. 2147 for more details.])
fi
+if test "$BOOST_STATIC_ASSERT_WOULDFAIL" = "yes" -a X"$werror_ok" = X1; then
+ AC_MSG_ERROR([Failed to use Boost static assertions. Try upgrading Boost to 1.54 or higher (when using GCC 4.8) or specifying --without-werror. See trac ticket no. 3039 for more details.])
+fi
+
# There's a known bug in FreeBSD ports for Boost that would trigger a false
# warning in build with g++ and -Werror (we exclude clang++ explicitly to
# avoid unexpected false positives).
@@ -882,6 +998,19 @@ if test "$BOOST_NUMERIC_CAST_WOULDFAIL" = "yes" -a X"$werror_ok" = X1 -a $CLANGP
AC_MSG_ERROR([Failed to compile a required header file. If you are using FreeBSD and Boost installed via ports, retry with specifying --without-werror. See the ChangeLog entry for Trac no. 1991 for more details.])
fi
+build_experimental_resolver=no
+AC_ARG_ENABLE(experimental-resolver,
+ [AC_HELP_STRING([--enable-experimental-resolver],
+ [enable building of the experimental resolver [default=no]])],
+ [build_experimental_resolver=$enableval])
+AM_CONDITIONAL([BUILD_EXPERIMENTAL_RESOLVER], [test "$build_experimental_resolver" = "yes"])
+if test "$build_experimental_resolver" = "yes"; then
+ BUILD_EXPERIMENTAL_RESOLVER=yes
+else
+ BUILD_EXPERIMENTAL_RESOLVER=no
+fi
+AC_SUBST(BUILD_EXPERIMENTAL_RESOLVER)
+
use_shared_memory=yes
AC_ARG_WITH(shared-memory,
AC_HELP_STRING([--with-shared-memory],
@@ -891,8 +1020,22 @@ if test X$use_shared_memory = Xyes -a "$BOOST_MAPPED_FILE_WOULDFAIL" = "yes"; th
AC_MSG_ERROR([Boost shared memory does not compile on this system. If you don't need it (most normal users won't) build without it by rerunning this script with --without-shared-memory; using a different compiler or a different version of Boost may also help.])
fi
AM_CONDITIONAL([USE_SHARED_MEMORY], [test x$use_shared_memory = xyes])
+if test "x$use_shared_memory" = "xyes"; then
+ AC_DEFINE(USE_SHARED_MEMORY, 1, [Define to 1 if shared memory support is enabled])
+fi
AC_SUBST(BOOST_MAPPED_FILE_CXXFLAG)
+if test "$BOOST_OFFSET_PTR_OLD" = "yes" -a "$use_shared_memory" = "yes" ; then
+ AC_MSG_ERROR([You're trying to compile against boost older than 1.48 with
+shared memory. Older versions of boost have a bug which causes segfaults in
+offset_ptr implementation when compiled by GCC with optimisations enabled.
+See ticket no. 3025 for details.
+
+Either update boost to newer version or use --without-shared-memory.
+Note that most users likely don't need shared memory support.
+])
+fi
+
# Add some default CPP flags needed for Boost, identified by the AX macro.
CPPFLAGS="$CPPFLAGS $CPPFLAGS_BOOST_THREADCONF"
@@ -910,6 +1053,7 @@ AC_SUBST(MULTITHREADING_FLAG)
GTEST_LDFLAGS=
GTEST_LDADD=
DISTCHECK_GTEST_CONFIGURE_FLAG=
+GTEST_VERSION="unknown"
if test "x$enable_gtest" = "xyes" ; then
@@ -965,6 +1109,7 @@ if test "$gtest_path" != "no" ; then
GTEST_INCLUDES=`${GTEST_CONFIG} --cppflags`
GTEST_LDFLAGS=`${GTEST_CONFIG} --ldflags`
GTEST_LDADD=`${GTEST_CONFIG} --libs`
+ GTEST_VERSION=`${GTEST_CONFIG} --version`
GTEST_FOUND="true"
else
AC_MSG_WARN([Unable to locate Google Test gtest-config.])
@@ -1050,6 +1195,8 @@ fi
AX_SQLITE3_FOR_BIND10
if test "x$have_sqlite" = "xyes" ; then
enable_features="$enable_features SQLite3"
+
+ AX_PYTHON_SQLITE3
fi
#
@@ -1113,6 +1260,11 @@ if test "x$enable_generate_docs" != xno ; then
fi
AC_MSG_RESULT(yes)
fi
+
+ AC_PATH_PROG([ELINKS], [elinks])
+ if test -z "$ELINKS"; then
+ AC_MSG_ERROR("elinks not found; it is required for --enable-generate-docs")
+ fi
fi
@@ -1129,6 +1281,14 @@ AC_ARG_ENABLE(logger-checks, [AC_HELP_STRING([--enable-logger-checks],
AM_CONDITIONAL(ENABLE_LOGGER_CHECKS, test x$enable_logger_checks != xno)
AM_COND_IF([ENABLE_LOGGER_CHECKS], [AC_DEFINE([ENABLE_LOGGER_CHECKS], [1], [Check logger messages?])])
+# Check for asciidoc
+AC_PATH_PROG(ASCIIDOC, asciidoc, no)
+AM_CONDITIONAL(HAVE_ASCIIDOC, test "x$ASCIIDOC" != "xno")
+
+# Check for plantuml
+AC_PATH_PROG(PLANTUML, plantuml, no)
+AM_CONDITIONAL(HAVE_PLANTUML, test "x$PLANTUML" != "xno")
+
# Check for valgrind
AC_PATH_PROG(VALGRIND, valgrind, no)
AM_CONDITIONAL(HAVE_VALGRIND, test "x$VALGRIND" != "xno")
@@ -1165,264 +1325,305 @@ AC_TRY_LINK(
AM_CONDITIONAL(HAVE_OPTRESET, test "x$var_optreset_exists" != "xno")
AM_COND_IF([HAVE_OPTRESET], [AC_DEFINE([HAVE_OPTRESET], [1], [Check for optreset?])])
-AC_CONFIG_FILES([Makefile
- doc/Makefile
+AC_CONFIG_FILES([compatcheck/Makefile
+ dns++.pc
+ doc/design/datasrc/Makefile
+ doc/design/Makefile
doc/guide/Makefile
- compatcheck/Makefile
- src/Makefile
- src/bin/Makefile
+ doc/Makefile
+ doc/version.ent
+ ext/asio/asio/Makefile
+ ext/asio/Makefile
+ ext/Makefile
+ m4macros/Makefile
+ Makefile
+ src/bin/auth/auth.spec.pre
+ src/bin/auth/benchmarks/Makefile
+ src/bin/auth/gen-statisticsitems.py.pre
+ src/bin/auth/Makefile
+ src/bin/auth/spec_config.h.pre
+ src/bin/auth/tests/Makefile
+ src/bin/auth/tests/testdata/example-base.zone
+ src/bin/auth/tests/testdata/example-nsec3.zone
+ src/bin/auth/tests/testdata/example.zone
+ src/bin/auth/tests/testdata/Makefile
src/bin/bind10/bind10
+ src/bin/bind10/init.py
src/bin/bind10/Makefile
+ src/bin/bind10/run_bind10.sh
+ src/bin/bind10/tests/init_test.py
src/bin/bind10/tests/Makefile
- src/bin/cmdctl/Makefile
- src/bin/cmdctl/tests/Makefile
+ src/bin/bindctl/bindctl_main.py
src/bin/bindctl/Makefile
+ src/bin/bindctl/run_bindctl.sh
+ src/bin/bindctl/tests/bindctl_test
src/bin/bindctl/tests/Makefile
- src/bin/cfgmgr/Makefile
+ src/bin/cfgmgr/b10-cfgmgr.py
src/bin/cfgmgr/local_plugins/Makefile
+ src/bin/cfgmgr/Makefile
+ src/bin/cfgmgr/plugins/datasrc.spec.pre
src/bin/cfgmgr/plugins/Makefile
src/bin/cfgmgr/plugins/tests/Makefile
+ src/bin/cfgmgr/tests/b10-cfgmgr_test.py
src/bin/cfgmgr/tests/Makefile
+ src/bin/cmdctl/cmdctl.py
+ src/bin/cmdctl/cmdctl.spec.pre
+ src/bin/cmdctl/Makefile
+ src/bin/cmdctl/run_b10-cmdctl.sh
+ src/bin/cmdctl/tests/cmdctl_test
+ src/bin/cmdctl/tests/Makefile
+ src/bin/d2/Makefile
+ src/bin/d2/spec_config.h.pre
+ src/bin/d2/tests/Makefile
+ src/bin/d2/tests/test_data_files_config.h
+ src/bin/dbutil/dbutil.py
src/bin/dbutil/Makefile
+ src/bin/dbutil/run_dbutil.sh
+ src/bin/dbutil/tests/dbutil_test.sh
src/bin/dbutil/tests/Makefile
src/bin/dbutil/tests/testdata/Makefile
- src/bin/loadzone/Makefile
- src/bin/loadzone/tests/Makefile
- src/bin/loadzone/tests/correct/Makefile
- src/bin/msgq/Makefile
- src/bin/msgq/tests/Makefile
- src/bin/auth/Makefile
- src/bin/auth/tests/Makefile
- src/bin/auth/tests/testdata/Makefile
- src/bin/auth/benchmarks/Makefile
+ src/bin/ddns/ddns.py
src/bin/ddns/Makefile
src/bin/ddns/tests/Makefile
- src/bin/dhcp6/Makefile
- src/bin/dhcp6/tests/Makefile
src/bin/dhcp4/Makefile
+ src/bin/dhcp4/spec_config.h.pre
src/bin/dhcp4/tests/Makefile
+ src/bin/dhcp4/tests/marker_file.h
+ src/bin/dhcp4/tests/test_data_files_config.h
+ src/bin/dhcp4/tests/test_libraries.h
+ src/bin/dhcp6/Makefile
+ src/bin/dhcp6/spec_config.h.pre
+ src/bin/dhcp6/tests/Makefile
+ src/bin/dhcp6/tests/marker_file.h
+ src/bin/dhcp6/tests/test_data_files_config.h
+ src/bin/dhcp6/tests/test_libraries.h
+ src/bin/loadzone/loadzone.py
+ src/bin/loadzone/Makefile
+ src/bin/loadzone/run_loadzone.sh
+ src/bin/loadzone/tests/correct/correct_test.sh
+ src/bin/loadzone/tests/correct/Makefile
+ src/bin/loadzone/tests/Makefile
+ src/bin/Makefile
+ src/bin/memmgr/Makefile
+ src/bin/memmgr/memmgr.py
+ src/bin/memmgr/memmgr.spec.pre
+ src/bin/memmgr/tests/Makefile
+ src/bin/msgq/Makefile
+ src/bin/msgq/msgq.py
+ src/bin/msgq/run_msgq.sh
+ src/bin/msgq/tests/Makefile
+ src/bin/resolver/bench/Makefile
src/bin/resolver/Makefile
+ src/bin/resolver/resolver.spec.pre
+ src/bin/resolver/spec_config.h.pre
src/bin/resolver/tests/Makefile
- src/bin/resolver/bench/Makefile
- src/bin/sysinfo/Makefile
src/bin/sockcreator/Makefile
src/bin/sockcreator/tests/Makefile
+ src/bin/stats/Makefile
+ src/bin/stats/stats_httpd.py
+ src/bin/stats/stats.py
+ src/bin/stats/tests/Makefile
+ src/bin/stats/tests/testdata/Makefile
+ src/bin/sysinfo/Makefile
+ src/bin/sysinfo/run_sysinfo.sh
+ src/bin/sysinfo/sysinfo.py
+ src/bin/tests/Makefile
+ src/bin/tests/process_rename_test.py
+ src/bin/usermgr/b10-cmdctl-usermgr.py
+ src/bin/usermgr/Makefile
+ src/bin/usermgr/run_b10-cmdctl-usermgr.sh
+ src/bin/usermgr/tests/Makefile
src/bin/xfrin/Makefile
+ src/bin/xfrin/run_b10-xfrin.sh
src/bin/xfrin/tests/Makefile
src/bin/xfrin/tests/testdata/Makefile
+ src/bin/xfrin/tests/xfrin_test
+ src/bin/xfrin/xfrin.py
src/bin/xfrout/Makefile
+ src/bin/xfrout/run_b10-xfrout.sh
src/bin/xfrout/tests/Makefile
+ src/bin/xfrout/tests/xfrout_test
+ src/bin/xfrout/tests/xfrout_test.py
+ src/bin/xfrout/xfrout.py
+ src/bin/xfrout/xfrout.spec.pre
src/bin/zonemgr/Makefile
+ src/bin/zonemgr/run_b10-zonemgr.sh
src/bin/zonemgr/tests/Makefile
- src/bin/stats/Makefile
- src/bin/stats/tests/Makefile
- src/bin/stats/tests/testdata/Makefile
- src/bin/usermgr/Makefile
- src/bin/usermgr/tests/Makefile
- src/bin/tests/Makefile
- src/lib/Makefile
- src/lib/asiolink/Makefile
- src/lib/asiolink/tests/Makefile
+ src/bin/zonemgr/tests/zonemgr_test
+ src/bin/zonemgr/zonemgr.py
+ src/bin/zonemgr/zonemgr.spec.pre
+ src/hooks/dhcp/Makefile
+ src/hooks/dhcp/user_chk/Makefile
+ src/hooks/dhcp/user_chk/tests/Makefile
+ src/hooks/dhcp/user_chk/tests/test_data_files_config.h
+ src/hooks/Makefile
+ src/lib/acl/Makefile
+ src/lib/acl/tests/Makefile
src/lib/asiodns/Makefile
src/lib/asiodns/tests/Makefile
- src/lib/bench/Makefile
+ src/lib/asiolink/Makefile
+ src/lib/asiolink/tests/Makefile
src/lib/bench/example/Makefile
+ src/lib/bench/Makefile
src/lib/bench/tests/Makefile
+ src/lib/cache/Makefile
+ src/lib/cache/tests/Makefile
src/lib/cc/Makefile
+ src/lib/cc/session_config.h.pre
src/lib/cc/tests/Makefile
- src/lib/python/Makefile
- src/lib/python/isc/Makefile
+ src/lib/cc/tests/session_unittests_config.h
+ src/lib/config/Makefile
+ src/lib/config/tests/data_def_unittests_config.h
+ src/lib/config/tests/Makefile
+ src/lib/config/tests/testdata/Makefile
+ src/lib/cryptolink/Makefile
+ src/lib/cryptolink/tests/Makefile
+ src/lib/datasrc/datasrc_config.h.pre
+ src/lib/datasrc/Makefile
+ src/lib/datasrc/memory/benchmarks/Makefile
+ src/lib/datasrc/memory/Makefile
+ src/lib/datasrc/tests/Makefile
+ src/lib/datasrc/tests/memory/Makefile
+ src/lib/datasrc/tests/memory/testdata/Makefile
+ src/lib/datasrc/tests/testdata/Makefile
+ src/lib/dhcp_ddns/Makefile
+ src/lib/dhcp_ddns/tests/Makefile
+ src/lib/dhcp/Makefile
+ src/lib/dhcpsrv/Makefile
+ src/lib/dhcpsrv/tests/Makefile
+ src/lib/dhcpsrv/tests/test_libraries.h
+ src/lib/dhcp/tests/Makefile
+ src/lib/dns/benchmarks/Makefile
+ src/lib/dns/gen-rdatacode.py
+ src/lib/dns/Makefile
+ src/lib/dns/python/Makefile
+ src/lib/dns/python/pydnspp_config.h
+ src/lib/dns/python/tests/Makefile
+ src/lib/dns/tests/Makefile
+ src/lib/dns/tests/testdata/Makefile
+ src/lib/exceptions/Makefile
+ src/lib/exceptions/tests/Makefile
+ src/lib/hooks/Makefile
+ src/lib/hooks/tests/Makefile
+ src/lib/hooks/tests/marker_file.h
+ src/lib/hooks/tests/test_libraries.h
+ src/lib/log/compiler/Makefile
+ src/lib/log/interprocess/Makefile
+ src/lib/log/interprocess/tests/Makefile
+ src/lib/log/Makefile
+ src/lib/log/tests/buffer_logger_test.sh
+ src/lib/log/tests/console_test.sh
+ src/lib/log/tests/destination_test.sh
+ src/lib/log/tests/init_logger_test.sh
+ src/lib/log/tests/local_file_test.sh
+ src/lib/log/tests/logger_lock_test.sh
+ src/lib/log/tests/Makefile
+ src/lib/log/tests/severity_test.sh
+ src/lib/log/tests/tempdir.h
+ src/lib/Makefile
+ src/lib/nsas/Makefile
+ src/lib/nsas/tests/Makefile
+ src/lib/python/bind10_config.py
src/lib/python/isc/acl/Makefile
src/lib/python/isc/acl/tests/Makefile
- src/lib/python/isc/util/Makefile
- src/lib/python/isc/util/tests/Makefile
- src/lib/python/isc/util/cio/Makefile
- src/lib/python/isc/util/cio/tests/Makefile
- src/lib/python/isc/datasrc/Makefile
- src/lib/python/isc/datasrc/tests/Makefile
- src/lib/python/isc/dns/Makefile
- src/lib/python/isc/cc/Makefile
+ src/lib/python/isc/bind10/Makefile
+ src/lib/python/isc/bind10/tests/Makefile
src/lib/python/isc/cc/cc_generated/Makefile
+ src/lib/python/isc/cc/Makefile
+ src/lib/python/isc/cc/tests/cc_test
src/lib/python/isc/cc/tests/Makefile
src/lib/python/isc/config/Makefile
+ src/lib/python/isc/config/tests/config_test
src/lib/python/isc/config/tests/Makefile
+ src/lib/python/isc/datasrc/Makefile
+ src/lib/python/isc/datasrc/tests/Makefile
+ src/lib/python/isc/datasrc/tests/testdata/Makefile
+ src/lib/python/isc/ddns/Makefile
+ src/lib/python/isc/ddns/tests/Makefile
+ src/lib/python/isc/dns/Makefile
src/lib/python/isc/log/Makefile
- src/lib/python/isc/log/tests/Makefile
src/lib/python/isc/log_messages/Makefile
+ src/lib/python/isc/log_messages/work/__init__.py
src/lib/python/isc/log_messages/work/Makefile
+ src/lib/python/isc/log/tests/log_console.py
+ src/lib/python/isc/log/tests/Makefile
+ src/lib/python/isc/Makefile
+ src/lib/python/isc/memmgr/Makefile
+ src/lib/python/isc/memmgr/tests/Makefile
+ src/lib/python/isc/memmgr/tests/testdata/Makefile
src/lib/python/isc/net/Makefile
src/lib/python/isc/net/tests/Makefile
src/lib/python/isc/notify/Makefile
src/lib/python/isc/notify/tests/Makefile
- src/lib/python/isc/testutils/Makefile
- src/lib/python/isc/bind10/Makefile
- src/lib/python/isc/bind10/tests/Makefile
- src/lib/python/isc/ddns/Makefile
- src/lib/python/isc/ddns/tests/Makefile
- src/lib/python/isc/xfrin/Makefile
- src/lib/python/isc/xfrin/tests/Makefile
+ src/lib/python/isc/notify/tests/notify_out_test
src/lib/python/isc/server_common/Makefile
src/lib/python/isc/server_common/tests/Makefile
- src/lib/python/isc/sysinfo/Makefile
- src/lib/python/isc/sysinfo/tests/Makefile
src/lib/python/isc/statistics/Makefile
src/lib/python/isc/statistics/tests/Makefile
- src/lib/config/Makefile
- src/lib/config/tests/Makefile
- src/lib/config/tests/testdata/Makefile
- src/lib/cryptolink/Makefile
- src/lib/cryptolink/tests/Makefile
- src/lib/dns/Makefile
- src/lib/dns/tests/Makefile
- src/lib/dns/tests/testdata/Makefile
- src/lib/dns/python/Makefile
- src/lib/dns/python/tests/Makefile
- src/lib/dns/benchmarks/Makefile
- src/lib/dhcp/Makefile
- src/lib/dhcp/tests/Makefile
- src/lib/dhcpsrv/Makefile
- src/lib/dhcpsrv/tests/Makefile
- src/lib/exceptions/Makefile
- src/lib/exceptions/tests/Makefile
- src/lib/datasrc/Makefile
- src/lib/datasrc/memory/Makefile
- src/lib/datasrc/memory/benchmarks/Makefile
- src/lib/datasrc/tests/Makefile
- src/lib/datasrc/tests/testdata/Makefile
- src/lib/datasrc/tests/memory/Makefile
- src/lib/datasrc/tests/memory/testdata/Makefile
- src/lib/xfr/Makefile
- src/lib/xfr/tests/Makefile
- src/lib/log/Makefile
- src/lib/log/compiler/Makefile
- src/lib/log/tests/Makefile
+ src/lib/python/isc/sysinfo/Makefile
+ src/lib/python/isc/sysinfo/tests/Makefile
+ src/lib/python/isc/testutils/Makefile
+ src/lib/python/isc/util/cio/Makefile
+ src/lib/python/isc/util/cio/tests/Makefile
+ src/lib/python/isc/util/Makefile
+ src/lib/python/isc/util/tests/Makefile
+ src/lib/python/isc/xfrin/Makefile
+ src/lib/python/isc/xfrin/tests/Makefile
+ src/lib/python/Makefile
src/lib/resolve/Makefile
src/lib/resolve/tests/Makefile
- src/lib/testutils/Makefile
- src/lib/testutils/testdata/Makefile
- src/lib/nsas/Makefile
- src/lib/nsas/tests/Makefile
- src/lib/cache/Makefile
- src/lib/cache/tests/Makefile
src/lib/server_common/Makefile
+ src/lib/server_common/tests/data_path.h
src/lib/server_common/tests/Makefile
- src/lib/util/Makefile
+ src/lib/statistics/Makefile
+ src/lib/statistics/tests/Makefile
+ src/lib/testutils/Makefile
+ src/lib/testutils/testdata/Makefile
src/lib/util/io/Makefile
- src/lib/util/threads/Makefile
- src/lib/util/threads/tests/Makefile
- src/lib/util/unittests/Makefile
+ src/lib/util/Makefile
+ src/lib/util/python/doxygen2pydoc.py
+ src/lib/util/python/gen_wiredata.py
src/lib/util/python/Makefile
+ src/lib/util/python/mkpywrapper.py
src/lib/util/pyunittests/Makefile
src/lib/util/tests/Makefile
- src/lib/acl/Makefile
- src/lib/acl/tests/Makefile
- src/lib/statistics/Makefile
- src/lib/statistics/tests/Makefile
+ src/lib/util/threads/Makefile
+ src/lib/util/threads/tests/Makefile
+ src/lib/util/unittests/Makefile
+ src/lib/xfr/Makefile
+ src/lib/xfr/tests/Makefile
+ src/Makefile
+ tests/lettuce/Makefile
+ tests/lettuce/setup_intree_bind10.sh
tests/Makefile
- tests/tools/Makefile
tests/tools/badpacket/Makefile
tests/tools/badpacket/tests/Makefile
+ tests/tools/Makefile
tests/tools/perfdhcp/Makefile
tests/tools/perfdhcp/tests/Makefile
tests/tools/perfdhcp/tests/testdata/Makefile
- m4macros/Makefile
- dns++.pc
- ])
-AC_OUTPUT([doc/version.ent
- src/bin/cfgmgr/b10-cfgmgr.py
- src/bin/cfgmgr/tests/b10-cfgmgr_test.py
- src/bin/cfgmgr/plugins/datasrc.spec.pre
- src/bin/cmdctl/cmdctl.py
- src/bin/cmdctl/run_b10-cmdctl.sh
- src/bin/cmdctl/tests/cmdctl_test
- src/bin/cmdctl/cmdctl.spec.pre
- src/bin/dbutil/dbutil.py
- src/bin/dbutil/run_dbutil.sh
- src/bin/dbutil/tests/dbutil_test.sh
- src/bin/ddns/ddns.py
- src/bin/xfrin/tests/xfrin_test
- src/bin/xfrin/xfrin.py
- src/bin/xfrin/run_b10-xfrin.sh
- src/bin/xfrout/xfrout.py
- src/bin/xfrout/xfrout.spec.pre
- src/bin/xfrout/tests/xfrout_test
- src/bin/xfrout/tests/xfrout_test.py
- src/bin/xfrout/run_b10-xfrout.sh
- src/bin/resolver/resolver.spec.pre
- src/bin/resolver/spec_config.h.pre
- src/bin/zonemgr/zonemgr.py
- src/bin/zonemgr/zonemgr.spec.pre
- src/bin/zonemgr/tests/zonemgr_test
- src/bin/zonemgr/run_b10-zonemgr.sh
- src/bin/sysinfo/sysinfo.py
- src/bin/sysinfo/run_sysinfo.sh
- src/bin/stats/stats.py
- src/bin/stats/stats_httpd.py
- src/bin/bind10/init.py
- src/bin/bind10/run_bind10.sh
- src/bin/bind10/tests/init_test.py
- src/bin/bindctl/run_bindctl.sh
- src/bin/bindctl/bindctl_main.py
- src/bin/bindctl/tests/bindctl_test
- src/bin/loadzone/run_loadzone.sh
- src/bin/loadzone/tests/correct/correct_test.sh
- src/bin/loadzone/loadzone.py
- src/bin/usermgr/run_b10-cmdctl-usermgr.sh
- src/bin/usermgr/b10-cmdctl-usermgr.py
- src/bin/msgq/msgq.py
- src/bin/msgq/run_msgq.sh
- src/bin/auth/auth.spec.pre
- src/bin/auth/spec_config.h.pre
- src/bin/auth/tests/testdata/example.zone
- src/bin/auth/tests/testdata/example-base.zone
- src/bin/auth/tests/testdata/example-nsec3.zone
- src/bin/auth/gen-statisticsitems.py.pre
- src/bin/dhcp4/spec_config.h.pre
- src/bin/dhcp6/spec_config.h.pre
- src/bin/tests/process_rename_test.py
- src/lib/config/tests/data_def_unittests_config.h
- src/lib/python/isc/config/tests/config_test
- src/lib/python/isc/cc/tests/cc_test
- src/lib/python/isc/notify/tests/notify_out_test
- src/lib/python/isc/log/tests/log_console.py
- src/lib/python/isc/log_messages/work/__init__.py
- src/lib/dns/gen-rdatacode.py
- src/lib/python/bind10_config.py
- src/lib/cc/session_config.h.pre
- src/lib/cc/tests/session_unittests_config.h
- src/lib/datasrc/datasrc_config.h.pre
- src/lib/log/tests/console_test.sh
- src/lib/log/tests/destination_test.sh
- src/lib/log/tests/init_logger_test.sh
- src/lib/log/tests/buffer_logger_test.sh
- src/lib/log/tests/local_file_test.sh
- src/lib/log/tests/logger_lock_test.sh
- src/lib/log/tests/severity_test.sh
- src/lib/log/tests/tempdir.h
- src/lib/util/python/mkpywrapper.py
- src/lib/util/python/gen_wiredata.py
- src/lib/server_common/tests/data_path.h
- tests/lettuce/setup_intree_bind10.sh
- ], [
- chmod +x src/bin/cmdctl/run_b10-cmdctl.sh
- chmod +x src/bin/xfrin/run_b10-xfrin.sh
- chmod +x src/bin/xfrout/run_b10-xfrout.sh
- chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
+])
+
+ AC_CONFIG_COMMANDS([permissions], [
chmod +x src/bin/bind10/bind10
chmod +x src/bin/bind10/run_bind10.sh
+ chmod +x src/bin/bindctl/run_bindctl.sh
+ chmod +x src/bin/bindctl/tests/bindctl_test
+ chmod +x src/bin/cmdctl/run_b10-cmdctl.sh
chmod +x src/bin/cmdctl/tests/cmdctl_test
chmod +x src/bin/dbutil/run_dbutil.sh
chmod +x src/bin/dbutil/tests/dbutil_test.sh
- chmod +x src/bin/xfrin/tests/xfrin_test
- chmod +x src/bin/xfrout/tests/xfrout_test
- chmod +x src/bin/zonemgr/tests/zonemgr_test
- chmod +x src/bin/bindctl/tests/bindctl_test
- chmod +x src/bin/bindctl/run_bindctl.sh
chmod +x src/bin/loadzone/run_loadzone.sh
chmod +x src/bin/loadzone/tests/correct/correct_test.sh
+ chmod +x src/bin/msgq/run_msgq.sh
chmod +x src/bin/sysinfo/run_sysinfo.sh
chmod +x src/bin/usermgr/run_b10-cmdctl-usermgr.sh
- chmod +x src/bin/msgq/run_msgq.sh
+ chmod +x src/bin/xfrin/run_b10-xfrin.sh
+ chmod +x src/bin/xfrin/tests/xfrin_test
+ chmod +x src/bin/xfrout/run_b10-xfrout.sh
+ chmod +x src/bin/xfrout/tests/xfrout_test
+ chmod +x src/bin/zonemgr/run_b10-zonemgr.sh
+ chmod +x src/bin/zonemgr/tests/zonemgr_test
chmod +x src/lib/dns/gen-rdatacode.py
chmod +x src/lib/log/tests/console_test.sh
chmod +x src/lib/log/tests/destination_test.sh
@@ -1430,10 +1631,12 @@ AC_OUTPUT([doc/version.ent
chmod +x src/lib/log/tests/local_file_test.sh
chmod +x src/lib/log/tests/logger_lock_test.sh
chmod +x src/lib/log/tests/severity_test.sh
- chmod +x src/lib/util/python/mkpywrapper.py
- chmod +x src/lib/util/python/gen_wiredata.py
chmod +x src/lib/python/isc/log/tests/log_console.py
- ])
+ chmod +x src/lib/util/python/doxygen2pydoc.py
+ chmod +x src/lib/util/python/gen_wiredata.py
+ chmod +x src/lib/util/python/mkpywrapper.py
+])
+
AC_OUTPUT
dnl Print the results
@@ -1445,39 +1648,69 @@ cat > config.report << END
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Package:
- Name: $PACKAGE_NAME
- Version: $PACKAGE_VERSION
-
-C++ Compiler: $CXX
-
-Flags:
- DEFS: $DEFS
- CPPFLAGS: $CPPFLAGS
- CXXFLAGS: $CXXFLAGS
- LDFLAGS: $LDFLAGS
- B10_CXXFLAGS: $B10_CXXFLAGS
- OS Family: $OS_TYPE
-dnl includes too
- Python: ${PYTHON_INCLUDES}
- ${PYTHON_CXXFLAGS}
- ${PYTHON_LDFLAGS}
- ${PYTHON_LIB}
- Boost: ${BOOST_INCLUDES}
- Botan: ${BOTAN_INCLUDES}
- ${BOTAN_LDFLAGS}
- ${BOTAN_LIBS}
- Log4cplus: ${LOG4CPLUS_INCLUDES}
- ${LOG4CPLUS_LIBS}
- SQLite: $SQLITE_CFLAGS
- $SQLITE_LIBS
+ Name: ${PACKAGE_NAME}
+ Version: ${PACKAGE_VERSION}
+ OS Family: ${OS_TYPE}
+ Using GNU sed: ${GNU_SED}
+
+C++ Compiler:
+ CXX: ${CXX}
+ CXX_VERSION: ${CXX_VERSION}
+ DEFS: ${DEFS}
+ CPPFLAGS: ${CPPFLAGS}
+ CXXFLAGS: ${CXXFLAGS}
+ LDFLAGS: ${LDFLAGS}
+ B10_CXXFLAGS: ${B10_CXXFLAGS}
+
+Python:
+ PYTHON_VERSION: ${PYTHON_VERSION}
+ PYTHON_INCLUDES: ${PYTHON_INCLUDES}
+ PYTHON_CXXFLAGS: ${PYTHON_CXXFLAGS}
+ PYTHON_LDFLAGS: ${PYTHON_LDFLAGS}
+ PYTHON_LIB: ${PYTHON_LIB}
+
+Boost:
+ BOOST_VERSION: ${BOOST_VERSION}
+ BOOST_INCLUDES: ${BOOST_INCLUDES}
+
+Botan:
+ BOTAN_VERSION: ${BOTAN_VERSION}
+ BOTAN_INCLUDES: ${BOTAN_INCLUDES}
+ BOTAN_LDFLAGS: ${BOTAN_LDFLAGS}
+ BOTAN_LIBS: ${BOTAN_LIBS}
+
+Log4cplus:
+ LOG4CPLUS_VERSION: ${LOG4CPLUS_VERSION}
+ LOG4CPLUS_INCLUDES: ${LOG4CPLUS_INCLUDES}
+ LOG4CPLUS_LIBS: ${LOG4CPLUS_LIBS}
+
+SQLite:
+ SQLITE_VERSION: ${SQLITE_VERSION}
+ SQLITE_CFLAGS: ${SQLITE_CFLAGS}
+ SQLITE_LIBS: ${SQLITE_LIBS}
END
# Avoid confusion on DNS/DHCP and only mention MySQL if it
# were specified on the command line.
if test "$MYSQL_CPPFLAGS" != "" ; then
cat >> config.report << END
- MySQL: $MYSQL_CPPFLAGS
- $MYSQL_LIBS
+
+MySQL:
+ MYSQL_VERSION: ${MYSQL_VERSION}
+ MYSQL_CPPFLAGS: ${MYSQL_CPPFLAGS}
+ MYSQL_LIBS: ${MYSQL_LIBS}
+END
+fi
+
+if test "$enable_gtest" != "no"; then
+cat >> config.report << END
+
+GTest:
+ GTEST_VERSION: ${GTEST_VERSION}
+ GTEST_INCLUDES: ${GTEST_INCLUDES}
+ GTEST_LDFLAGS: ${GTEST_LDFLAGS}
+ GTEST_LDADD: ${GTEST_LDADD}
+ GTEST_SOURCE: ${GTEST_SOURCE}
END
fi
@@ -1500,6 +1733,12 @@ END
cat config.report
cat <<EOF
- Now you can type "make" to build BIND 10
+ Now you can type "make" to build BIND 10. Note that if you intend to
+ run "make check", you must run "make" first as some files need to be
+ generated by "make" before "make check" can be run.
+
+ When running "make install" do not use any form of parallel or job
+ server options (such as GNU make's -j option). Doing so may cause
+ errors.
EOF
diff --git a/doc/Doxyfile b/doc/Doxyfile
index 5c071c1..c2aec85 100644
--- a/doc/Doxyfile
+++ b/doc/Doxyfile
@@ -661,34 +661,38 @@ WARN_LOGFILE =
# directories like "/usr/src/myproject". Separate the files or directories
# with spaces.
-INPUT = ../src/lib/exceptions \
+INPUT = ../src/bin/auth \
+ ../src/bin/d2 \
+ ../src/bin/dhcp4 \
+ ../src/bin/dhcp6 \
+ ../src/bin/resolver \
+ ../src/bin/sockcreator \
+ ../src/hooks/dhcp/user_chk \
+ ../src/lib/acl \
+ ../src/lib/asiolink \
+ ../src/lib/bench \
+ ../src/lib/cache \
../src/lib/cc \
../src/lib/config \
../src/lib/cryptolink \
- ../src/lib/dns \
../src/lib/datasrc \
../src/lib/datasrc/memory \
- ../src/bin/auth \
- ../src/bin/resolver \
- ../src/lib/bench \
+ ../src/lib/dhcp \
+ ../src/lib/dhcp_ddns \
+ ../src/lib/dhcpsrv \
+ ../src/lib/dns \
+ ../src/lib/exceptions \
+ ../src/lib/hooks \
../src/lib/log \
../src/lib/log/compiler \
- ../src/lib/asiolink/ \
../src/lib/nsas \
- ../src/lib/testutils \
- ../src/lib/cache \
- ../src/lib/server_common/ \
- ../src/bin/sockcreator/ \
- ../src/lib/util/ \
- ../src/lib/util/io/ \
- ../src/lib/util/threads/ \
../src/lib/resolve \
- ../src/lib/acl \
+ ../src/lib/server_common \
../src/lib/statistics \
- ../src/bin/dhcp6 \
- ../src/lib/dhcp \
- ../src/lib/dhcpsrv \
- ../src/bin/dhcp4 \
+ ../src/lib/testutils \
+ ../src/lib/util \
+ ../src/lib/util/io \
+ ../src/lib/util/threads \
../tests/tools/perfdhcp \
devel
@@ -775,7 +779,7 @@ EXAMPLE_RECURSIVE = NO
# directories that contain image that are included in the documentation (see
# the \image command).
-IMAGE_PATH = ../doc/images
+IMAGE_PATH = ../doc/images ../src/lib/hooks/images
# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program
diff --git a/doc/Doxyfile-xml b/doc/Doxyfile-xml
new file mode 100644
index 0000000..ae5be8a
--- /dev/null
+++ b/doc/Doxyfile-xml
@@ -0,0 +1,7 @@
+# This is a doxygen configuration for generating XML output as well as HTML.
+#
+# Inherit everything from our default Doxyfile except GENERATE_XML, which
+# will be reset to YES
+
+ at INCLUDE = Doxyfile
+GENERATE_XML = YES
diff --git a/doc/Makefile.am b/doc/Makefile.am
index 3120280..4af8188 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -1,6 +1,6 @@
-SUBDIRS = guide
+SUBDIRS = guide design
-EXTRA_DIST = version.ent.in differences.txt
+EXTRA_DIST = version.ent.in differences.txt Doxyfile Doxyfile-xml
devel:
mkdir -p html
diff --git a/doc/design/Makefile.am b/doc/design/Makefile.am
new file mode 100644
index 0000000..e0c888e
--- /dev/null
+++ b/doc/design/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = datasrc
diff --git a/doc/design/datasrc/.gitignore b/doc/design/datasrc/.gitignore
new file mode 100644
index 0000000..065b83e
--- /dev/null
+++ b/doc/design/datasrc/.gitignore
@@ -0,0 +1,3 @@
+/*.html
+/*.png
+/*.xml
diff --git a/doc/design/datasrc/Makefile.am b/doc/design/datasrc/Makefile.am
new file mode 100644
index 0000000..d339cd7
--- /dev/null
+++ b/doc/design/datasrc/Makefile.am
@@ -0,0 +1,30 @@
+UML_FILES = \
+ overview.txt \
+ auth-local.txt \
+ auth-mapped.txt \
+ memmgr-mapped-init.txt \
+ memmgr-mapped-reload.txt
+
+TEXT_FILES = \
+ data-source-classes.txt
+
+devel: $(patsubst %.txt, %.png, $(UML_FILES)) $(patsubst %.txt, %.html, $(TEXT_FILES))
+
+.txt.html:
+if HAVE_ASCIIDOC
+ $(AM_V_GEN) $(ASCIIDOC) -n $<
+else
+ @echo "*** asciidoc is required to regenerate $(@) ***"; exit 1;
+endif
+
+.txt.png:
+if HAVE_PLANTUML
+ $(AM_V_GEN) $(PLANTUML) $<
+else
+ @echo "*** plantuml is required to regenerate $(@) ***"; exit 1;
+endif
+
+CLEANFILES = \
+ $(patsubst %.txt, %.png, $(UML_FILES)) \
+ $(patsubst %.txt, %.html, $(TEXT_FILES)) \
+ $(patsubst %.txt, %.xml, $(TEXT_FILES))
diff --git a/doc/design/datasrc/auth-local.txt b/doc/design/datasrc/auth-local.txt
new file mode 100644
index 0000000..afe8ad8
--- /dev/null
+++ b/doc/design/datasrc/auth-local.txt
@@ -0,0 +1,142 @@
+ at startuml
+
+participant auth as ":b10-auth"
+[-> auth: new/initial config\n(datasrc cfg)
+activate auth
+
+participant list as ":Configurable\nClientList"
+create list
+auth -> list: <<construct>>
+
+auth -> list: configure(cfg)
+activate list
+
+participant cache_config as ":CacheConfig"
+create cache_config
+list -> cache_config: <<construct>> (cfg)
+
+participant zt_segment as ":ZoneTable\nSegment\n(Local)"
+create zt_segment
+list -> zt_segment: <<construct>>
+activate zt_segment
+
+participant zone_table as ":ZoneTable"
+create zone_table
+zt_segment -> zone_table: <<construct>>
+
+deactivate zt_segment
+
+list -> zt_segment: isWritable()
+activate zt_segment
+note over zt_segment: Local segments are\nalways writable
+zt_segment --> list: true
+deactivate zt_segment
+
+loop for each zone in cache_config
+list -> cache_config: getLoadAction()
+activate cache_config
+
+participant la1 as "la1:LoadAction"
+create la1
+cache_config -> la1: <<construct>>
+
+participant la2 as "la2:LoadAction"
+
+cache_config --> list : la1
+
+deactivate cache_config
+
+participant w1 as "w1:ZoneWriter"
+create w1
+list -> w1: <<construct>> (la1)
+
+participant w2 as "w2:ZoneWriter"
+
+list -> w1: load()
+activate w1
+w1 -> la1: (funcall)
+activate la1
+
+participant zd1 as "zd1:ZoneData"
+create zd1
+la1 -> zd1: <<construct>> via helpers
+
+participant zd2 as "zd2:ZoneData"
+
+la1 --> w1: zd1
+deactivate la1
+deactivate w1
+
+list -> w1: install()
+activate w1
+
+w1 -> zone_table: addZone(zd1)
+activate zone_table
+zone_table --> w1: NULL (no old data)
+deactivate zone_table
+
+deactivate w1
+
+end
+
+deactivate list
+deactivate auth
+
+...
+
+[-> auth: reload\n(zonename)
+activate auth
+
+auth -> list: getCachedw1\n(zone_name)
+activate list
+
+list -> cache_config: getLoadAction()
+activate cache_config
+
+create la2
+cache_config -> la2: <<construct>>
+
+cache_config --> list : la2
+
+deactivate cache_config
+
+create w2
+list -> w2: <<construct>> (la2)
+
+list --> auth: w2
+
+deactivate list
+
+
+auth -> w2: load()
+activate w2
+w2 -> la2: (funcall)
+activate la2
+
+create zd2
+la2 -> zd2: <<construct>> via helpers
+
+la2 --> w2: zd2
+deactivate la2
+deactivate w2
+
+auth -> w2: install()
+activate w2
+
+w2 -> zone_table: addZone(zd2)
+activate zone_table
+zone_table --> w2: zd1 (old data)
+deactivate zone_table
+
+deactivate w2
+
+auth -> w2: cleanup()
+activate w2
+
+w2 -> zd1: <<destroy>>
+destroy zd1
+deactivate w2
+
+deactivate auth
+
+ at enduml
diff --git a/doc/design/datasrc/auth-mapped.txt b/doc/design/datasrc/auth-mapped.txt
new file mode 100644
index 0000000..b5b1a39
--- /dev/null
+++ b/doc/design/datasrc/auth-mapped.txt
@@ -0,0 +1,99 @@
+ at startuml
+
+participant auth as ":b10-auth"
+[-> auth: new/initial config\n(datasrc cfg)
+activate auth
+
+participant list as ":Configurable\nClientList"
+create list
+auth -> list: <<construct>>
+
+auth -> list: configure(cfg)
+activate list
+
+participant CacheConfig as ":CacheConfig"
+create CacheConfig
+list -> CacheConfig: <<construct>> (cfg)
+
+participant zt_segment as ":ZoneTable\nSegment\n(Mapped)"
+create zt_segment
+list -> zt_segment: <<construct>>
+
+list -> zt_segment: isWritable()
+activate zt_segment
+note over zt_segment: Segment not writable\nwhen not reset
+zt_segment --> list: false
+deactivate zt_segment
+
+deactivate list
+
+auth -> list: getStatus()
+activate list
+list --> auth: DataSourceStatus[]
+deactivate list
+
+[<- auth: subscribe to\nmemmgr group
+
+deactivate auth
+
+...
+
+[-> auth: command from\nmemmgr\n(datasrc_name,\nsegmentparam)
+activate auth
+
+auth -> list: resetMemorySegment\n(datasrc_name,\nREAD_ONLY,\nsegmentparam)
+activate list
+
+list -> zt_segment: reset\n(READ_ONLY,\nsegmentparam)
+activate zt_segment
+
+participant segment as "seg1:Memory\nSegment\n(Mapped)"
+create segment
+zt_segment -> segment: <<construct>>
+
+deactivate zt_segment
+deactivate list
+deactivate auth
+
+...
+
+[-> auth: command from\nmemmgr\n(datasrc_name,\nsegmentparam)
+activate auth
+
+auth -> list: resetMemorySegment\n(datasrc_name,\nREAD_ONLY,\nsegmentparam)
+activate list
+
+list -> zt_segment: reset\n(READ_ONLY,\nsegmentparam)
+activate zt_segment
+
+zt_segment -> segment: <<destroy>>
+destroy segment
+
+participant segment2 as "seg2:Memory\nSegment\n(Mapped)"
+create segment2
+zt_segment -> segment2: <<construct>>
+
+deactivate zt_segment
+deactivate list
+deactivate auth
+
+...
+
+[-> auth: reload\n(zonename)
+activate auth
+
+auth -> list: getCachedZoneWriter\n(zone_name)
+activate list
+
+list -> zt_segment: isWritable()
+activate zt_segment
+note over zt_segment: Segment not writable\nas it is READ_ONLY
+zt_segment --> list: false
+deactivate zt_segment
+
+list --> auth: CACHE_NOT_WRITABLE
+deactivate list
+
+deactivate auth
+
+ at enduml
diff --git a/doc/design/datasrc/data-source-classes.txt b/doc/design/datasrc/data-source-classes.txt
new file mode 100644
index 0000000..0f2dcbb
--- /dev/null
+++ b/doc/design/datasrc/data-source-classes.txt
@@ -0,0 +1,366 @@
+Data Source Library Classes
+===========================
+
+About this document
+-------------------
+
+This memo describes major classes used in the data source library,
+mainly focusing on handling in-memory cache with consideration of the
+shared memory support. It will give an overview of the entire design
+architecture and some specific details of how these classes are expected
+to be used.
+
+Before reading, the higher level inter-module protocol should be understood:
+http://bind10.isc.org/wiki/SharedMemoryIPC
+
+Overall relationships between classes
+-------------------------------------
+
+The following diagram shows major classes in the data source library
+related to in-memory caches and their relationship.
+
+image::overview.png[Class diagram showing overview of relationships]
+
+Major design decisions of this architecture are:
+
+* Keep each class as concise as possible, each focusing on one or
+ small set of responsibilities. Smaller classes are generally easier
+ to understand (at the cost of understanding how they work in the
+ "big picture" of course) and easier to test.
+
+* On a related point, minimize dependency to any single class. A
+ monolithic class on which many others are dependent is generally
+ difficult to maintain because you'll need to ensure a change to the
+ monolithic class doesn't break anything on any other classes.
+
+* Use polymorphism for any "fluid" behavior, and hide specific details
+ under abstract interfaces so implementation details won't be
+ directly referenced from any other part of the library.
+ Specifically, the underlying memory segment type (local, mapped, and
+ possibly others) and the source of in-memory data (master file or
+ other data source) are hidden via a kind of polymorphism.
+
+* Separate classes directly used by applications from classes that
+ implement details. Make the former classes as generic as possible,
+ agnostic about implementation specific details such as the memory
+ segment type (or, ideally and where possible, whether it's for
+ in-memory cache or the underlying data source).
+
+The following give a summarized description of these classes.
+
+* `ConfigurableClientList`: The front end to application classes. An
+ application that uses the data source library generally maintains
+ one or more `ConfigurableClientList` object (usually one per RR
+ class, or when we support views, probably one per view). This class
+ is a container of sets of data source related classes, providing
+ accessor to these classes and also acting as a factory of other
+ related class objects. Note: Due to internal implementation
+ reasons, there is a base class for `ConfigurableClientList` named
+ `ClientList` in the C++ version, and applications are expected to
+ use the latter. But conceptually `ConfigurableClientList` is an
+ independent value class; the inheritance is not for polymorphism.
+ Note also that the Python version doesn't have the base class.
+
+* `DataSourceInfo`: this is a straightforward tuple of set of class
+ objects corresponding to a single data source, including
+ `DataSourceClient`, `CacheConfig`, and `ZoneTableSegment`.
+ `ConfigurableClientList` maintains a list of `DataSourceInfo`, one
+ for each data source specified in its configuration.
+
+* `DataSourceClient`: The front end class to applications for a single
+ data source. Applications will get a specific `DataSourceClient`
+ object by `ConfigurableClientList::find()`.
+ `DataSourceClient` itself is a set of factories for various
+ operations on the data source such as lookup or update.
+
+* `CacheConfig`: library internal representation of in-memory cache
+ configuration for a data source. It knows which zones are to be
+ cached and where the zone data (RRs) should come from, either from a
+ master file or other data source. With this knowledge it will
+ create an appropriate `LoadAction` object. Note that `CacheConfig`
+ isn't aware of the underlying memory segment type for the in-memory
+ data. It's intentionally separated from this class (see the
+ conciseness and minimal-dependency design decisions above).
+
+* `ZoneTableSegment`: when in-memory cache is enabled, it provides
+ memory-segment-type independent interface to the in-memory data.
+ This is an abstract base class (see polymorphism in the design
+ decisions) and inherited by segment-type specific subclasses:
+ `ZoneTableSegmentLocal` and `ZoneTableSegmentMapped` (and possibly
+ others). Any subclass of `ZoneTableSegment` is expected to maintain
+ the specific type of `MemorySegment` object.
+
+* `ZoneWriter`: a frontend utility class for applications to update
+ in-memory zone data (currently it can only load a whole zone and
+ replace any existing zone content with a new one, but this should be
+ extended so it can handle partial updates).
+ Applications will get a specific `ZoneWriter`
+ object by `ConfigurableClientList::getCachedZoneWriter()`.
+ `ZoneWriter` is constructed with `ZoneableSegment` and `LoadAction`.
+ Since these are abstract classes, `ZoneWriter` doesn't have to be
+ aware of "fluid" details. It's only responsible for "somehow" preparing
+ `ZoneData` for a new version of a specified zone using `LoadAction`,
+ and installing it in the `ZoneTable` (which can be accessed via
+ `ZoneTableSegment`).
+
+* `DataSourceStatus`: created by `ConfigurableClientList::getStatus()`,
+ a straightforward tuple that represents some status information of a
+ specific data source managed in the `ConfigurableClientList`.
+ `getStatus()` generates `DataSourceStatus` for all data sources
+ managed in it, and returns them as a vector.
+
+* `ZoneTableAccessor`, `ZoneTableIterator`: frontend classes to get
+ access to the conceptual "zone table" (a set of zones) stored in a
+ specific data source. In particular, `ZoneTableIterator` allows
+ applications to iterate over all zones (by name) stored in the
+ specific data source.
+ Applications will get a specific `ZoneTableAccessor`
+ object by `ConfigurableClientList::getZoneTableAccessor()`,
+ and get an iterator object by calling `getIterator` on the accessor.
+ These are abstract classes and provide unified interfaces
+ independent from whether it's for in-memory cached zones or "real"
+ underlying data source. But the initial implementation only
+ provides the in-memory cache version of subclass (see the next
+ item).
+
+* `ZoneTableAccessorCache`, `ZoneTableIteratorCache`: implementation
+ classes of `ZoneTableAccessor` and `ZoneTableIterator` for in-memory
+ cache. They refer to `CacheConfig` to get a list of zones to be
+ cached.
+
+* `ZoneTableHeader`, `ZoneTable`: top-level interface to actual
+ in-memory data. These were separated based on a prior version of
+ the design (http://bind10.isc.org/wiki/ScalableZoneLoadDesign) where
+ `ZoneTableHeader` may contain multiple `ZoneTable`s. It's
+ one-to-one relationship in the latest version (of implementation),
+ so we could probably unify them as a cleanup.
+
+* `ZoneData`: representing the in-memory content of a single zone.
+ `ZoneTable` contains (zero, one or) multiple `ZoneData` objects.
+
+* `RdataSet`: representing the in-memory content of (data of) a single
+ RRset.
+ `ZoneData` contains `RdataSet`s corresponding to the RRsets stored
+ in the zone.
+
+* `LoadAction`: a "polymorphic" functor that implements loading zone
+ data into memory. It hides from its user (i.e., `ZoneWriter`)
+ details about the source of the data: master file or other data
+ source (and perhaps some others). The "polymorphism" is actually
+ realized as different implementations of the functor interface, not
+ class inheritance (but conceptually the effect and goal is the
+ same). Note: there's a proposal to replace `LoadAction` with
+ a revised `ZoneDataLoader`, although the overall concept doesn't
+ change. See Trac ticket #2912.
+
+* `ZoneDataLoader` and `ZoneDataUpdater`: helper classes for the
+ `LoadAction` functor(s). These work independently from the source
+ of data, taking a sequence of RRsets objects, converting them
+ into the in-memory data structures (`RdataSet`), and installing them
+ into a newly created `ZoneData` object.
+
+Sequence for auth module using local memory segment
+---------------------------------------------------
+
+In the remaining sections, we explain how the classes shown in the
+previous section work together through their methods for commonly
+intended operations.
+
+The following sequence diagram shows the case for the authoritative
+DNS server module to maintain "local" in-memory data. Note that
+"auth" is a conceptual "class" (not actually implemented as a C++
+class) to represent the server application behavior. For the purpose
+of this document that should be sufficient. The same note applies to
+all examples below.
+
+image::auth-local.png[Sequence diagram for auth server using local memory segment]
+
+1. On startup, the auth module creates a `ConfigurableClientList`
+ for each RR class specified in the configuration for "data_sources"
+ module. It then calls `ConfigurableClientList::configure()`
+ for the given configuration of that RR class.
+
+2. For each data source, `ConfigurableClientList` creates a
+ `CacheConfig` object with the corresponding cache related
+ configuration.
+
+3. If in-memory cache is enabled for the data source,
+ `ZoneTableSegment` is also created. In this scenario the cache
+ type is specified as "local" in the configuration, so a functor
+ creates `ZoneTableSegmentLocal` as the actual instance.
+ In this case its `ZoneTable` is immediately created, too.
+
+4. `ConfigurableClientList` checks if the created `ZoneTableSegment` is
+ writable. It is always so for "local" type of segments. So
+ `ConfigurableClientList` immediately loads zones to be cached into
+ memory. For each such zone, it first gets the appropriate
+ `LoadAction` through `CacheConfig`, then creates `ZoneWriter` with
+ the `LoadAction`, and loads the data using the writer.
+
+5. If the auth module receives a "reload" command for a cached zone
+ from other module (xfrin, an end user, etc), it calls
+ `ConfigurableClientList::getCachedZoneWriter` to load and install
+ the new version of the zone. The same loading sequence takes place
+ except that the user of the writer is the auth module.
+ Also, the old version of the zone data is destroyed at the end of
+ the process.
+
+Sequence for auth module using mapped memory segment
+----------------------------------------------------
+
+This is an example for the authoritative server module that uses
+mapped type memory segment for in-memory data.
+
+image::auth-mapped.png[Sequence diagram for auth server using mapped memory segment]
+
+1. The sequence is the same to the point of creating `CacheConfig`.
+
+2. But in this case a `ZoneTableSegmentMapped` object is created based
+ on the configuration of the cache type. This type of
+ `ZoneTableSegment` is initially empty and isn't even associated
+ with a `MemorySegment` (and therefore considered non-writable).
+
+3. `ConfigurableClientList` checks if the zone table segment is
+ writable to know whether to load zones into memory by itself,
+ but as `ZoneTableSegment::isWritable()` returns false, it skips
+ the loading.
+
+4. The auth module gets the status of each data source, and notices
+ there's a `WAITING` state of segment. So it subscribes to the
+ "Memmgr" group on a command session and waits for an update
+ from the memory manager (memmgr) module. (See also the note at the
+ end of the section)
+
+5. When the auth module receives an update command from memmgr, it
+ calls `ConfigurableClientList::resetMemorySegment()` with the command
+ argument and the segment mode of `READ_ONLY`.
+ Note that the auth module handles the command argument as mostly
+ opaque data; it's not expected to deal with details of segment
+ type-specific behavior. If the reset fails, auth aborts (as there's
+ no clear way to handle the failure).
+
+6. `ConfigurableClientList::resetMemorySegment()` subsequently calls
+ `reset()` method on the corresponding `ZoneTableSegment` with the
+ given parameters.
+ In the case of `ZoneTableSegmentMapped`, it creates a new
+ `MemorySegment` object for the mapped type, which internally maps
+ the specific file into memory.
+ memmgr is expected to have prepared all necessary data in the file,
+ so all the data are immediately ready for use (i.e., there
+ shouldn't be any explicit load operation).
+
+7. When a change is made in the mapped data, memmgr will send another
+ update command with parameters for new mapping. The auth module
+ calls `ConfigurableClientList::resetMemorySegment()`, and the
+ underlying memory segment is swapped with a new one. The old
+ memory segment object is destroyed. Note that
+ this "destroy" just means unmapping the memory region; the data
+ stored in the file are intact. Again, if mapping fails, auth
+ aborts.
+
+8. If the auth module happens to receive a reload command from other
+ module, it could call
+ `ConfigurableClientList::getCachedZoneWriter()`
+ to reload the data by itself, just like in the previous section.
+ In this case, however, the writability check of
+ `getCachedZoneWriter()` fails (the segment was created as
+ `READ_ONLY` and is non-writable), so loading won't happen.
+
+NOTE: While less likely in practice, it's possible that the same auth
+module uses both "local" and "mapped" (and even others) type of
+segments for different data sources. In such cases the sequence is
+either the one in this or previous section depending on the specified
+segment type in the configuration. The auth module itself isn't aware
+of per segment-type details, but changes the behavior depending on the
+segment state of each data source at step 4 above: if it's `WAITING`,
+it means the auth module needs help from memmgr (that's all the auth
+module should know; it shouldn't be bothered with further details such
+as mapped file names); if it's something else, the auth module doesn't
+have to do anything further.
+
+Sequence for memmgr module initialization using mapped memory segment
+---------------------------------------------------------------------
+
+This sequence shows the common initialization sequence for the
+memory manager (memmgr) module using a mapped type memory segment.
+This is a mixture of the sequences shown in Sections 2 and 3.
+
+image::memmgr-mapped-init.png[]
+
+1. Initial sequence is the same until the application module (memmgr)
+ calls `ConfigurableClientList::getStatus()` as that for the
+ previous section.
+
+2. The memmgr module identifies the data sources whose in-memory cache
+ type is "mapped". (Unlike other application modules, the memmgr
+ should know what such types means due to its exact responsibility).
+ For each such data source, it calls
+ `ConfigurableClientList::resetMemorySegment` with the READ_WRITE
+ mode and other mapped-type specific parameters. memmgr should be
+ able to generate the parameters from its own configuration and
+ other data source specific information (such as the RR class and
+ data source name).
+
+3. The `ConfigurableClientList` class calls
+ `ZoneTableSegment::reset()` on the corresponding zone table
+ segment with the given parameters. In this case, since the mode is
+ READ_WRITE, a new `ZoneTable` will be created (assuming this is a
+ very first time initialization; if there's already a zone table
+ in the segment, it will be used).
+
+4. The memmgr module then calls
+ `ConfigurableClientList::getZoneTableAccessor()`, and calls the
+ `getItertor()` method on it to get a list of zones for which
+ zone data are to be loaded into the memory segment.
+
+5. The memmgr module loads the zone data for each such zone. This
+ sequence is the same as shown in Section 2.
+
+6. On loading all zone data, the memmgr module sends an update command
+ to all interested modules (such as auth) in the segment, and waits
+ for acknowledgment from all of them.
+
+7. Then it calls `ConfigurableClientList::resetMemorySegment()` for
+ this data source with almost the same parameter as step 2 above,
+ but with a different mapped file name. This will make a swap of
+ the underlying memory segment with a new mapping. The old
+ `MemorySegment` object will be destroyed, but as explained in the
+ previous section, it simply means unmapping the file.
+
+8. The memmgr loads the zone data into the newly mapped memory region
+ by repeating the sequence shown in step 5.
+
+9. The memmgr repeats all this sequence for data sources that use
+ "mapped" segment for in-memory cache. Note: it could handle
+ multiple data sources in parallel, e.g., while waiting for
+ acknowledgment from other modules.
+
+Sequence for memmgr module to reload a zone using mapped memory segment
+-----------------------------------------------------------------------
+
+This example is a continuation of the previous section, describing how
+the memory manager reloads a zone in mapped memory segment.
+
+image::memmgr-mapped-reload.png[]
+
+1. When the memmgr module receives a reload command from other module,
+ it calls `ConfigurableClientList::getCachedZoneWriter()` for the
+ specified zone name. This method checks the writability of
+ the segment, and since it's writable (as memmgr created it in the
+ READ_WRITE mode), `getCachedZoneWriter()` succeeds and returns
+ a `ZoneWriter`.
+
+2. The memmgr module uses the writer to load the new version of zone
+ data. There is nothing specific to mapped-type segment here.
+
+3. The memmgr module then sends an update command to other modules
+ that would share this version, and waits for acknowledgment from
+ all of them.
+
+4. On getting acknowledgments, the memmgr module calls
+ `ConfigurableClientList::resetMemorySegment()` with the parameter
+ specifying the other mapped file. This will swap the underlying
+ `MemorySegment` with a newly created one, mapping the other file.
+
+5. The memmgr updates this segment, too, so the two files will contain
+ the same version of data.
diff --git a/doc/design/datasrc/memmgr-mapped-init.txt b/doc/design/datasrc/memmgr-mapped-init.txt
new file mode 100644
index 0000000..9daea13
--- /dev/null
+++ b/doc/design/datasrc/memmgr-mapped-init.txt
@@ -0,0 +1,137 @@
+ at startuml
+
+participant memmgr as ":b10-memmgr"
+[-> memmgr: new/initial config\n(datasrc cfg)
+activate memmgr
+
+participant list as ":Configurable\nClientList"
+create list
+memmgr -> list: <<construct>>
+
+memmgr -> list: configure(cfg)
+activate list
+
+participant CacheConfig as ":CacheConfig"
+create CacheConfig
+list -> CacheConfig: <<construct>> (cfg)
+
+participant zt_segment as ":ZoneTable\nSegment\n(Mapped)"
+create zt_segment
+list -> zt_segment: <<construct>>
+
+list -> zt_segment: isWritable()
+activate zt_segment
+note over zt_segment: Segment not writable\nwhen not reset
+zt_segment --> list: false
+deactivate zt_segment
+
+deactivate list
+
+memmgr -> list: getStatus()
+activate list
+list --> memmgr: DataSourceStatus[]
+deactivate list
+
+loop for each datasrc with mapped segment
+
+memmgr -> list: resetMemorySegment\n(datasrc_name,\nREAD_WRITE,\nsegmentparam)
+activate list
+
+list -> zt_segment: reset\n(READ_WRITE,\nsegmentparam)
+activate zt_segment
+
+participant segment as "seg1:Memory\nSegment\n(Mapped)"
+create segment
+zt_segment -> segment: <<construct>>
+
+participant segment.2 as "seg2:Memory\nSegment\n(Mapped)"
+
+participant ZoneTable as ":ZoneTable"
+create ZoneTable
+zt_segment -> ZoneTable: <<construct>>
+
+deactivate zt_segment
+deactivate list
+
+memmgr -> list: getZoneTableAccessor\n(datasrc_name,\ncache=true)
+activate list
+list -> memmgr: ZoneTableAccessor
+deactivate list
+
+
+loop for each zone given by ZoneTableIterator
+
+memmgr -> list: getCachedZoneWriter\n(zone_name)
+activate list
+
+list -> CacheConfig: getLoadAction()
+activate CacheConfig
+
+participant LoadAction as "la:LoadAction"
+create LoadAction
+CacheConfig -> LoadAction: <<construct>>
+
+CacheConfig --> list : la
+
+deactivate CacheConfig
+
+participant ZoneWriter as "zw:ZoneWriter"
+create ZoneWriter
+list -> ZoneWriter: <<construct>> (la)
+
+list --> memmgr: zw
+
+deactivate list
+
+
+memmgr -> ZoneWriter: load()
+activate ZoneWriter
+ZoneWriter -> LoadAction: (funcall)
+activate LoadAction
+
+participant ZoneData as "zd:ZoneData"
+create ZoneData
+LoadAction -> ZoneData: <<construct>> via helpers
+
+LoadAction --> ZoneWriter: zd
+deactivate LoadAction
+deactivate ZoneWriter
+
+memmgr -> ZoneWriter: install()
+activate ZoneWriter
+
+ZoneWriter -> ZoneTable: addZone(zd)
+activate ZoneTable
+ZoneTable --> ZoneWriter: NULL (no old data)
+deactivate ZoneTable
+
+deactivate ZoneWriter
+
+end
+
+[<- memmgr: command to\nmodules\n(datasrc_name,\nsegmentparam)
+[--> memmgr: ack from all\nmodules
+
+memmgr -> list: resetMemorySegment\n(datasrc_name,\nREAD_WRITE,\nsegmentparam)
+activate list
+
+list -> zt_segment: reset\n(READ_WRITE,\nsegmentparam)
+activate zt_segment
+
+zt_segment -> segment: <<destroy>>
+destroy segment
+create segment.2
+zt_segment -> segment.2: <<construct>>
+
+deactivate zt_segment
+deactivate list
+
+note left of memmgr: load zone\nfor each zone\ngiven by\nZoneTableIterator
+
+end
+
+[<-- memmgr
+
+deactivate memmgr
+
+ at enduml
diff --git a/doc/design/datasrc/memmgr-mapped-reload.txt b/doc/design/datasrc/memmgr-mapped-reload.txt
new file mode 100644
index 0000000..676e961
--- /dev/null
+++ b/doc/design/datasrc/memmgr-mapped-reload.txt
@@ -0,0 +1,94 @@
+ at startuml
+
+participant memmgr as ":b10-memmgr"
+[-> memmgr: reload\n(zonename)
+activate memmgr
+
+participant list as ":Configurable\nClientList"
+memmgr -> list: getCachedZoneWriter\n(zone_name)
+activate list
+
+participant CacheConfig as ":CacheConfig"
+
+participant zt_segment as ":ZoneTable\nSegment\n(Mapped)"
+participant segment as "existing:Memory\nSegment\n(Mapped)"
+participant segment2 as "new:Memory\nSegment\n(Mapped)"
+
+list -> zt_segment: isWritable()
+activate zt_segment
+zt_segment --> list: true
+deactivate zt_segment
+
+list -> CacheConfig: getLoadAction()
+activate CacheConfig
+
+participant ZoneTable as ":ZoneTable"
+participant ZoneWriter as "zw:ZoneWriter"
+
+participant LoadAction as "la:LoadAction"
+create LoadAction
+CacheConfig -> LoadAction: <<construct>>
+CacheConfig --> list: la
+deactivate CacheConfig
+
+create ZoneWriter
+list -> ZoneWriter: <<construct>> (la)
+list --> memmgr: zw
+deactivate list
+
+memmgr -> ZoneWriter: load()
+activate ZoneWriter
+ZoneWriter -> LoadAction: (funcall)
+activate LoadAction
+
+participant ZoneData as "zd_existing\n:ZoneData"
+participant ZoneData2 as "zd_new\n:ZoneData"
+
+create ZoneData2
+LoadAction -> ZoneData2: <<construct>> via helpers
+
+LoadAction --> ZoneWriter: zd_new
+deactivate LoadAction
+deactivate ZoneWriter
+
+memmgr -> ZoneWriter: install()
+activate ZoneWriter
+
+ZoneWriter -> ZoneTable: addZone(zd_new)
+activate ZoneTable
+ZoneTable --> ZoneWriter: zd_existing (old data)
+deactivate ZoneTable
+
+deactivate ZoneWriter
+
+memmgr -> ZoneWriter: cleanup()
+activate ZoneWriter
+
+ZoneWriter -> ZoneData: <<destroy>>
+destroy ZoneData
+deactivate ZoneWriter
+
+[<- memmgr: command to\nmodules\n(datasrc_name,\nsegmentparam)
+[--> memmgr: ack from all\nmodules
+
+memmgr -> list: resetMemorySegment\n(datasrc_name,\nREAD_WRITE,\nsegmentparam)
+activate list
+
+list -> zt_segment: reset\n(READ_WRITE,\nsegmentparam)
+activate zt_segment
+
+zt_segment -> segment: <<destroy>>
+destroy segment
+create segment2
+zt_segment -> segment2: <<construct>>
+
+deactivate zt_segment
+deactivate list
+
+note left of memmgr: (repeat the\nsame sequence\nfor loading to the\nother segment)
+
+memmgr -> list: getCachedZoneWriter\n(zone_name)
+
+...
+
+ at enduml
diff --git a/doc/design/datasrc/overview.txt b/doc/design/datasrc/overview.txt
new file mode 100644
index 0000000..4ee971c
--- /dev/null
+++ b/doc/design/datasrc/overview.txt
@@ -0,0 +1,68 @@
+ at startuml
+
+hide members
+
+note "Automatic placement of classes\ndoesn't look good. This diagram\nhas to be improved." as n1
+
+Auth "1" *--> "*" ConfigurableClientList
+Auth --> DataSourceClient
+Auth --> ZoneWriter
+Auth --> ZoneTableAccessor
+Auth --> DataSourceStatus
+Auth --> ZoneTableIterator
+
+ConfigurableClientList "1" *--> "*" DataSourceInfo
+ConfigurableClientList ..> ZoneTableSegment : <<reset>>
+ConfigurableClientList ..> DataSourceStatus : <<create>>
+ConfigurableClientList ..> ZoneWriter : <<create>>
+ConfigurableClientList ..> ZoneTableAccessor : <<create>>
+
+DataSourceInfo "1" *--> "*" DataSourceClient
+DataSourceInfo "1" *--> "*" CacheConfig
+DataSourceInfo "1" *--> "*" ZoneTableSegment
+
+ZoneTableAccessor ..> ZoneTableIterator : <<create>>
+
+ZoneTableAccessorCache --> CacheConfig
+ZoneTableAccessorCache ..> ZoneTableIteratorCache : <<create>>
+ZoneTableAccessorCache --o ZoneTableAccessor
+
+ZoneTableIteratorCache --o ZoneTableIterator
+ZoneTableIteratorCache --> CacheConfig
+
+ZoneWriter --> ZoneTableSegment
+ZoneWriter ..> ZoneData : add/replace
+
+ZoneTableSegment "1" *--> "1" ZoneTableHeader
+ZoneTableSegment "1" *--> "1" MemorySegment
+
+CacheConfig ..> LoadAction
+
+LoadAction ..> ZoneData : create
+LoadAction *--> ZoneDataLoader
+
+ZoneDataLoader --> ZoneData
+ZoneDataLoader *--> ZoneDataUpdater
+ZoneDataLoader --> MemorySegment
+
+ZoneDataUpdater --> ZoneData
+ZoneDataUpdater ..> RdataSet : create
+ZoneDataUpdater ..> RdataSet : add
+
+ZoneTableHeader "1" *--> "1" ZoneTable
+ZoneTable "1" *--> "1" ZoneData
+ZoneData "1" *--> "1" RdataSet
+
+LoadFromFile --o LoadAction
+IteratorLoader --o LoadAction
+
+MemorySegmentMapped --o MemorySegment
+MemorySegmentLocal --o MemorySegment
+
+ZoneTableSegmentMapped --o ZoneTableSegment
+ZoneTableSegmentLocal --o ZoneTableSegment
+
+ZoneTableSegmentMapped *--> MemorySegmentMapped
+ZoneTableSegmentLocal *--> MemorySegmentLocal
+
+ at enduml
diff --git a/doc/design/ipc-high.txt b/doc/design/ipc-high.txt
index 3f46b5c..5addb88 100644
--- a/doc/design/ipc-high.txt
+++ b/doc/design/ipc-high.txt
@@ -210,7 +210,7 @@ about changes to zone data, they'd subscribe to the
`Notifications/ZoneUpdates` group. Then, other client (let's say
`XfrIn`, with session ID `s12345`) would send something like:
- s12345 -> Notifications/ZoneUpdates
+ s12345 -> notifications/ZoneUpdates
{"notification": ["zone-update", {
"class": "IN",
"origin": "example.org.",
@@ -221,7 +221,7 @@ Both receivers would receive the message and know that the
`example.org` zone is now at version 123456. Note that multiple users
may produce the same kind of notification. Also, single group may be
used to send multiple notification names (but they should be related;
-in our example, the `Notifications/ZoneUpdates` could be used for
+in our example, the `notifications/ZoneUpdates` could be used for
`zone-update`, `zone-available` and `zone-unavailable` notifications
for change in zone data, configuration of new zone in the system and
removal of a zone from configuration).
diff --git a/doc/design/resolver/01-scaling-across-cores b/doc/design/resolver/01-scaling-across-cores
new file mode 100644
index 0000000..dbd962f
--- /dev/null
+++ b/doc/design/resolver/01-scaling-across-cores
@@ -0,0 +1,347 @@
+Scaling across (many) cores
+===========================
+
+Problem statement
+-----------------
+
+The general issue is how to insure that the resolver scales.
+
+Currently resolvers are CPU bound, and it seems likely that both
+instructions-per-cycle and CPU frequency will not increase radically,
+scaling will need to be across multiple cores.
+
+How can we best scale a recursive resolver across multiple cores?
+
+Image of how resolution looks like
+----------------------------------
+
+ Receive the query. @# <------------------------\
+ | |
+ | |
+ v |
+ Parse it, etc. $ |
+ | |
+ | |
+ v |
+ Look into the cache. $# |
+ Cry <---- No <---------- Is it there? -----------> Yes ---------\ |
+ | ^ | |
+ Prepare upstream query $ | | |
+ | | | |
+ v | | |
+ Send an upstream query (#) | | |
+ | | | |
+ | | | |
+ v | | |
+ Wait for answer @(#) | | |
+ | | | |
+ v | | |
+ Parse $ | | |
+ | | | |
+ v | | |
+ Is it enough? $ ----> No ---------/ | |
+ | | |
+ Yes | |
+ | | |
+ \-----------------------> Build answer $ <----------------------/ |
+ | |
+ | |
+ v |
+ Send answer # -----------------------------/
+
+This is simplified version, however. There may be other tasks (validation, for
+example), which are not drawn mostly for simplicity, as they don't produce more
+problems. The validation would be done as part of some computational task and
+they could do more lookups in the cache or upstream queries.
+
+Also, multiple queries may generate the same upstream query, so they should be
+aggregated together somehow.
+
+Legend
+~~~~~~
+ * $ - CPU intensive
+ * @ - Waiting for external event
+ * # - Possible interaction with other tasks
+
+Goals
+-----
+ * Run the CPU intensive tasks in multiple threads to allow concurrency.
+ * Minimise waiting for locks.
+ * Don't require too much memory.
+ * Minimise the number of upstream queries (both because they are slow and
+ expensive and also because we don't want to eat too much bandwidth and spam
+ the authoritative servers).
+ * Design simple enough so it can be implemented.
+
+Naïve version
+-------------
+
+Let's look at possible approaches and list their pros and cons. Many of the
+simple versions would not really work, but let's have a look at them anyway,
+because thinking about them might bring some solutions for the real versions.
+
+We take one query, handle it fully, with blocking waits for the answers. After
+this is done, we take another. The cache is private for each one process.
+
+Advantages:
+
+ * Very simple.
+ * No locks.
+
+Disadvantages:
+
+ * To scale across cores, we need to run *a lot* of processes, since they'd be
+ waiting for something most of their time. That means a lot of memory eaten,
+ because each one has its own cache. Also, running so many processes may be
+ problematic, processes are not very cheap.
+ * Many things would be asked multiple times, because the caches are not
+ shared.
+
+Threads
+~~~~~~~
+
+Some of the problems could be solved by using threads, but they'd not improve
+it much, since threads are not really cheap either (starting several hundred
+threads might not be a good idea either).
+
+Also, threads bring other problems. When we still assume separate caches (for
+caches, see below), we need to ensure safe access to logging, configuration,
+network, etc. These could be a bottleneck (eg. if we lock every time we read a
+packet from network, when there are many threads, they'll just fight over the
+lock).
+
+Supercache
+~~~~~~~~~~
+
+The problem with cache could be solved by placing a ``supercache'' between the
+resolvers and the Internet. That one would do almost no processing, it would
+just take the query, looked up in the cache and either answered from the cache
+or forwarded the query to the external world. It would store the answer and
+forward it back.
+
+The cache, if single-threaded, could be a bottle-neck. To solve it, there could
+be several approaches:
+
+Layered cache::
+ Each process has it's own small cache, which catches many queries. Then, a
+ group of processes shares another level of bigger cache, which catches most
+ of the queries that get past the private caches. We further group them and
+ each level handles less queries from each process, so they can keep up.
+ However, with each level, we add some overhead to do another lookup.
+Segmented cache::
+ We have several caches of the same level, in parallel. When we would ask a
+ cache, we hash the query and decide which cache to ask by the hash. Only that
+ cache would have that answer if any and each could run in a separate process.
+ The only problem is, could there be a pattern of queries that would skew to
+ use only one cache while the rest would be idle?
+Shared cache access::
+ A cache would be accessed by multiple processes/threads. See below for
+ details, but there's a risk of lock contention on the cache (it depends on
+ the data structure).
+
+Upstream queries
+~~~~~~~~~~~~~~~~
+
+Before doing an upstream query, we look into the cache to ensure we don't have
+the information yet. When we get the answer, we want to update the cache.
+
+This suggests the upstream queries are tightly coupled with the cache. Now,
+when we have several cache processes/threads, each can have some set of opened
+sockets which are not shared with other caches to do the lookups. This way we
+can avoid locking the upstream network communication.
+
+Also, we can have three conceptual states for data in cache, and act
+differently when it is requested.
+
+Present::
+ If it is available, in positive or negative version, we just provide the
+ answer right away.
+Not present::
+ The continuation of processing is queued somehow (blocked/callback is
+ stored/whatever). An upstream query is sent and we get to the next state.
+Waiting for answer::
+ If another query for the same thing arrives, we just queue it the same way
+ and keep waiting. When the answer comes, all the queued tasks are resumed.
+ If the TTL > 0, we store the answer and set it to ``present''.
+
+We want to do aggregation of upstream queries anyway, using cache for it saves
+some more processing and possibly locks.
+
+Multiple parallel queries
+-------------------------
+
+It seems obvious we can't afford to have a thread or process for each
+outstanding query. We need to handle multiple queries in each one at any given
+time.
+
+Coroutines
+~~~~~~~~~~
+
+The OS-level threads might be too expensive, but coroutines might be cheap
+enough. In that way, we could still write a code that would be easy to read,
+but limit the number of OS threads to reasonable number.
+
+In this model, when a query comes, a new coroutine/user-level thread is created
+for it. We use special reads and writes whenever there's an operation that
+could block. These reads and writes would internally schedule the operation
+and switch to another coroutine (if there's any ready to be executed).
+
+Each thread/process maintains its own set of coroutines and they do not
+migrate. This way, the queue of coroutines is kept lock-less, as well as any
+private caches. Only the shared caches are protected by a lock.
+
+[NOTE]
+The `coro` unit we have in the current code is *not* considered a coroutine
+library here. We would need a coroutine library where we have real stack for
+each coroutine and we switch the stacks on coroutine switch. That is possible
+with reasonable amount of dark magic (see `ucontext.h`, for example, but there
+are surely some higher-level libraries for that).
+
+There are some trouble with multiple coroutines waiting on the same event, like
+the same upstream query (possibly even coroutines from different threads), but
+it should be possible to solve.
+
+Event-based
+~~~~~~~~~~~
+
+We use events (`asio` and stuff) for writing it. Each outstanding query is an
+object with some callbacks on it. When we would do a possibly blocking
+operation, we schedule a callback to happen once the operation finishes.
+
+This is more lightweight than the coroutines (the query objects will be smaller
+than the stacks for coroutines), but it is harder to write and read for.
+
+[NOTE]
+Do not consider cross-breeding the models. That leads to space-time distortions
+and brain damage. Implementing one on top of other is OK, but mixing it in the
+same bit of code is a way do madhouse.
+
+Landlords and peasants
+~~~~~~~~~~~~~~~~~~~~~~
+
+In both the coroutines and event-based models, the cache and other shared
+things are easier to imagine as objects the working threads fight over to hold
+for a short while. In this model, it is easier to imagine each such shared
+object as something owned by a landlord that doesn't let anyone else on it,
+but you can send requests to him.
+
+A query is an object once again, with some kind of state machine.
+
+Then there are two kinds of threads. The peasants are just to do the heavy
+work. There's a global work-queue for peasants. Once a peasant is idle, it
+comes to the queue and picks up a handful of queries from there. It does as
+much on each the query as possible without requiring any shared resource.
+
+The other kind, the landlords, have a resource to watch over each. So we would
+have a cache (or several parts of cache), the sockets for accepting queries and
+answering them, possibly more. Each of these would have a separate landlord
+thread and a queue of tasks to do on the resource (look up something, send an
+answer...).
+
+Similarly, the landlord would take a handful of tasks from its queue and start
+handling them. It would possibly produce some more tasks for the peasants.
+
+The point here is, all the synchronisation is done on the queues, not on the
+shared resources themselves. And, we would append to a queues once the whole
+batch was completed. By tweaking the size of the batch, we could balance the
+lock contention, throughput and RTT. The append/remove would be a quick
+operation, and the cost of locks would amortize in the larger amount of queries
+handled per one lock operation.
+
+The possible downside is, a query needs to travel across several threads during
+its lifetime. It might turn out it is faster to move the query between cores
+than accessing the cache from several threads, since it is smaller, but it
+might be slower as well.
+
+It would be critical to make some kind of queue that is fast to append to and
+fast to take out first n items. Also, the tasks in the queues can be just
+abstract `boost::function<void (Worker&)>` functors, and each worker would just
+iterate through the queue, calling each functor. The parameter would be to
+allow easy generation of more tasks for other queues (they would be stored
+privately first, and appended to remote queues at the end of batch).
+
+Also, if we wanted to generate multiple parallel upstream queries from a single
+query, we would need to be careful. A query object would not have a lock on
+itself and the upstream queries could end up in a different caches/threads. To
+protect the original query, we would add another landlord that would aggregate
+answers together and let the query continue processing once it got enough
+answers. That way, the answers would be pushed all to the same threads and they
+could not fight over the query.
+
+[NOTE]
+This model would work only with threads, not processes.
+
+Shared caches
+-------------
+
+While it seems it is good to have some sort of L1 cache with pre-rendered
+answers (according to measurements in the #2777 ticket), we probably need some
+kind of larger shared cache.
+
+If we had just a single shared cache protected by lock, there'd be a lot of
+lock contention on the lock.
+
+Partitioning the cache
+~~~~~~~~~~~~~~~~~~~~~~
+
+We split the cache into parts, either by the layers or by parallel bits we
+switch between by a hash. If we take it to the extreme, a lock on each hash
+bucket would be this kind, though that might be wasting resources (how
+expensive is it to create a lock?).
+
+Landlords
+~~~~~~~~~
+
+The landlords do synchronizations themselves. Still, the cache would need to be
+partitioned.
+
+RCU
+~~~
+
+The RCU is a lock-less synchronization mechanism. An item is accessed through a
+pointer. An updater creates a copy of the structure (in our case, it would be
+content of single hash bucket) and then atomically replaces the pointer. The
+readers from before have the old version, the new ones get the new version.
+When all the old readers die out, the old copy is reclaimed. Also, the
+reclamation can AFAIK be postponed for later times when we are slightly more
+idle or to a different thread.
+
+We could use it for cache â in the fast track, we would just read the cache. In
+the slow one, we would have to wait in queue to do the update, in a single
+updater thread (because we don't really want to be updating the same cell twice
+at the same time).
+
+Proposals
+---------
+
+In either case, we would have some kind of L1 cache with pre-rendered answers.
+For these proposals (except the third), we wouldn't care if we split the cache
+into parallel chunks or layers.
+
+Hybrid RCU/Landlord
+~~~~~~~~~~~~~~~~~~~
+
+The landlord approach, just read only accesses to the cache are done directly
+by the peasants. Only if they don't find what they want, they'd append the
+queue to the task of the landlord. The landlord would be doing the RCU updates.
+It could happen that by the time the landlord gets to the task the answer is
+already there, but that would not matter much.
+
+Accessing network would be from landlords.
+
+Coroutines+RCU
+~~~~~~~~~~~~~~
+
+We would do the coroutines, and the reads from shared cache would go without
+locking. When doing write, we would have to lock.
+
+To avoid locking, each worker thread would have its own set of upstream sockets
+and we would dup the sockets from users so we don't have to lock that.
+
+Multiple processes with coroutines and RCU
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This would need the layered cache. The upper caches would be mapped to local
+memory for read-only access. Each cache would be a separate process. The
+process would do the updates â if the answer was not there, the process would
+be asked by some kind of IPC to pull it from upstream cache or network.
diff --git a/doc/design/resolver/02-mixed-recursive-authority-setup b/doc/design/resolver/02-mixed-recursive-authority-setup
new file mode 100644
index 0000000..a1cc5f6
--- /dev/null
+++ b/doc/design/resolver/02-mixed-recursive-authority-setup
@@ -0,0 +1,150 @@
+Mixed recursive & authoritative setup
+=====================================
+
+Ideally we will run the authoritative server independently of the
+recursive resolver.
+
+We need a way to run both an authoritative and a recursive resolver on
+the same machine and listening on the same IP/port. But we need a way to
+run only one of them as well.
+
+This is mostly the same problem as we have with DDNS packets and xfr-out
+requests, but they aren't that performance sensitive as auth & resolver.
+
+There are a number of possible approaches to this:
+
+One fat module
+--------------
+
+With some build system or dynamic linker tricks, we create three modules:
+
+ * Stand-alone auth
+ * Stand-alone resolver
+ * Compound module containing both
+
+The user then chooses either one stand-alone module, or the compound one,
+depending on the requirements.
+
+Advantages
+~~~~~~~~~~
+
+ * It is easier to switch between processing and ask authoritative questions
+ from within the resolver processing.
+
+Disadvantages
+~~~~~~~~~~~~~
+
+ * The code is not separated (one bugs takes down both, admin can't see which
+ one takes how much CPU).
+ * BIND 9 does this and its code is a jungle. Maybe it's not just a
+ coincidence.
+ * Limits flexibility -- for example, we can't then decide to make the resolver
+ threaded (or we would have to make sure the auth processing doesn't break
+ with threads, which will be hard).
+
+There's also the idea of putting the auth into a loadable library and the
+resolver could load and use it somehow. But the advantages and disadvantages
+are probably the same.
+
+Auth first
+----------
+
+We do the same as with xfrout and ddns. When a query comes, it is examined and
+if the `RD` bit is set, it is forwarded to the resolver.
+
+Advantages
+~~~~~~~~~~
+
+ * Separate auth and resolver modules
+ * Minimal changes to auth
+ * No slowdown on the auth side
+
+Disadvantages
+~~~~~~~~~~~~~
+
+ * Counter-intuitive asymmetric design
+ * Possible slowdown on the resolver side
+ * Resolver needs to know both modes (for running stand-alone too)
+
+There's also the possibility of the reverse -- resolver first. It may make
+more sense for performance (the more usual scenario would probably be a
+high-load resolver with just few low-volume authoritative zones). On the other
+hand, auth already has some forwarding tricks.
+
+Auth with cache
+---------------
+
+This is mostly the same as ``Auth first'', however, the cache is in the auth
+server. If it is in the cache, it is answered right away. If not, it is then
+forwarded to the resolver. The resolver then updates the cache too.
+
+Advantages
+~~~~~~~~~~
+
+ * Probably good performance
+
+Disadvantages
+~~~~~~~~~~~~~
+
+ * Cache duplication (several auth modules, it doesn't feel like it would work
+ with shared memory without locking).
+ * Cache is probably very different from authoritative zones, it would
+ complicate auth processing.
+ * The resolver needs own copy of cache (to be able to get partial results),
+ probably a different one than the auth server.
+
+Receptionist
+------------
+
+One module does only the listening. It doesn't process the queries itself, it
+only looks into them and forwards them to the processing modules.
+
+Advantages
+~~~~~~~~~~
+
+ * Clean design with separated modules
+ * Easy to run modules stand-alone
+ * Allows for solving the xfrout & ddns forwarding without auth running
+ * Allows for views (different auths with different configurations)
+ * Allows balancing/clustering across multiple machines
+ * Easy to create new modules for different kinds of DNS handling and share
+ port with them too
+
+Disadvantages
+~~~~~~~~~~~~~
+
+ * Need to set up another module (not a problem if we have inter-module
+ dependencies in b10-init)
+ * Possible performance impact. However, experiments show this is not an issue,
+ and the receptionist can actually increase the throughput with some tuning
+ and the increase in RTT is not big.
+
+Implementation ideas
+~~~~~~~~~~~~~~~~~~~~
+
+ * Let's have a new TCP transport, where we send not only the DNS messages,
+ but also the source and destination ports and addresses (two reasons --
+ ACLs in target module and not keeping state in the receptionist). It would
+ allow for transfer of a batch of messages at once, to save some calls to
+ kernel (like a length of block of messages, it is read at once, then they
+ are all parsed one by one, the whole block of answers is sent back).
+ * A module creates a listening socket (UNIX by default) on startup and
+ contacts all the receptionists. It sends what kind of packets to send
+ to the module and the address of the UNIX socket. All the receptionists
+ connect to the module. This allows for auto-configuring the receptionist.
+ * The queries are sent from the receptionist in batches, the answers are sent
+ back to the receptionist in batches too.
+ * It is possible to fine-tune and use OS-specific tricks (like epoll or
+ sending multiple UDP messages by single call to sendmmsg()).
+
+Proposal
+--------
+
+Implement the receptionist in a way we can still work without it (not throwing
+the current UDPServer and TCPServer in asiodns away).
+
+The way we handle xfrout and DDNS needs some changes, since we can't forward
+sockets for the query. We would implement the receptionist protocol on them,
+which would allow the receptionist to forward messages to them. We would then
+modify auth to be able to forward the queries over the receptionist protocol,
+so ordinary users don't need to start the receptionist.
diff --git a/doc/design/resolver/03-cache-algorithm b/doc/design/resolver/03-cache-algorithm
new file mode 100644
index 0000000..261d824
--- /dev/null
+++ b/doc/design/resolver/03-cache-algorithm
@@ -0,0 +1,256 @@
+03-cache-algorithm
+
+Introduction
+------------
+Cache performance may be important for the resolver. It might not be
+critical. We need to research this.
+
+One key question is: given a specific cache hit rate, how much of an
+impact does cache performance have?
+
+For example, if we have 90% cache hit rate, will we still be spending
+most of our time in system calls or in looking things up in our cache?
+
+There are several ways we can consider figuring this out, including
+measuring this in existing resolvers (BIND 9, Unbound) or modeling
+with specific values.
+
+Once we know how critical the cache performance is, we can consider
+which algorithm is best for that. If it is very critical, then a
+custom algorithm designed for DNS caching makes sense. If it is not,
+then we can consider using an STL-based data structure.
+
+Effectiveness of Cache
+----------------------
+
+First, I'll try to answer the introductory questions.
+
+In some simplified model, we can express the amount of running time
+for answering queries directly from the cache in the total running
+time including that used for recursive resolution due to cache miss as
+follows:
+
+A = r*Q2*/(r*Q2+ Q1*(1-r))
+where
+A: amount of time for answering queries from the cache per unit time
+ (such as sec, 0<=A<=1)
+r: cache hit rate (0<=r<=1)
+Q1: max qps of the server with 100% cache hit
+Q2: max qps of the server with 0% cache hit
+
+Q1 can be measured easily for given data set; measuring Q2 is tricky
+in general (it requires many external queries with unreliable
+results), but we can still have some not-so-unrealistic numbers
+through controlled simulation.
+
+As a data point for these values, see a previous experimental results
+of mine:
+https://lists.isc.org/pipermail/bind10-dev/2012-July/003628.html
+
+Looking at the "ideal" server implementation (no protocol overhead)
+with the set up 90% and 85% cache hit rate with 1 recursion on cache
+miss, and with the possible maximum total throughput, we can deduce
+Q1 and Q2, which are: 170591qps and 60138qps respectively.
+
+This means, with 90% cache hit rate (r = 0.9), the server would spend
+76% of its run time for receiving queries and answering responses
+directly from the cache: 0.9*60138/(0.9*60138 + 0.1*170591) = 0.76.
+
+I also ran more realistic experiments: using BIND 9.9.2 and unbound
+1.4.19 in the "forward only" mode with crafted query data and the
+forwarded server to emulate the situation of 100% and 0% cache hit
+rates. I then measured the max response throughput using a
+queryperf-like tool. In both cases Q2 is about 28% of Q1 (I'm not
+showing specific numbers to avoid unnecessary discussion about
+specific performance of existing servers; it's out of scope of this
+memo). Using Q2 = 0.28*Q1, above equation with 90% cache hit rate
+will be: A = 0.9 * 0.28 / (0.9*0.28 + 0.1) = 0.716. So the server will
+spend about 72% of its running time to answer queries directly from
+the cache.
+
+Of course, these experimental results are too simplified. First, in
+these experiments we assumed only one external query is needed on
+cache miss. In general it can be more; however, it may not actually
+be too optimistic either: in my another research result:
+http://bind10.isc.org/wiki/ResolverPerformanceResearch
+In the more detailed analysis using real query sample and tracing what
+an actual resolver would do, it looked we'd need about 1.44 to 1.63
+external queries per cache miss in average.
+
+Still, of course, the real world cases are not that simple: in reality
+we'd need to deal with timeouts, slower remote servers, unexpected
+intermediate results, etc. DNSSEC validating resolvers will clearly
+need to do more work.
+
+So, in the real world deployment Q2 should be much smaller than Q1.
+Here are some specific cases of the relationship between Q1 and Q2 for
+given A (assuming r = 0.9):
+
+70%: Q2 = 0.26 * Q1
+60%: Q2 = 0.17 * Q1
+50%: Q2 = 0.11 * Q1
+
+So, even if "recursive resolution is 10 times heavier" than the cache
+only case, we can assume the server spends a half of its run time for
+answering queries directly from the cache at the cache hit rate of
+90%. I think this is a reasonably safe assumption.
+
+Now, assuming the number of 50% or more, does this suggest we should
+highly optimize the cache? Opinions may vary on this point, but I
+personally think the answer is yes. I've written an experimental
+cache only implementation that employs the idea of fully-rendered
+cached data. On one test machine (2.20GHz AMD64, using a single
+core), queryperf-like benchmark shows it can handle over 180Kqps,
+while BIND 9.9.2 can just handle 41K qps. The experimental
+implementation skips some necessary features for a production server,
+and cache management itself is always inevitable bottleneck, so the
+production version wouldn't be that fast, but it still suggests it may
+not be very difficult to reach over 100Kqps in production environment
+including recursive resolution overhead.
+
+Cache Types
+-----------
+
+1. Record cache
+
+Conceptually, any recursive resolver (with cache) implementation would
+have cache for RRs (or RRsets in the modern version of protocol) given
+in responses to its external queries. In BIND 9, it's called the
+"cached DB", using an in-memory rbt-like tree. unbound calls it
+"rrset cache", which is implemented as a hash table.
+
+2. Delegation cache
+
+Recursive server implementations would also have cache to determine
+the deepest zone cut for a given query name in the recursion process.
+Neither BIND 9 nor unbound has a separate cache for this purpose;
+basically they try to find an NR RRset from the "record cache" whose
+owner name best matches the given query name.
+
+3. Remote server cache
+
+In addition, a recursive server implementation may maintain a cache
+for information of remote authoritative servers. Both BIND 9 and
+unbound conceptually have this type of cache, although there are some
+non-negligible differences in details. BIND 9's implementation of
+this cache is called ADB. Its a hash table whose key is domain name,
+and each entry stores corresponding IPv6/v4 addresses; another data
+structure for each address stores averaged RTT for the address,
+lameness information, EDNS availability, etc. unbound's
+implementation is called "infrastructure cache". It's a hash table
+keyed with IP addresses whose entries store similar information as
+that in BIND 9's per address ADB entry. In unbound a remote server's
+address must be determined by looking up the record cache (rrset cache
+in unbound terminology); unlike BIND 9's ADB, there's no direct
+shortcut from a server's domain name to IP addresses.
+
+4. Full response cache
+
+unbound has an additional cache layer, called the "message cache".
+It's a hash table whose hash key is query parameter (essentially qname
+and type) and entry is a sequence to record (rrset) cache entries.
+This sequence constructs a complete response to the corresponding
+query, so it would help optimize building a response message skipping
+the record cache for each section (answer/authority/additional) of the
+response message. PowerDNS recursor has (seemingly) the same concept
+called "packet cache" (but I don't know its implementation details
+very much).
+
+BIND 9 doesn't have this type of cache; it always looks into the
+record cache to build a complete response to a given query.
+
+Miscellaneous General Requirements
+----------------------------------
+
+- Minimize contention between threads (if threaded)
+- Cache purge policy: normally only a very small part of cached DNS
+ information will be reused, and those reused are very heavily
+ reused. So LRU-like algorithm should generally work well, but we'll
+ also need to honor DNS TTL.
+
+Random Ideas for BIND 10
+------------------------
+
+Below are specific random ideas for BIND 10. Some are based on
+experimental results with reasonably realistic data; some others are
+mostly a guess.
+
+1. Fully rendered response cache
+
+Some real world query samples show that a very small portion of entire
+queries are very popular and queried very often and many times; the
+rest is rarely reused, if any. Two different data sets show top
+10,000 queries would cover around 80% of total queries, regardless
+of the size of the total queries. This suggests an idea of having a
+small, highly optimized full response cache.
+
+I tried this idea in the jinmei-l1cache branch. It's a hash table
+keyed with a tuple of query name and type whose entry stores fully
+rendered, wire-format response image (answer section only, assuming
+the "minimal-responses" option). It also maintains offsets to each
+RR, so it can easily update TTLs when necessary or rotate RRs if
+optionally requested. If neither TTL adjustment nor RR rotation is
+required, query handling is just to lookup the hash table and copy the
+pre-rendered data. Experimental benchmark showed it ran vary fast;
+more than 4 times faster than BIND 9, and even much faster than other
+implementations that have full response cache (although, as usual, the
+comparison is not entirely fair).
+
+Also, the cache size is quite small; the run time memory footprint of
+this server process was just about 5MB. So, I think it's reasonable
+to have each process/thread have their own copy of this cache to
+completely eliminate contention. Also, if we can keep the cache size
+this small, it would be easier to dump it to a file on shutdown and
+reuse it on restart. This will be quite effective (if the downtime is
+reasonably short) because the cached data are expected to be highly
+popular.
+
+2. Record cache
+
+For the normal record cache, I don't have a particular idea beyond
+something obvious, like a hash table to map from query parameters to
+corresponding RRset (or negative information). But I guess this cache
+should be shared by multiple threads. That will help reconstruct the
+full response cache data on TTL expiration more efficiently. And, if
+shared, the data structure should be chosen so that contention
+overhead can be minimized. In general, I guess something like hash
+tables is more suitable than tree-like structure in that sense.
+
+There's other points to discuss for this cache related to other types
+of cache (see below).
+
+3. Separate delegation cache
+
+One thing I'm guessing is that it may make sense if we have a separate
+cache structure for delegation data. It's conceptually a set of NS
+RRs so we can identify the best (longest) matching one for a given
+query name.
+
+Analysis of some sets of query data showed the vast majority of
+end client's queries are for A and AAAA (not surprisingly). So, even
+if we separate this cache from the record cache, the additional
+overhead (both for memory and fetch) will probably (hopefully) be
+marginal. Separating caches will also help reduce contention between
+threads. It *might* also help improve lookup performance because this
+can be optimized for longest match search.
+
+4. Remote server cache without involving the record cache
+
+Likewise, it may make sense to maintain the remote server cache
+separately from the record cache. I guess these AAAA and A records
+are rarely the queried by end clients, so, like the case of delegation
+cache it's possible that the data sets are mostly disjoint. Also, for
+this purpose the RRsets don't have to have higher trust rank (per
+RFC2181 5.4.1): glue or additional are okay, and, by separating these
+from the record cache, we can avoid accidental promotion of these data
+to trustworthy answers and returning them to clients (BIND 9 had this
+type of bugs before).
+
+Custom vs Existing Library (STL etc)
+------------------------------------
+
+It may have to be discussed, but I guess in many cases we end up
+introducing custom implementation because these caches should be
+highly performance sensitive, directly related to our core business, and
+also have to be memory efficient. But in some sub-components we may
+be able to benefit from existing generic libraries.
diff --git a/doc/design/resolver/README b/doc/design/resolver/README
new file mode 100644
index 0000000..b6e9285
--- /dev/null
+++ b/doc/design/resolver/README
@@ -0,0 +1,5 @@
+This directory contains research and design documents for the BIND 10
+resolver reimplementation.
+
+Each file contains a specific issue and discussion surrounding that
+issue.
diff --git a/doc/devel/contribute.dox b/doc/devel/contribute.dox
new file mode 100644
index 0000000..9103bed
--- /dev/null
+++ b/doc/devel/contribute.dox
@@ -0,0 +1,162 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+
+ @page contributorGuide BIND10 Contributor's Guide
+
+So you found a bug in BIND10 or plan to develop an extension and want to
+send a patch? Great! This page will explain how to contribute your
+changes smoothly.
+
+ at section contributorGuideWritePatch Writing a patch
+
+Before you start working on a patch or a new feature, it is a good idea
+to discuss it first with BIND10 developers. You can post your questions
+to the \c bind10-dev mailing list
+(https://lists.isc.org/mailman/listinfo/bind10-dev) for general BIND10
+stuff, or to the \c bind10-dhcp mailing list
+(https://lists.isc.org/mailman/listinfo/bind10-dhcp) for DHCP specific
+topics. If you prefer to get faster feedback, most BIND10 developers
+hang out in the \c bind10 jabber room
+(xmpp:bind10 at conference.jabber.isc.org). Those involved in DHCP also use
+the \c dhcp chatroom (xmpp:dhcp at conference.jabber.isc.org). Feel free to
+join these rooms and talk to us. It is possible that someone else is
+working on your specific issue or perhaps the solution you plan to
+implement is not the best one. Often having a 10 minute talk could save
+many hours of engineering work.
+
+First step would be to get the source code from our Git repository. The
+procedure is very easy and is explained here:
+http://bind10.isc.org/wiki/GitGuidelines. While it is possible to
+provide a patch against the latest stable release, it makes the review
+process much easier if it is for latest code from the Git \c master
+branch.
+
+Ok, so you have written a patch? Great! Before you submit it, make sure
+that your code compiles. This may seem obvious, but there's more to
+it. You have surely checked that it compiles on your system, but BIND10
+is portable software. Besides Linux, it is compiled and used on
+relatively uncommon systems like OpenBSD and Solaris 11. Will your code
+compile and work there? What about endianess? It is likely that you used
+a regular x86 architecture machine to write your patch, but the software
+is expected to run on many other architectures. You may take a look at
+system specific build notes (http://bind10.isc.org/wiki/SystemSpecificNotes).
+For a complete list of systems we build on, you may take a look at the
+following build farm report: http://git.bind10.isc.org/~tester/builder/builder-new.html .
+
+Does your patch conform to BIND10 coding guidelines
+(http://bind10.isc.org/wiki/CodingGuidelines)? You still can submit a
+patch that does not adhere to it, but that will decrease its chances of
+being accepted. If the deviations are minor, the BIND10 engineer who
+does the review will likely fix the issues. However, if there are lots
+of issues, the reviewer may simply reject the patch and ask you to fix
+it before re-submitting.
+
+ at section contributorGuideUnittests Running unit-tests
+
+One of the ground rules in BIND10 development is that every piece of
+code has to be tested. We now have an extensive set of unit-tests for
+almost every line of code. Even if you are fixing something small,
+like a single line fix, it is encouraged to write unit-tests for that
+change. That is even more true for new code. If you write a new
+function, method or a class, you definitely should write unit-tests
+for it.
+
+BIND10 uses the Google C++ Testing Framework (also called googletest or
+gtest) as a base for our C++ unit-tests. See
+http://code.google.com/p/googletest/ for details. For Python unit-tests,
+we use the its \c unittest library which is included in Python. You must
+have \c gtest installed or at least extracted in a directory before
+compiling BIND10 unit-tests. To enable unit-tests in BIND10, use:
+
+ at code
+./configure --with-gtest=/path/to/your/gtest/dir
+ at endcode
+
+or
+
+ at code
+./configure --with-gtest-source=/path/to/your/gtest/dir
+ at endcode
+
+Depending on how you compiled or installed \c gtest (e.g. from sources
+or using some package management system) one of those two switches will
+find \c gtest. After that you make run unit-tests:
+
+ at code
+make check
+ at endcode
+
+If you happen to add new files or have modified any \c Makefile.am
+files, it is also a good idea to check if you haven't broken the
+distribution process:
+
+ at code
+make distcheck
+ at endcode
+
+There are other useful switches which can be passed to configure. It is
+always a good idea to use \c --enable-logger-checks, which does sanity
+checks on logger parameters. If you happen to modify anything in the
+documentation, use \c --enable-generate-docs. If you are modifying DHCP
+code, you are likely to be interested in enabling the MySQL backend for
+DHCP. Note that if the backend is not enabled, MySQL specific unit-tests
+are skipped. From that perspective, it is useful to use
+\c --with-dhcp-mysql. For a complete list of all switches, use:
+
+ at code
+ ./configure --help
+ at endcode
+
+ at section contributorGuideReview Going through a review
+
+Once all those are checked and working, feel free to create a ticket for
+your patch at http://bind10.isc.org/ or attach your patch to an existing
+ticket if you have fixed it. It would be nice if you also join the
+\c bind10 or \c dhcp chatroom saying that you have submitted a
+patch. Alternatively, you may send a note to the \c bind10-dev or
+\c bind10-dhcp mailing lists.
+
+Here's the tricky part. One of BIND10 developers will review your patch,
+but it may not happen immediately. Unfortunately, developers are usually
+working under a tight schedule, so any extra unplanned review work may
+take a while sometimes. Having said that, we value external
+contributions very much and will do whatever we can to review patches in
+a timely manner. Don't get discouraged if your patch is not accepted
+after first review. To keep the code quality high, we use the same
+review processes for internal code and for external patches. It may take
+some cycles of review/updated patch submissions before the code is
+finally accepted.
+
+Once the process is almost complete, the developer will likely ask you
+how you would like to be credited. The typical answers are by first and
+last name, by nickname, by company name or anonymously. Typically we
+will add a note to the \c ChangeLog and also set you as the author of
+the commit applying the patch. If the contributted feature is big or
+critical for whatever reason, it may also be mentioned in release notes.
+
+ at section contributorGuideExtra Extra steps
+
+If you are interested in knowing the results of more in-depth testing,
+you are welcome to visit the BIND10 build farm:
+http://git.bind10.isc.org/~tester/builder/builder-new.html. This is a
+live result page with all tests being run on various systems. Besides
+basic unit-tests, we also have reports from Valgrind (memory debugger),
+cppcheck and clang-analyzer (static code analyzers), Lettuce system
+tests and more. Although it is not possible for non ISC employees to run
+tests on that farm, it is possible that your contributed patch will end
+up there sooner or later.
+
+*/
diff --git a/doc/devel/mainpage.dox b/doc/devel/mainpage.dox
index 295fd03..f9c5d61 100644
--- a/doc/devel/mainpage.dox
+++ b/doc/devel/mainpage.dox
@@ -1,44 +1,91 @@
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
/**
- *
* @mainpage BIND10 Developer's Guide
*
* Welcome to BIND10 Developer's Guide. This documentation is addressed
- * at existing and prospecting developers and programmers, who would like
- * to gain insight into internal workings of BIND 10. It could also be useful
- * for existing and prospective contributors.
+ * at existing and prospecting developers and programmers and provides
+ * information needed to both extend and maintain BIND 10.
+ *
+ * If you wish to write "hook" code - code that is loaded by BIND 10 at
+ * run-time and modifies its behavior you should read the section
+ * @ref hooksdgDevelopersGuide.
+ *
+ * BIND 10 maintenance information is divided into a number of sections
+ * depending on focus. DNS-specific issues are covered in the
+ * @ref dnsMaintenanceGuide while information on DHCP-specific topics can
+ * be found in the @ref dhcpMaintenanceGuide. General BIND 10 topics, not
+ * specific to any protocol, are discussed in @ref miscellaneousTopics.
*
* If you are a user or system administrator, rather than software engineer,
- * you should read <a href="http://bind10.isc.org/docs/bind10-guide.html">BIND10
+ * you should read the
+ * <a href="http://bind10.isc.org/docs/bind10-guide.html">BIND10
* Guide (Administrator Reference for BIND10)</a> instead.
*
- * Regardless of your field of expertise, you are encouraged to visit
+ * Regardless of your field of expertise, you are encouraged to visit the
* <a href="http://bind10.isc.org/">BIND10 webpage (http://bind10.isc.org)</a>
*
- * @section DNS
+ * @section contrib Contributor's Guide
+ * - @subpage contributorGuide
+ *
+ * @section hooksFramework Hooks Framework
+ * - @subpage hooksdgDevelopersGuide
+ * - @subpage dhcpv4Hooks
+ * - @subpage dhcpv6Hooks
+ * - @subpage hooksComponentDeveloperGuide
+ * - @subpage hooksmgMaintenanceGuide
+ * - @subpage libdhcp_user_chk
+ *
+ * @section dnsMaintenanceGuide DNS Maintenance Guide
* - Authoritative DNS (todo)
* - Recursive resolver (todo)
* - @subpage DataScrubbing
*
- * @section DHCP
+ * @section dhcpMaintenanceGuide DHCP Maintenance Guide
* - @subpage dhcp4
* - @subpage dhcpv4Session
* - @subpage dhcpv4ConfigParser
* - @subpage dhcpv4ConfigInherit
+ * - @subpage dhcpv4OptionsParse
+ * - @subpage dhcpv4DDNSIntegration
+ * - @subpage dhcpv4Classifier
+ * - @subpage dhcpv4Other
* - @subpage dhcp6
* - @subpage dhcpv6Session
* - @subpage dhcpv6ConfigParser
* - @subpage dhcpv6ConfigInherit
+ * - @subpage dhcpv6DDNSIntegration
+ * - @subpage dhcpv6OptionsParse
+ * - @subpage dhcpv6Classifier
+ * - @subpage dhcpv6Other
* - @subpage libdhcp
* - @subpage libdhcpIntro
+ * - @subpage libdhcpRelay
* - @subpage libdhcpIfaceMgr
+ * - @subpage libdhcpPktFilter
+ * - @subpage libdhcpPktFilter6
+ * - @subpage libdhcpErrorLogging
* - @subpage libdhcpsrv
* - @subpage leasemgr
* - @subpage cfgmgr
* - @subpage allocengine
* - @subpage dhcpDatabaseBackends
* - @subpage perfdhcpInternals
+ * - @subpage libdhcp_ddns
*
- * @section misc Miscellaneous topics
+ * @section miscellaneousTopics Miscellaneous Topics
* - @subpage LoggingApi
* - @subpage LoggingApiOverview
* - @subpage LoggingApiLoggerNames
@@ -46,7 +93,10 @@
* - @subpage SocketSessionUtility
* - <a href="./doxygen-error.log">Documentation warnings and errors</a>
*
- * @todo: Move this logo to the right (and possibly up). Not sure what
- * is the best way to do it in Doxygen, without using CSS hacks.
* @image html isc-logo.png
*/
+/*
+ * @todo: Move the logo to the right (and possibly up). Not sure what
+ * is the best way to do it in Doxygen, without using CSS hacks.
+ */
+
diff --git a/doc/guide/.gitignore b/doc/guide/.gitignore
index 168d4ed..fc2510e 100644
--- a/doc/guide/.gitignore
+++ b/doc/guide/.gitignore
@@ -1,3 +1,4 @@
/bind10-guide.html
/bind10-guide.txt
/bind10-messages.html
+/bind10-messages.xml
diff --git a/doc/guide/Makefile.am b/doc/guide/Makefile.am
index 1d63c04..3bffa1a 100644
--- a/doc/guide/Makefile.am
+++ b/doc/guide/Makefile.am
@@ -17,20 +17,18 @@ bind10-guide.html: bind10-guide.xml
-o $@ \
--stringparam section.autolabel 1 \
--stringparam section.label.includes.component.label 1 \
- --stringparam html.stylesheet $(srcdir)/bind10-guide.css \
+ --stringparam html.stylesheet bind10-guide.css \
http://docbook.sourceforge.net/release/xsl/current/html/docbook.xsl \
$(srcdir)/bind10-guide.xml
-HTML2TXT = elinks -dump -no-numbering -no-references
-
bind10-guide.txt: bind10-guide.html
- $(HTML2TXT) bind10-guide.html > $@
+ @ELINKS@ -dump -no-numbering -no-references bind10-guide.html > $@
bind10-messages.html: bind10-messages.xml
@XSLTPROC@ --novalid --xinclude --nonet \
--path $(top_builddir)/doc \
-o $@ \
- --stringparam html.stylesheet $(srcdir)/bind10-guide.css \
+ --stringparam html.stylesheet bind10-guide.css \
http://docbook.sourceforge.net/release/xsl/current/html/docbook.xsl \
bind10-messages.xml
diff --git a/doc/guide/bind10-guide.xml b/doc/guide/bind10-guide.xml
index d0d1d4c..1be98f9 100644
--- a/doc/guide/bind10-guide.xml
+++ b/doc/guide/bind10-guide.xml
@@ -427,7 +427,7 @@ var/
<listitem>
<para>Go into the source and run configure:
<screen>$ <userinput>cd bind10-<replaceable>VERSION</replaceable></userinput>
- $ <userinput>./configure</userinput></screen>
+$ <userinput>./configure</userinput></screen>
</para>
</listitem>
@@ -438,21 +438,29 @@ var/
</listitem>
<listitem>
- <para>Install it as root (to default /usr/local):
+ <para>Install it as root (by default to prefix
+ <filename>/usr/local/</filename>):
<screen>$ <userinput>make install</userinput></screen>
</para>
</listitem>
<listitem>
+ <para>Change directory to the install prefix (by default
+ <filename>/usr/local/</filename>):
+ <screen>$ <userinput>cd /usr/local/</userinput></screen>
+ </para>
+ </listitem>
+
+ <listitem>
<para>Create a user for yourself:
- <screen>$ <userinput>cd /usr/local/etc/bind10/</userinput></screen>
- <screen>$ <userinput>/usr/local/sbin/b10-cmdctl-usermgr</userinput></screen>
+ <screen>$ <userinput>sbin/b10-cmdctl-usermgr add root</userinput></screen>
+ and enter a newly chosen password when prompted.
</para>
</listitem>
<listitem>
<para>Start the server (as root):
- <screen>$ <userinput>/usr/local/sbin/bind10</userinput></screen>
+ <screen>$ <userinput>sbin/bind10</userinput></screen>
</para>
</listitem>
@@ -461,7 +469,7 @@ var/
configuration. In another console, enable the authoritative
DNS service (by using the <command>bindctl</command> utility
to configure the <command>b10-auth</command> component to
- run): <screen>$ <userinput>bindctl</userinput></screen>
+ run): <screen>$ <userinput>bin/bindctl</userinput></screen>
(Login with the username and password you used above to create a user.)
<screen>
> <userinput>config add Init/components b10-auth</userinput>
@@ -481,7 +489,7 @@ var/
<listitem>
<para>Load desired zone file(s), for example:
- <screen>$ <userinput>b10-loadzone <replaceable>-c '{"database_file": "/usr/local/var/bind10/zone.sqlite3"}'</replaceable> <replaceable>your.zone.example.org</replaceable> <replaceable>your.zone.file</replaceable></userinput></screen>
+ <screen>$ <userinput>bin/b10-loadzone <replaceable>-c '{"database_file": "/usr/local/var/bind10/zone.sqlite3"}'</replaceable> <replaceable>your.zone.example.org</replaceable> <replaceable>your.zone.file</replaceable></userinput></screen>
</para>
(If you use the sqlite3 data source with the default DB
file, you can omit the -c option).
@@ -828,6 +836,11 @@ as a dependency earlier -->
and documentation, run:
<screen>$ <userinput>make install</userinput></screen>
</para>
+ <para>
+ Please don't use any form of parallel or job server options
+ (such as GNU make's <command>-j</command> option) when
+ performing this step. Doing so may cause errors.
+ </para>
<note>
<para>The install step may require superuser privileges.</para>
</note>
@@ -2601,21 +2614,26 @@ can use various data source backends.
> <userinput>config set data_sources/classes/IN[1]/params { "example.org": "/path/to/example.org", "example.com": "/path/to/example.com" }</userinput>
> <userinput>config commit</userinput></screen>
+ Unfortunately, due to current technical limitations, the
+ params must be set as one JSON blob. To reload a zone, use the
+ same <command>Auth loadzone</command> command as above.
+ </para>
+
+ <para>
Initially, a map value has to be set, but this value may be an
- empty map. After that, key/value pairs can be added with 'config
- add' and keys can be removed with 'config remove'. The initial
- value may be an empty map, but it has to be set before zones are
- added or removed.
+ empty map. After that, key/value pairs can be added with
+ <command>config add</command> and keys can be removed with
+ <command>config remove</command>. The initial value may be an
+ empty map, but it has to be set before zones are added or
+ removed.
<screen>
> <userinput>config set data_sources/classes/IN[1]/params {}</userinput>
> <userinput>config add data_sources/classes/IN[1]/params another.example.org /path/to/another.example.org</userinput>
> <userinput>config add data_sources/classes/IN[1]/params another.example.com /path/to/another.example.com</userinput>
> <userinput>config remove data_sources/classes/IN[1]/params another.example.org</userinput>
- </screen>
+> <userinput>config commit</userinput></screen>
- <command>bindctl</command>. To reload a zone, you the same command
- as above.
</para>
</section>
@@ -2624,7 +2642,7 @@ can use various data source backends.
There's also <varname>Auth/database_file</varname> configuration
variable, pointing to a SQLite3 database file. This is no longer
used by <command>b10-auth</command>, but it is left in place for
- now, since other modules use it. Once <command>b10-xfrin</command>,
+ now, since other modules use it. Once <command>b10-zonemgr</command>,
<command>b10-xfrout</command> and <command>b10-ddns</command>
are ported to the new configuration, this will disappear. But for
now, make sure that if you use any of these modules, the new
@@ -2634,6 +2652,50 @@ can use various data source backends.
</para>
</note>
+ <section id='datasrc-static'>
+ <title>Adding a static data source</title>
+
+ <para>
+ BIND 10 includes a zone file named
+ <filename>static.zone</filename> in the CH (Chaos) class for
+ providing information about the server via the AUTHORS.BIND
+ and VERSION.BIND TXT records. By default, this BIND zone is
+ configured and its records are served.
+ </para>
+
+ <para>
+ If you have removed this zone from the configuration (e.g., by
+ using the commands in the previous section to disable the
+ "built-in data source"), here is how you can add it back to
+ serve the zones in the <filename>static.zone</filename> file.
+ </para>
+
+ <para>First, add the CH class if it doesn't exist:
+
+ <screen>> <userinput>config add data_sources/classes CH</userinput>
+> <userinput>config commit</userinput></screen>
+
+ Then, add a data source of type <emphasis>MasterFiles</emphasis>
+ in the CH class to serve the zones in
+ <filename>static.zone</filename>:
+
+ <screen>> <userinput>config add data_sources/classes/CH</userinput>
+> <userinput>config set data_sources/classes/CH[0]/type MasterFiles</userinput>
+> <userinput>config set data_sources/classes/CH[0]/cache-enable true</userinput>
+> <userinput>config set data_sources/classes/CH[0]/params {"BIND": "/usr/local/bind10/share/bind10/static.zone"}</userinput>
+> <userinput>config commit</userinput></screen>
+
+ Then, lookup the static data from
+ <filename>static.zone</filename> to test it (assuming your
+ authoritative server is running on <command>localhost</command>):
+
+ <screen>> <userinput>dig @localhost -c CH -t TXT version.bind</userinput>
+> <userinput>dig @localhost -c CH -t TXT authors.bind</userinput></screen>
+
+ </para>
+
+ </section>
+
</section>
<section>
@@ -2725,19 +2787,29 @@ TODO
<para>
The <command>b10-xfrin</command> process supports both AXFR and
- IXFR. Due to some implementation limitations of the current
- development release, however, it only tries AXFR by default,
- and care should be taken to enable IXFR.
+ IXFR.
</para>
-<!-- TODO: http://bind10.isc.org/ticket/1279 -->
<section>
<title>Configuration for Incoming Zone Transfers</title>
<para>
- In practice, you need to specify a list of secondary zones to
- enable incoming zone transfers for these zones (you can still
- trigger a zone transfer manually, without a prior configuration
- (see below)).
+ In order to enable incoming zone transfers for a secondary
+ zone, you will first need to make the zone "exist" in some
+ data source.
+ One easy way to do this is to create an empty zone using the
+ <command>b10-loadzone</command> utility.
+ For example, this makes an empty zone (or empties any existing
+ content of the zone) "example.com" in the default data source
+ for <command>b10-loadzone</command> (which is SQLite3-based
+ data source):
+ <screen>$ <userinput>b10-loadzone <replaceable>-e</replaceable> <replaceable>example.com</replaceable></userinput></screen>
+ </para>
+
+ <para>
+ Next, you need to specify a list of secondary zones to
+ enable incoming zone transfers for these zones in most
+ practical cases (you can still trigger a zone transfer
+ manually, without a prior configuration (see below)).
</para>
<para>
@@ -2752,6 +2824,17 @@ TODO
(We assume there has been no zone configuration before).
</para>
+
+ <note>
+ <simpara>
+ There is a plan to revise overall zone management
+ configuration (which are primary and secondary zones, which
+ data source they are stored, etc) so it can be configured
+ more consistently and in a unified way among various BIND 10 modules.
+ When it's done, part or all of the initial configuration
+ setup described in this section may be deprecated.
+ </simpara>
+ </note>
</section>
<section>
@@ -2763,34 +2846,82 @@ TODO
> <userinput>config set Xfrin/zones[0]/tsig_key "<option>example.key</option>"</userinput>
</section>
- <section>
- <title>Enabling IXFR</title>
- <para>
- As noted above, <command>b10-xfrin</command> uses AXFR for
- zone transfers by default. To enable IXFR for zone transfers
- for a particular zone, set the <varname>use_ixfr</varname>
- configuration parameter to <quote>true</quote>.
- In the above example of configuration sequence, you'll need
- to add the following before performing <userinput>commit</userinput>:
- <screen>> <userinput>config set Xfrin/zones[0]/use_ixfr true</userinput></screen>
- </para>
+ <section id="request_ixfr">
+ <title>Control the use of IXFR</title>
+ <para>
+ By default, <command>b10-xfrin</command> uses IXFR for
+ transferring zones specified in
+ the <varname>Xfrin/zones</varname> list of the configuration,
+ unless it doesn't know the current SOA serial of the zone
+ (including the case where the zone has never transferred or
+ locally loaded), in which case it automatically uses AXFR.
+ If the attempt of IXFR fails, <command>b10-xfrin</command>
+ automatically retries the transfer using AXFR.
+ In general, this works for any master server implementations
+ including those that don't support IXFR and in any local state
+ of the zone. So there should normally be no need to configure
+ on whether to use IXFR.
+ </para>
+
+ <para>
+ In some cases, however, it may be desirable to specify how and
+ whether to use IXFR and AXFR.
+ The <varname>request_ixfr</varname> configuration item under
+ <varname>Xfrin/zones</varname> can be used to control such
+ policies.
+ It can take the following values.
+ </para>
+ <variablelist>
+ <varlistentry>
+ <term>yes</term>
+ <listitem>
+ <simpara>
+ This is the default behavior as described above.
+ </simpara>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>no</term>
+ <listitem>
+ <simpara>
+ Only use AXFR. Note that this value normally shouldn't
+ be needed thanks to the automatic fallback from IXFR to IXFR.
+ A possible case where this value needs to be used is
+ that the master server has a bug and crashes if it
+ receives an IXFR request.
+ </simpara>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>only</term>
+ <listitem>
+ <simpara>
+ Only use IXFR except when the current SOA serial is not
+ known.
+ This value has a severe drawback, that is, if the master
+ server does not support IXFR zone transfers never
+ succeed (except for the very first one, which will use AXFR),
+ and the zone will eventually expire.
+ Therefore it should not be used in general.
+ Still, in some special cases the use of this value may
+ make sense. For example, if the operator is sure that
+ the master server supports IXFR and the zone is very
+ large, they may want to avoid falling back to AXFR as
+ it can be more expensive.
+ </simpara>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+
+ <note>
+ <simpara>
+ There used to be a boolean configuration item named
+ <varname>use_ixfr</varname>.
+ It was deprecated for the finer control described above.
+ The <varname>request_ixfr</varname> item should be used instead.
+ </simpara>
+ </note>
-<!-- TODO: http://bind10.isc.org/ticket/1279 -->
- <note><simpara>
- One reason why IXFR is disabled by default in the current
- release is because it does not support automatic fallback from IXFR to
- AXFR when it encounters a primary server that doesn't support
- outbound IXFR (and, not many existing implementations support
- it). Another, related reason is that it does not use AXFR even
- if it has no knowledge about the zone (like at the very first
- time the secondary server is set up). IXFR requires the
- "current version" of the zone, so obviously it doesn't work
- in this situation and AXFR is the only workable choice.
- The current release of <command>b10-xfrin</command> does not
- make this selection automatically.
- These features will be implemented in a near future
- version, at which point we will enable IXFR by default.
- </simpara></note>
</section>
<!-- TODO:
@@ -2854,6 +2985,23 @@ what if a NOTIFY is sent?
<screen>> <userinput>Xfrin retransfer zone_name="<option>foo.example.org</option>" master=<option>192.0.2.99</option></userinput></screen>
</para>
+
+ <para>
+ The <command>retransfer</command> command always uses AXFR.
+ To use IXFR for a zone that has already been transferred once,
+ use the <command>refresh</command> command.
+ It honors the <varname>Xfrin/zones/request_ixfr</varname>
+ configuration item (see <xref linkend="request_ixfr"/>.), and
+ if it's configured to use IXFR, it will be used.
+ </para>
+
+ <para>
+ Both the <command>retransfer</command>
+ and <command>refresh</command> commands can be used for
+ an initial transfer before setting up secondary
+ configurations.
+ In this case AXFR will be used for the obvious reason.
+ </para>
</section>
<section>
@@ -2879,7 +3027,6 @@ http://bind10.isc.org/wiki/ScalableZoneLoadDesign#a7.2UpdatingaZone
</para>
</section>
-<!-- TODO: can that retransfer be used to identify a new zone? -->
<!-- TODO: what if doesn't exist at that master IP? -->
</chapter>
@@ -3514,7 +3661,7 @@ $</screen>
will be available. It will look similar to this:
<screen>
> <userinput>config show Dhcp4</userinput>
-Dhcp4/interface/ list (default)
+Dhcp4/interfaces/ list (default)
Dhcp4/renew-timer 1000 integer (default)
Dhcp4/rebind-timer 2000 integer (default)
Dhcp4/valid-lifetime 4000 integer (default)
@@ -3601,6 +3748,60 @@ Dhcp4/subnet4 [] list (default)
</note>
</section>
+ <section id="dhcp4-interface-selection">
+ <title>Interface selection</title>
+ <para>
+ When DHCPv4 server starts up, by default it will listen to the DHCP
+ traffic and respond to it on all interfaces detected during startup.
+ However, in many cases it is desired to configure the server to listen and
+ respond on selected interfaces only. The sample commands in this section
+ show how to make interface selection using bindctl.
+ </para>
+ <para>
+ The default configuration can be presented with the following command:
+ <screen>
+> <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0] "*" string</userinput></screen>
+ An asterisk sign plays a role of the wildcard and means "listen on all interfaces".
+ </para>
+ <para>
+ In order to override the default configuration, the existing entry can be replaced
+ with the actual interface name:
+ <screen>
+> <userinput>config set Dhcp4/interfaces[0] eth1</userinput>
+> <userinput>config commit</userinput></screen>
+ Other interface names can be added on one-by-one basis:
+ <screen>
+> <userinput>config add Dhcp4/interfaces eth2</userinput>
+> <userinput>config commit</userinput></screen>
+ Configuration will now contain two interfaces which can be presented as follows:
+ <screen>
+> <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0] "eth1" string</userinput>
+<userinput>Dhcp4/interfaces[1] "eth2" string</userinput></screen>
+ When configuration gets committed, the server will start to listen on
+ eth1 and eth2 interfaces only.
+ </para>
+ <para>
+ It is possible to use wildcard interface name (asterisk) concurrently with explicit
+ interface names:
+ <screen>
+> <userinput>config add Dhcp4/interfaces *</userinput>
+> <userinput>config commit</userinput></screen>
+ This will result in the following configuration:
+ <screen>
+> <userinput>config show Dhcp4/interfaces</userinput>
+<userinput>Dhcp4/interfaces[0] "eth1" string</userinput>
+<userinput>Dhcp4/interfaces[1] "eth2" string</userinput>
+<userinput>Dhcp4/interfaces[2] "*" string</userinput></screen>
+ The presence of the wildcard name implies that server will listen on all interfaces.
+ In order to fall back to the previous configuration when server listens on eth1 and eth2:
+ <screen>
+> <userinput>config remove Dhcp4/interfaces[2]</userinput>
+> <userinput>config commit</userinput></screen>
+ </para>
+ </section>
+
<section id="dhcp4-address-config">
<title>Configuration of Address Pools</title>
<para>
@@ -3745,7 +3946,10 @@ Dhcp4/subnet4 [] list (default)
</note>
<para>
- Below is a list of currently supported standard DHCPv4 options. The "Name" and "Code"
+ The currently supported standard DHCPv4 options are
+ listed in <xref linkend="dhcp4-std-options-list"/>
+ and <xref linkend="dhcp4-std-options-list-part2"/>.
+ The "Name" and "Code"
are the values that should be used as a name in the option-data
structures. "Type" designates the format of the data: the meanings of
the various types is given in <xref linkend="dhcp-types"/>.
@@ -3759,115 +3963,157 @@ Dhcp4/subnet4 [] list (default)
<!-- @todo: describe record types -->
<para>
- <table border="1" cellpadding="5%" id="dhcp4-std-options-list">
- <caption>List of standard DHCPv4 options</caption>
+ <table frame="all" id="dhcp4-std-options-list">
+ <title>List of standard DHCPv4 options</title>
+ <tgroup cols='4'>
+ <colspec colname='name'/>
+ <colspec colname='code'/>
+ <colspec colname='type'/>
+ <colspec colname='array'/>
<thead>
- <tr><th>Name</th><th>Code</th><th>Type</th><th>Array?</th></tr>
+ <row>
+ <entry>Name</entry>
+ <entry>Code</entry>
+ <entry>Type</entry>
+ <entry>Array?</entry>
+ </row>
</thead>
<tbody>
-<tr><td>subnet-mask</td><td>1</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>time-offset</td><td>2</td><td>uint32</td><td>false</td></tr>
-<tr><td>routers</td><td>3</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>time-servers</td><td>4</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>name-servers</td><td>5</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>domain-name-servers</td><td>6</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>log-servers</td><td>7</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>cookie-servers</td><td>8</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>lpr-servers</td><td>9</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>impress-servers</td><td>10</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>resource-location-servers</td><td>11</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>host-name</td><td>12</td><td>string</td><td>false</td></tr>
-<tr><td>boot-size</td><td>13</td><td>uint16</td><td>false</td></tr>
-<tr><td>merit-dump</td><td>14</td><td>string</td><td>false</td></tr>
-<tr><td>domain-name</td><td>15</td><td>fqdn</td><td>false</td></tr>
-<tr><td>swap-server</td><td>16</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>root-path</td><td>17</td><td>string</td><td>false</td></tr>
-<tr><td>extensions-path</td><td>18</td><td>string</td><td>false</td></tr>
-<tr><td>ip-forwarding</td><td>19</td><td>boolean</td><td>false</td></tr>
-<tr><td>non-local-source-routing</td><td>20</td><td>boolean</td><td>false</td></tr>
-<tr><td>policy-filter</td><td>21</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>max-dgram-reassembly</td><td>22</td><td>uint16</td><td>false</td></tr>
-<tr><td>default-ip-ttl</td><td>23</td><td>uint8</td><td>false</td></tr>
-<tr><td>path-mtu-aging-timeout</td><td>24</td><td>uint32</td><td>false</td></tr>
-<tr><td>path-mtu-plateau-table</td><td>25</td><td>uint16</td><td>true</td></tr>
-<tr><td>interface-mtu</td><td>26</td><td>uint16</td><td>false</td></tr>
-<tr><td>all-subnets-local</td><td>27</td><td>boolean</td><td>false</td></tr>
-<tr><td>broadcast-address</td><td>28</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>perform-mask-discovery</td><td>29</td><td>boolean</td><td>false</td></tr>
-<tr><td>mask-supplier</td><td>30</td><td>boolean</td><td>false</td></tr>
-<tr><td>router-discovery</td><td>31</td><td>boolean</td><td>false</td></tr>
-<tr><td>router-solicitation-address</td><td>32</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>static-routes</td><td>33</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>trailer-encapsulation</td><td>34</td><td>boolean</td><td>false</td></tr>
-<tr><td>arp-cache-timeout</td><td>35</td><td>uint32</td><td>false</td></tr>
-<tr><td>ieee802-3-encapsulation</td><td>36</td><td>boolean</td><td>false</td></tr>
-<tr><td>default-tcp-ttl</td><td>37</td><td>uint8</td><td>false</td></tr>
-<tr><td>tcp-keepalive-internal</td><td>38</td><td>uint32</td><td>false</td></tr>
-<tr><td>tcp-keepalive-garbage</td><td>39</td><td>boolean</td><td>false</td></tr>
-<tr><td>nis-domain</td><td>40</td><td>string</td><td>false</td></tr>
-<tr><td>nis-servers</td><td>41</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>ntp-servers</td><td>42</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>vendor-encapsulated-options</td><td>43</td><td>empty</td><td>false</td></tr>
-<tr><td>netbios-name-servers</td><td>44</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>netbios-dd-server</td><td>45</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>netbios-node-type</td><td>46</td><td>uint8</td><td>false</td></tr>
-<tr><td>netbios-scope</td><td>47</td><td>string</td><td>false</td></tr>
-<tr><td>font-servers</td><td>48</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>x-display-manager</td><td>49</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>dhcp-requested-address</td><td>50</td><td>ipv4-address</td><td>false</td></tr>
+<row><entry>subnet-mask</entry><entry>1</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>time-offset</entry><entry>2</entry><entry>int32</entry><entry>false</entry></row>
+<row><entry>routers</entry><entry>3</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>time-servers</entry><entry>4</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>name-servers</entry><entry>5</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>domain-name-servers</entry><entry>6</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>log-servers</entry><entry>7</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>cookie-servers</entry><entry>8</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>lpr-servers</entry><entry>9</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>impress-servers</entry><entry>10</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>resource-location-servers</entry><entry>11</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>host-name</entry><entry>12</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>boot-size</entry><entry>13</entry><entry>uint16</entry><entry>false</entry></row>
+<row><entry>merit-dump</entry><entry>14</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>domain-name</entry><entry>15</entry><entry>fqdn</entry><entry>false</entry></row>
+<row><entry>swap-server</entry><entry>16</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>root-path</entry><entry>17</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>extensions-path</entry><entry>18</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>ip-forwarding</entry><entry>19</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>non-local-source-routing</entry><entry>20</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>policy-filter</entry><entry>21</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>max-dgram-reassembly</entry><entry>22</entry><entry>uint16</entry><entry>false</entry></row>
+<row><entry>default-ip-ttl</entry><entry>23</entry><entry>uint8</entry><entry>false</entry></row>
+<row><entry>path-mtu-aging-timeout</entry><entry>24</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>path-mtu-plateau-table</entry><entry>25</entry><entry>uint16</entry><entry>true</entry></row>
+<row><entry>interface-mtu</entry><entry>26</entry><entry>uint16</entry><entry>false</entry></row>
+<row><entry>all-subnets-local</entry><entry>27</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>broadcast-address</entry><entry>28</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>perform-mask-discovery</entry><entry>29</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>mask-supplier</entry><entry>30</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>router-discovery</entry><entry>31</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>router-solicitation-address</entry><entry>32</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>static-routes</entry><entry>33</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>trailer-encapsulation</entry><entry>34</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>arp-cache-timeout</entry><entry>35</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>ieee802-3-encapsulation</entry><entry>36</entry><entry>boolean</entry><entry>false</entry></row>
+<row><entry>default-tcp-ttl</entry><entry>37</entry><entry>uint8</entry><entry>false</entry></row>
+<row><entry>tcp-keepalive-internal</entry><entry>38</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>tcp-keepalive-garbage</entry><entry>39</entry><entry>boolean</entry><entry>false</entry></row>
+
+ </tbody>
+ </tgroup>
+ </table>
+ </para>
+
+ <para>
+ <table frame="all" id="dhcp4-std-options-list-part2">
+ <title>List of standard DHCPv4 options (continued)</title>
+ <tgroup cols='4'>
+ <colspec colname='name'/>
+ <colspec colname='code'/>
+ <colspec colname='type'/>
+ <colspec colname='array'/>
+ <thead>
+ <row>
+ <entry>Name</entry>
+ <entry>Code</entry>
+ <entry>Type</entry>
+ <entry>Array?</entry>
+ </row>
+ </thead>
+ <tbody>
+
+<row><entry>nis-domain</entry><entry>40</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>nis-servers</entry><entry>41</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>ntp-servers</entry><entry>42</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>vendor-encapsulated-options</entry><entry>43</entry><entry>empty</entry><entry>false</entry></row>
+<row><entry>netbios-name-servers</entry><entry>44</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>netbios-dd-server</entry><entry>45</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>netbios-node-type</entry><entry>46</entry><entry>uint8</entry><entry>false</entry></row>
+<row><entry>netbios-scope</entry><entry>47</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>font-servers</entry><entry>48</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>x-display-manager</entry><entry>49</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>dhcp-requested-address</entry><entry>50</entry><entry>ipv4-address</entry><entry>false</entry></row>
<!-- Lease time should not be configured by a user.
-<tr><td>dhcp-lease-time</td><td>51</td><td>uint32</td><td>false</td></tr>
+<row><entry>dhcp-lease-time</entry><entry>51</entry><entry>uint32</entry><entry>false</entry></row>
-->
-<tr><td>dhcp-option-overload</td><td>52</td><td>uint8</td><td>false</td></tr>
+<row><entry>dhcp-option-overload</entry><entry>52</entry><entry>uint8</entry><entry>false</entry></row>
<!-- Message Type, Server Identifier and Parameter Request List should not be configured by a user.
-<tr><td>dhcp-message-type</td><td>53</td><td>uint8</td><td>false</td></tr>
-<tr><td>dhcp-server-identifier</td><td>54</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>dhcp-parameter-request-list</td><td>55</td><td>uint8</td><td>true</td></tr>
+<row><entry>dhcp-message-type</entry><entry>53</entry><entry>uint8</entry><entry>false</entry></row>
+<row><entry>dhcp-server-identifier</entry><entry>54</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>dhcp-parameter-request-list</entry><entry>55</entry><entry>uint8</entry><entry>true</entry></row>
-->
-<tr><td>dhcp-message</td><td>56</td><td>string</td><td>false</td></tr>
-<tr><td>dhcp-max-message-size</td><td>57</td><td>uint16</td><td>false</td></tr>
+<row><entry>dhcp-message</entry><entry>56</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>dhcp-max-message-size</entry><entry>57</entry><entry>uint16</entry><entry>false</entry></row>
<!-- Renewal and rebinding time should not be configured by a user.
-<tr><td>dhcp-renewal-time</td><td>58</td><td>uint32</td><td>false</td></tr>
-<tr><td>dhcp-rebinding-time</td><td>59</td><td>uint32</td><td>false</td></tr>
+<row><entry>dhcp-renewal-time</entry><entry>58</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>dhcp-rebinding-time</entry><entry>59</entry><entry>uint32</entry><entry>false</entry></row>
-->
-<tr><td>vendor-class-identifier</td><td>60</td><td>binary</td><td>false</td></tr>
+<row><entry>vendor-class-identifier</entry><entry>60</entry><entry>binary</entry><entry>false</entry></row>
<!-- Client identifier should not be configured by a user.
-<tr><td>dhcp-client-identifier</td><td>61</td><td>binary</td><td>false</td></tr>
+<row><entry>dhcp-client-identifier</entry><entry>61</entry><entry>binary</entry><entry>false</entry></row>
-->
-<tr><td>nwip-domain-name</td><td>62</td><td>string</td><td>false</td></tr>
-<tr><td>nwip-suboptions</td><td>63</td><td>binary</td><td>false</td></tr>
-<tr><td>user-class</td><td>77</td><td>binary</td><td>false</td></tr>
-<tr><td>fqdn</td><td>81</td><td>record</td><td>false</td></tr>
-<tr><td>dhcp-agent-options</td><td>82</td><td>empty</td><td>false</td></tr>
-<tr><td>authenticate</td><td>90</td><td>binary</td><td>false</td></tr>
-<tr><td>client-last-transaction-time</td><td>91</td><td>uint32</td><td>false</td></tr>
-<tr><td>associated-ip</td><td>92</td><td>ipv4-address</td><td>true</td></tr>
-<tr><td>subnet-selection</td><td>118</td><td>ipv4-address</td><td>false</td></tr>
-<tr><td>domain-search</td><td>119</td><td>binary</td><td>false</td></tr>
-<tr><td>vivco-suboptions</td><td>124</td><td>binary</td><td>false</td></tr>
-<tr><td>vivso-suboptions</td><td>125</td><td>binary</td><td>false</td></tr>
+<row><entry>nwip-domain-name</entry><entry>62</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>nwip-suboptions</entry><entry>63</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>tftp-server-name</entry><entry>66</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>boot-file-name</entry><entry>67</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>user-class</entry><entry>77</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>fqdn</entry><entry>81</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>dhcp-agent-options</entry><entry>82</entry><entry>empty</entry><entry>false</entry></row>
+<row><entry>authenticate</entry><entry>90</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>client-last-transaction-time</entry><entry>91</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>associated-ip</entry><entry>92</entry><entry>ipv4-address</entry><entry>true</entry></row>
+<row><entry>subnet-selection</entry><entry>118</entry><entry>ipv4-address</entry><entry>false</entry></row>
+<row><entry>domain-search</entry><entry>119</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>vivco-suboptions</entry><entry>124</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>vivso-suboptions</entry><entry>125</entry><entry>binary</entry><entry>false</entry></row>
</tbody>
+ </tgroup>
</table>
+
</para>
<para>
- <table border="1" cellpadding="5%" id="dhcp-types">
- <caption>List of standard DHCP option types</caption>
+ <table frame="all" id="dhcp-types">
+ <title>List of standard DHCP option types</title>
+ <tgroup cols='2'>
+ <colspec colname='name'/>
+ <colspec colname='meaning'/>
<thead>
- <tr><th>Name</th><th>Meaning</th></tr>
+ <row><entry>Name</entry><entry>Meaning</entry></row>
</thead>
<tbody>
- <tr><td>binary</td><td>An arbitrary string of bytes, specified as a set of hexadecimal digits.</td></tr>
- <tr><td>boolean</td><td>Boolean value with allowed values true or false</td></tr>
- <tr><td>empty</td><td>No value, data is carried in suboptions</td></tr>
- <tr><td>fqdn</td><td>Fully qualified domain name (e.g. www.example.com)</td></tr>
- <tr><td>ipv4-address</td><td>IPv4 address in the usual dotted-decimal notation (e.g. 192.0.2.1)</td></tr>
- <tr><td>ipv6-address</td><td>IPv6 address in the usual colon notation (e.g. 2001:db8::1)</td></tr>
- <tr><td>record</td><td>Structured data that may comprise any types (except "record" and "empty")</td></tr>
- <tr><td>string</td><td>Any text</td></tr>
- <tr><td>uint8</td><td>8 bit unsigned integer with allowed values 0 to 255</td></tr>
- <tr><td>uint16</td><td>16 bit unsinged integer with allowed values 0 to 65535</td></tr>
- <tr><td>uint32</td><td>32 bit unsigned integer with allowed values 0 to 4294967295</td></tr>
+ <row><entry>binary</entry><entry>An arbitrary string of bytes, specified as a set of hexadecimal digits.</entry></row>
+ <row><entry>boolean</entry><entry>Boolean value with allowed values true or false</entry></row>
+ <row><entry>empty</entry><entry>No value, data is carried in suboptions</entry></row>
+ <row><entry>fqdn</entry><entry>Fully qualified domain name (e.g. www.example.com)</entry></row>
+ <row><entry>ipv4-address</entry><entry>IPv4 address in the usual dotted-decimal notation (e.g. 192.0.2.1)</entry></row>
+ <row><entry>ipv6-address</entry><entry>IPv6 address in the usual colon notation (e.g. 2001:db8::1)</entry></row>
+ <row><entry>record</entry><entry>Structured data that may comprise any types (except "record" and "empty")</entry></row>
+ <row><entry>string</entry><entry>Any text</entry></row>
+ <row><entry>uint8</entry><entry>8 bit unsigned integer with allowed values 0 to 255</entry></row>
+ <row><entry>uint16</entry><entry>16 bit unsinged integer with allowed values 0 to 65535</entry></row>
+ <row><entry>uint32</entry><entry>32 bit unsigned integer with allowed values 0 to 4294967295</entry></row>
</tbody>
+ </tgroup>
</table>
</para>
</section>
@@ -4132,21 +4378,63 @@ Dhcp4/subnet4 [] list (default)
<para>
The DHCPv4 protocol uses a "server identifier" for clients to be able
to discriminate between several servers present on the same link: this
- value is an IPv4 address of the server. When started for the first time,
- the DHCPv4 server will choose one of its IPv4 addresses as its server-id,
- and store the chosen value to a file. That file will be read by the server
- and the contained value used whenever the server is subsequently started.
+ value is an IPv4 address of the server. The server chooses the IPv4 address
+ of the interface on which the message from the client (or relay) has been
+ received. A single server instance will use multiple server identifiers
+ if it is receiving queries on multiple interfaces.
</para>
<para>
- It is unlikely that this parameter should ever need to be changed.
- However, if such a need arises, stop the server, edit the file and restart
- the server. (The file is named b10-dhcp4-serverid and by default is
- stored in the "var" subdirectory of the directory in which BIND 10 is installed.
- This can be changed when BIND 10 is built by using "--localstatedir"
- on the "configure" command line.) The file is a text file that should
- contain an IPv4 address. Spaces are ignored, and no extra characters are allowed
- in this file.
+ Currently there is no mechanism to override the default server identifiers
+ by an administrator. In the future, the configuration mechanism will be used
+ to specify the custom server identifier.
+ </para>
+ </section>
+
+ <section id="dhcp4-next-server">
+ <title>Next server (siaddr)</title>
+ <para>In some cases, clients want to obtain configuration from the TFTP server.
+ Although there is a dedicated option for it, some devices may use siaddr field
+ in the DHCPv4 packet for that purpose. That specific field can be configured
+ using next-server directive. It is possible to define it in global scope or
+ for a given subnet only. If both are defined, subnet value takes precedence.
+ The value in subnet can be set to 0.0.0.0, which means that next-server should
+ not be sent. It may also be set to empty string, which means the same as if
+ it was not defined at all - use global value.
</para>
+
+<screen>
+> <userinput>config add Dhcp4/next-server</userinput>
+> <userinput>config set Dhcp4/next-server "192.0.2.123"</userinput>
+> <userinput>config commit</userinput>
+<userinput></userinput>
+> <userinput>config add Dhcp4/subnet[0]/next-server</userinput>
+> <userinput>config set Dhcp4/subnet[0]/next-server "192.0.2.234"</userinput>
+> <userinput>config commit</userinput>
+</screen>
+
+ </section>
+
+ <section id="dhcp4-echo-client-id">
+ <title>Echoing client-id (RFC6842)</title>
+ <para>Original DHCPv4 spec (RFC2131) states that the DHCPv4
+ server must not send back client-id options when responding to
+ clients. However, in some cases that confused clients that did
+ not have MAC address or client-id. See RFC6842 for details. That
+ behavior has changed with the publication of RFC6842 which
+ updated RFC2131. That update now states that the server must
+ send client-id if client sent it. That is the default behaviour
+ that Kea offers. However, in some cases older devices that do
+ not support RFC6842 may refuse to accept responses that include
+ client-id option. To enable backward compatibility, an optional
+ configuration parameter has been introduced. To configure it,
+ use the following commands:</para>
+
+<screen>
+> <userinput>config add Dhcp4/echo-client-id</userinput>
+> <userinput>config set Dhcp4/echo-client-id False</userinput>
+> <userinput>config commit</userinput>
+</screen>
+
</section>
<section id="dhcp4-std">
@@ -4159,11 +4447,20 @@ Dhcp4/subnet4 [] list (default)
REQUEST, RELEASE, ACK, and NAK.</simpara>
</listitem>
<listitem>
- <simpara><ulink url="http://tools.ietf.org/html/rfc2132">RFC 2132</ulink>: Supported options are: PAD (0),
+ <simpara><ulink url="http://tools.ietf.org/html/rfc2132">RFC 2132</ulink>:
+ Supported options are: PAD (0),
END(255), Message Type(53), DHCP Server Identifier (54),
Domain Name (15), DNS Servers (6), IP Address Lease Time
(51), Subnet mask (1), and Routers (3).</simpara>
</listitem>
+ <listitem>
+ <simpara><ulink url="http://tools.ietf.org/html/rfc3046">RFC 3046</ulink>:
+ Relay Agent Information option is supported.</simpara>
+ <simpara><ulink url="http://tools.ietf.org/html/rfc6842">RFC 6842</ulink>:
+ Server by default sends back client-id option. That capability may be
+ disabled. See <xref linkend="dhcp4-echo-client-id"/> for details.
+ </simpara>
+ </listitem>
</itemizedlist>
</section>
@@ -4174,6 +4471,21 @@ Dhcp4/subnet4 [] list (default)
development and should be treated as <quote>not implemented
yet</quote>, rather than actual limitations.</para>
<itemizedlist>
+ <listitem> <!-- see tickets #3234, #3281 -->
+ <para>
+ On-line configuration has some limitations. Adding new subnets or
+ modifying existing ones work, as is removing the last subnet from
+ the list. However, removing non-last (e.g. removing subnet 1,2 or 3 if
+ there are 4 subnets configured) will cause issues. The problem is
+ caused by simplistic subnet-id assignment. The subnets are always
+ numbered, starting from 1. That subnet-id is then used in leases
+ that are stored in the lease database. Removing non-last subnet will
+ cause the configuration information to mismatch data in the lease
+ database. It is possible to manually update subnet-id fields in
+ MySQL database, but it is awkward and error prone process. A better
+ reconfiguration support is planned.
+ </para>
+ </listitem>
<listitem>
<para>
On startup, the DHCPv4 server does not get the full configuration from
@@ -4186,21 +4498,6 @@ Dhcp4/renew-timer 1000 integer (default)
> <userinput>config commit</userinput></screen>
</para>
</listitem>
- <listitem>
- <simpara>During the initial IPv4 node configuration, the
- server is expected to send packets to a node that does not
- have IPv4 address assigned yet. The server requires
- certain tricks (or hacks) to transmit such packets. This
- is not implemented yet, therefore DHCPv4 server supports
- relayed traffic only (that is, normal point to point
- communication).</simpara>
- </listitem>
-
- <listitem>
- <simpara>Upon start, the server will open sockets on all
- interfaces that are not loopback, are up and running and
- have IPv4 address.</simpara>
- </listitem>
<listitem>
<simpara>The DHCPv4 server does not support
@@ -4210,7 +4507,7 @@ Dhcp4/renew-timer 1000 integer (default)
available from <ulink url="http://www.isc.org/software/dhcp"/>.</simpara>
</listitem>
<listitem>
- <simpara>Interface detection is currently working on Linux
+ <simpara>Raw sockets operation is working on Linux
only. See <xref linkend="iface-detect"/> for details.</simpara>
</listitem>
<listitem>
@@ -4281,7 +4578,7 @@ Dhcp4/renew-timer 1000 integer (default)
will be available. It will look similar to this:
<screen>
> <userinput>config show Dhcp6</userinput>
-Dhcp6/interface/ list (default)
+Dhcp6/interfaces/ list (default)
Dhcp6/renew-timer 1000 integer (default)
Dhcp6/rebind-timer 2000 integer (default)
Dhcp6/preferred-lifetime 3000 integer (default)
@@ -4374,6 +4671,96 @@ Dhcp6/subnet6/ list
</note>
</section>
+ <section id="dhcp6-interface-selection">
+ <title>Interface selection</title>
+ <para>
+ When DHCPv6 server starts up, by default it will listen to the DHCP
+ traffic and respond to it on all interfaces detected during startup.
+ However, in many cases it is desired to configure the server to listen and
+ respond on selected interfaces only. The sample commands in this section
+ show how to make interface selection using bindctl.
+ </para>
+ <para>
+ The default configuration can be presented with the following command:
+ <screen>
+> <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0] "*" string</userinput></screen>
+ An asterisk sign plays a role of the wildcard and means "listen on all interfaces".
+ </para>
+ <para>
+ In order to override the default configuration, the existing entry can be replaced
+ with the actual interface name:
+ <screen>
+> <userinput>config set Dhcp6/interfaces[0] eth1</userinput>
+> <userinput>config commit</userinput></screen>
+ Other interface names can be added on one-by-one basis:
+ <screen>
+> <userinput>config add Dhcp6/interfaces eth2</userinput>
+> <userinput>config commit</userinput></screen>
+ Configuration will now contain two interfaces which can be presented as follows:
+ <screen>
+> <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0] "eth1" string</userinput>
+<userinput>Dhcp6/interfaces[1] "eth2" string</userinput></screen>
+ When configuration gets committed, the server will start to listen on
+ eth1 and eth2 interfaces only.
+ </para>
+ <para>
+ It is possible to use wildcard interface name (asterisk) concurrently with explicit
+ interface names:
+ <screen>
+> <userinput>config add Dhcp6/interfaces *</userinput>
+> <userinput>config commit</userinput></screen>
+ This will result in the following configuration:
+ <screen>
+> <userinput>config show Dhcp6/interfaces</userinput>
+<userinput>Dhcp6/interfaces[0] "eth1" string</userinput>
+<userinput>Dhcp6/interfaces[1] "eth2" string</userinput>
+<userinput>Dhcp6/interfaces[2] "*" string</userinput></screen>
+ The presence of the wildcard name implies that server will listen on all interfaces.
+ In order to fall back to the previous configuration when server listens on eth1 and eth2:
+ <screen>
+> <userinput>config remove Dhcp6/interfaces[2]</userinput>
+> <userinput>config commit</userinput></screen>
+ </para>
+ </section>
+
+ <section id="dhcp6-unicast">
+ <title>Unicast traffic support</title>
+ <para>
+ When DHCPv6 server starts up, by default it listens to the DHCP traffic
+ sent to multicast address ff02::1:2 on each interface that it is
+ configured to listen on (see <xref linkend="dhcp6-interface-selection"/>).
+ In some cases it is useful to configure a server to handle incoming
+ traffic sent to the global unicast addresses as well. The most common
+ reason for that is to have relays send their traffic to the server
+ directly. To configure server to listen on specific unicast address, a
+ notation to specify interfaces has been extended. Interface name can be
+ optionally followed by a slash, followed by global unicast address that
+ server should listen on. That will be done in addition to normal
+ link-local binding + listening on ff02::1:2 address. The sample commands
+ listed below show how to listen on 2001:db8::1 (a global address)
+ configured on the eth1 interface.
+ </para>
+ <para>
+ <screen>
+> <userinput>config set Dhcp6/interfaces[0] eth1/2001:db8::1</userinput>
+> <userinput>config commit</userinput></screen>
+ When configuration gets committed, the server will start to listen on
+ eth1 on link-local address, mutlicast group (ff02::1:2) and 2001:db8::1.
+ </para>
+ <para>
+ It is possible to mix interface names, wildcards and interface name/addresses
+ on the Dhcp6/interface list. It is not possible to specify more than one
+ unicast address on a given interface.
+ </para>
+ <para>
+ Care should be taken to specify proper unicast addresses. The server will
+ attempt to bind to those addresses specified, without any additional checks.
+ That approach is selected on purpose, so in the software can be used to
+ communicate over uncommon addresses if the administrator desires so.
+ </para>
+ </section>
<section>
<title>Subnet and Address Pool</title>
@@ -4433,6 +4820,27 @@ Dhcp6/subnet6/ list
</para>
</section>
+ <section>
+<!-- @todo: add real meat to the prefix delegation config this is just place holder stuff -->
+ <title>Subnet and Prefix Delegation Pools</title>
+ <para>
+ Subnets may also be configured to delegate address prefixes....
+ A subnet may have one or more prefix delegation pools. Each pool has
+ a prefixed address, which is specified as a prefix and a prefix length,
+ as well as a delegated prefix length. A sample configuration is shown
+ below:
+ <screen>
+> <userinput>config add Dhcp6/subnet6</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/subnet "2001:db8:1::/64"</userinput>
+> <userinput>config show Dhcp6/subnet6[0]</userinput>
+> <userinput>config add Dhcp6/subnet6[0]/pd-pools</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/pd-pools[0]/prefix "2001:db8:1::"</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/pd-pools[0]/prefix-len 64</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/pd-pools[0]/delegated-len 96</userinput>
+> <userinput>config commit</userinput></screen>
+ </para>
+ </section>
+
<section id="dhcp6-std-options">
<title>Standard DHCPv6 options</title>
<para>
@@ -4460,7 +4868,7 @@ Dhcp6/subnet6/ list
contains information on all global options that the server is
supposed to configure in all subnets. The second line specifies
option name. For a complete list of currently supported names,
- see <xref linkend="dhcp6-std-options-list"/> below.
+ see <xref linkend="dhcp6-std-options-list"/>.
The third line specifies option code, which must match one of the
values from that
list. Line 4 specifies option space, which must always
@@ -4530,7 +4938,9 @@ Dhcp6/subnet6/ list
<para>
- Below is a list of currently supported standard DHCPv6 options. The "Name" and "Code"
+ The currently supported standard DHCPv6 options are
+ listed in <xref linkend="dhcp6-std-options-list"/>.
+ The "Name" and "Code"
are the values that should be used as a name in the option-data
structures. "Type" designates the format of the data: the meanings of
the various types is given in <xref linkend="dhcp-types"/>.
@@ -4545,63 +4955,68 @@ Dhcp6/subnet6/ list
<!-- @todo: describe record types -->
<para>
- <table border="1" cellpadding="5%" id="dhcp6-std-options-list">
- <caption>List of standard DHCPv6 options</caption>
+ <table frame="all" id="dhcp6-std-options-list">
+ <title>List of standard DHCPv6 options</title>
+ <tgroup cols='4'>
+ <colspec colname='name'/>
+ <colspec colname='code'/>
+ <colspec colname='type'/>
+ <colspec colname='array'/>
<thead>
- <tr><th>Name</th><th>Code</th><th>Type</th><th>Array?</th></tr>
- <tr></tr>
+ <row><entry>Name</entry><entry>Code</entry><entry>Type</entry><entry>Array?</entry></row>
</thead>
<tbody>
<!-- Our engine uses those options on its own, admin must not configure them on his own
-<tr><td>clientid</td><td>1</td><td>binary</td><td>false</td></tr>
-<tr><td>serverid</td><td>2</td><td>binary</td><td>false</td></tr>
-<tr><td>ia-na</td><td>3</td><td>record</td><td>false</td></tr>
-<tr><td>ia-ta</td><td>4</td><td>uint32</td><td>false</td></tr>
-<tr><td>iaaddr</td><td>5</td><td>record</td><td>false</td></tr>
-<tr><td>oro</td><td>6</td><td>uint16</td><td>true</td></tr> -->
-<tr><td>preference</td><td>7</td><td>uint8</td><td>false</td></tr>
+<row><entry>clientid</entry><entry>1</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>serverid</entry><entry>2</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>ia-na</entry><entry>3</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>ia-ta</entry><entry>4</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>iaaddr</entry><entry>5</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>oro</entry><entry>6</entry><entry>uint16</entry><entry>true</entry></row> -->
+<row><entry>preference</entry><entry>7</entry><entry>uint8</entry><entry>false</entry></row>
<!-- Our engine uses those options on its own, admin must not configure them on his own
-<tr><td>elapsed-time</td><td>8</td><td>uint16</td><td>false</td></tr>
-<tr><td>relay-msg</td><td>9</td><td>binary</td><td>false</td></tr>
-<tr><td>auth</td><td>11</td><td>binary</td><td>false</td></tr>
-<tr><td>unicast</td><td>12</td><td>ipv6-address</td><td>false</td></tr>
-<tr><td>status-code</td><td>13</td><td>record</td><td>false</td></tr>
-<tr><td>rapid-commit</td><td>14</td><td>empty</td><td>false</td></tr>
-<tr><td>user-class</td><td>15</td><td>binary</td><td>false</td></tr>
-<tr><td>vendor-class</td><td>16</td><td>record</td><td>false</td></tr>
-<tr><td>vendor-opts</td><td>17</td><td>uint32</td><td>false</td></tr>
-<tr><td>interface-id</td><td>18</td><td>binary</td><td>false</td></tr>
-<tr><td>reconf-msg</td><td>19</td><td>uint8</td><td>false</td></tr>
-<tr><td>reconf-accept</td><td>20</td><td>empty</td><td>false</td></tr> -->
-<tr><td>sip-server-dns</td><td>21</td><td>fqdn</td><td>true</td></tr>
-<tr><td>sip-server-addr</td><td>22</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>dns-servers</td><td>23</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>domain-search</td><td>24</td><td>fqdn</td><td>true</td></tr>
-<!-- <tr><td>ia-pd</td><td>25</td><td>record</td><td>false</td></tr> -->
-<!-- <tr><td>iaprefix</td><td>26</td><td>record</td><td>false</td></tr> -->
-<tr><td>nis-servers</td><td>27</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>nisp-servers</td><td>28</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>nis-domain-name</td><td>29</td><td>fqdn</td><td>true</td></tr>
-<tr><td>nisp-domain-name</td><td>30</td><td>fqdn</td><td>true</td></tr>
-<tr><td>sntp-servers</td><td>31</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>information-refresh-time</td><td>32</td><td>uint32</td><td>false</td></tr>
-<tr><td>bcmcs-server-dns</td><td>33</td><td>fqdn</td><td>true</td></tr>
-<tr><td>bcmcs-server-addr</td><td>34</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>geoconf-civic</td><td>36</td><td>record</td><td>false</td></tr>
-<tr><td>remote-id</td><td>37</td><td>record</td><td>false</td></tr>
-<tr><td>subscriber-id</td><td>38</td><td>binary</td><td>false</td></tr>
-<tr><td>client-fqdn</td><td>39</td><td>record</td><td>false</td></tr>
-<tr><td>pana-agent</td><td>40</td><td>ipv6-address</td><td>true</td></tr>
-<tr><td>new-posix-timezone</td><td>41</td><td>string</td><td>false</td></tr>
-<tr><td>new-tzdb-timezone</td><td>42</td><td>string</td><td>false</td></tr>
-<tr><td>ero</td><td>43</td><td>uint16</td><td>true</td></tr>
-<tr><td>lq-query</td><td>44</td><td>record</td><td>false</td></tr>
-<tr><td>client-data</td><td>45</td><td>empty</td><td>false</td></tr>
-<tr><td>clt-time</td><td>46</td><td>uint32</td><td>false</td></tr>
-<tr><td>lq-relay-data</td><td>47</td><td>record</td><td>false</td></tr>
-<tr><td>lq-client-link</td><td>48</td><td>ipv6-address</td><td>true</td></tr>
+<row><entry>elapsed-time</entry><entry>8</entry><entry>uint16</entry><entry>false</entry></row>
+<row><entry>relay-msg</entry><entry>9</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>auth</entry><entry>11</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>unicast</entry><entry>12</entry><entry>ipv6-address</entry><entry>false</entry></row>
+<row><entry>status-code</entry><entry>13</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>rapid-commit</entry><entry>14</entry><entry>empty</entry><entry>false</entry></row>
+<row><entry>user-class</entry><entry>15</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>vendor-class</entry><entry>16</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>vendor-opts</entry><entry>17</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>interface-id</entry><entry>18</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>reconf-msg</entry><entry>19</entry><entry>uint8</entry><entry>false</entry></row>
+<row><entry>reconf-accept</entry><entry>20</entry><entry>empty</entry><entry>false</entry></row> -->
+<row><entry>sip-server-dns</entry><entry>21</entry><entry>fqdn</entry><entry>true</entry></row>
+<row><entry>sip-server-addr</entry><entry>22</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>dns-servers</entry><entry>23</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>domain-search</entry><entry>24</entry><entry>fqdn</entry><entry>true</entry></row>
+<!-- <row><entry>ia-pd</entry><entry>25</entry><entry>record</entry><entry>false</entry></row> -->
+<!-- <row><entry>iaprefix</entry><entry>26</entry><entry>record</entry><entry>false</entry></row> -->
+<row><entry>nis-servers</entry><entry>27</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>nisp-servers</entry><entry>28</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>nis-domain-name</entry><entry>29</entry><entry>fqdn</entry><entry>true</entry></row>
+<row><entry>nisp-domain-name</entry><entry>30</entry><entry>fqdn</entry><entry>true</entry></row>
+<row><entry>sntp-servers</entry><entry>31</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>information-refresh-time</entry><entry>32</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>bcmcs-server-dns</entry><entry>33</entry><entry>fqdn</entry><entry>true</entry></row>
+<row><entry>bcmcs-server-addr</entry><entry>34</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>geoconf-civic</entry><entry>36</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>remote-id</entry><entry>37</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>subscriber-id</entry><entry>38</entry><entry>binary</entry><entry>false</entry></row>
+<row><entry>client-fqdn</entry><entry>39</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>pana-agent</entry><entry>40</entry><entry>ipv6-address</entry><entry>true</entry></row>
+<row><entry>new-posix-timezone</entry><entry>41</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>new-tzdb-timezone</entry><entry>42</entry><entry>string</entry><entry>false</entry></row>
+<row><entry>ero</entry><entry>43</entry><entry>uint16</entry><entry>true</entry></row>
+<row><entry>lq-query</entry><entry>44</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>client-data</entry><entry>45</entry><entry>empty</entry><entry>false</entry></row>
+<row><entry>clt-time</entry><entry>46</entry><entry>uint32</entry><entry>false</entry></row>
+<row><entry>lq-relay-data</entry><entry>47</entry><entry>record</entry><entry>false</entry></row>
+<row><entry>lq-client-link</entry><entry>48</entry><entry>ipv6-address</entry><entry>true</entry></row>
</tbody>
+ </tgroup>
</table>
</para>
</section>
@@ -4842,29 +5257,22 @@ should include options from the isc option space:
<section id="dhcp6-config-subnets">
<title>Subnet Selection</title>
<para>
- The DHCPv6 server may receive requests from local (connected
- to the same subnet as the server) and remote (connecting via
- relays) clients.
- <note>
- <para>
- Currently relayed DHCPv6 traffic is not supported. The server will
- only respond to local DHCPv6 requests - see <xref linkend="dhcp6-limit"/>
- </para>
- </note>
- As it may have many subnet configurations defined, it
- must select appropriate subnet for a given request. To do this, the server first
+ The DHCPv6 server may receive requests from local (connected to the
+ same subnet as the server) and remote (connecting via relays) clients.
+ As server may have many subnet configurations defined, it must select
+ appropriate subnet for a given request. To do this, the server first
checks if there is only one subnet defined and source of the packet is
- link-local. If this is the case, the server assumes that the only subnet
- defined is local and client is indeed connected to it. This check
- simplifies small deployments.
+ link-local. If this is the case, the server assumes that the only
+ subnet defined is local and client is indeed connected to it. This
+ check simplifies small deployments.
</para>
<para>
If there are two or more subnets defined, the server can not assume
which of those (if any) subnets are local. Therefore an optional
- "interface" parameter is available within a subnet definition to designate that a given subnet
- is local, i.e. reachable directly over specified interface. For example
- the server that is intended to serve a local subnet over eth0 may be configured
- as follows:
+ "interface" parameter is available within a subnet definition to
+ designate that a given subnet is local, i.e. reachable directly over
+ specified interface. For example the server that is intended to serve
+ a local subnet over eth0 may be configured as follows:
<screen>
> <userinput>config add Dhcp6/subnet6</userinput>
> <userinput>config set Dhcp6/subnet6[1]/subnet "2001:db8:beef::/48"</userinput>
@@ -4875,6 +5283,66 @@ should include options from the isc option space:
</para>
</section>
+ <section id="dhcp6-relays">
+ <title>DHCPv6 Relays</title>
+ <para>
+ A DHCPv6 server with multiple subnets defined must select the
+ appropriate subnet when it receives a request from client. For clients
+ connected via relays, two mechanisms are used:
+ </para>
+ <para>
+ The first uses the linkaddr field in the RELAY_FORW message. The name
+ of this field is somewhat misleading in that it does not contain a link-layer
+ address: instead, it holds an address (typically a global address) that is
+ used to identify a link. The DHCPv6 server checks if the address belongs
+ to a defined subnet and, if it does, that subnet is selected for the client's
+ request.
+ </para>
+ <para>
+ The second mechanism is based on interface-id options. While forwarding a client's
+ message, relays may insert an interface-id option into the message that
+ identifies the interface on the relay that received the message. (Some
+ relays allow configuration of that parameter, but it is sometimes
+ hardcoded and may range from the very simple (e.g. "vlan100") to the very cryptic:
+ one example seen on real hardware was "ISAM144|299|ipv6|nt:vp:1:110"). The
+ server can use this information to select the appropriate subnet.
+ The information is also returned to the relay which then knows the
+ interface to use to transmit the response to the client. In order for
+ this to work successfully, the relay interface IDs must be unique within
+ the network and the server configuration must match those values.
+ </para>
+ <para>
+ When configuring the DHCPv6 server, it should be noted that two
+ similarly-named parameters can be configured for a subnet:
+ <itemizedlist>
+ <listitem><simpara>
+ "interface" defines which local network interface can be used
+ to access a given subnet.
+ </simpara></listitem>
+ <listitem><simpara>
+ "interface-id" specifies the content of the interface-id option
+ used by relays to identify the interface on the relay to which
+ the response packet is sent.
+ </simpara></listitem>
+ </itemizedlist>
+ The two are mutually exclusive: a subnet cannot be both reachable locally
+ (direct traffic) and via relays (remote traffic). Specifying both is a
+ configuration error and the DHCPv6 server will refuse such a configuration.
+ </para>
+
+ <para>
+ To specify interface-id with value "vlan123", the following commands can
+ be used:
+ <screen>
+> <userinput>config add Dhcp6/subnet6</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/subnet "2001:db8:beef::/48"</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/pool [ "2001:db8:beef::/48" ]</userinput>
+> <userinput>config set Dhcp6/subnet6[0]/interface-id "vland123"</userinput>
+> <userinput>config commit</userinput>
+</screen>
+ </para>
+ </section>
+
</section>
<section id="dhcp6-serverid">
@@ -4916,6 +5384,9 @@ should include options from the isc option space:
<listitem>
<simpara><ulink url="http://tools.ietf.org/html/rfc3646">RFC 3646</ulink>: Supported option is DNS_SERVERS.</simpara>
</listitem>
+ <listitem>
+ <simpara><ulink url="http://tools.ietf.org/html/rfc4704">RFC 4704</ulink>: Supported option is CLIENT_FQDN.</simpara>
+ </listitem>
</itemizedlist>
</section>
@@ -4928,6 +5399,22 @@ should include options from the isc option space:
yet</quote>, rather than actual limitations.</para>
<itemizedlist>
+ <listitem> <!-- see tickets #3234, #3281 -->
+ <para>
+ On-line configuration has some limitations. Adding new subnets or
+ modifying existing ones work, as is removing the last subnet from
+ the list. However, removing non-last (e.g. removing subnet 1,2 or 3 if
+ there are 4 subnets configured) will cause issues. The problem is
+ caused by simplistic subnet-id assignment. The subnets are always
+ numbered, starting from 1. That subnet-id is then used in leases
+ that are stored in the lease database. Removing non-last subnet will
+ cause the configuration information to mismatch data in the lease
+ database. It is possible to manually update subnet-id fields in
+ MySQL database, but it is awkward and error prone process. A better
+ reconfiguration support is planned.
+ </para>
+ </listitem>
+
<listitem>
<para>
On startup, the DHCPv6 server does not get the full configuration from
@@ -4941,9 +5428,6 @@ Dhcp6/renew-timer 1000 integer (default)
</para>
</listitem>
<listitem>
- <simpara>Relayed traffic is not supported.</simpara>
- </listitem>
- <listitem>
<simpara>Temporary addresses are not supported.</simpara>
</listitem>
<listitem>
@@ -4956,10 +5440,6 @@ Dhcp6/renew-timer 1000 integer (default)
<listitem>
<simpara>DNS Update is not supported.</simpara>
</listitem>
- <listitem>
- <simpara>Interface detection is currently working on Linux
- only. See <xref linkend="iface-detect"/> for details.</simpara>
- </listitem>
</itemizedlist>
</section>
@@ -4994,16 +5474,17 @@ Dhcp6/renew-timer 1000 integer (default)
<!-- TODO: point to doxygen docs -->
<section id="iface-detect">
- <title>Interface detection</title>
+ <title>Interface detection and Socket handling</title>
<para>Both the DHCPv4 and DHCPv6 components share network
interface detection routines. Interface detection is
- currently only supported on Linux systems.</para>
-
- <para>For non-Linux systems, there is currently a stub
- implementation provided. The interface manager detects loopback
- interfaces only as their name (lo or lo0) can be easily predicted.
- Please contact the BIND 10 development team if you are interested
- in running DHCP components on systems other than Linux.</para>
+ currently supported on Linux, all BSD family (FreeBSD, NetBSD,
+ OpenBSD), Mac OS X and Solaris 11 systems.</para>
+
+ <para>DHCPv4 requires special raw socket processing to send and receive
+ packets from hosts that do not have IPv4 address assigned yet. Support
+ for this operation is implemented on Linux only, so it is likely that
+ DHCPv4 component will not work in certain cases on systems other than
+ Linux.</para>
</section>
<!--
@@ -5456,6 +5937,23 @@ TODO; there's a ticket to determine these levels, see #1074
If this is 0, no maximum file size is used.
</para>
+ <note>
+ <simpara>
+ Due to a limitation of the underlying logging library
+ (log4cplus), rolling over the log files (from ".1" to
+ ".2", etc) may show odd results: There can be
+ multiple small files at the timing of roll over. This
+ can happen when multiple BIND 10 processes try to roll
+ over the files simultaneously.
+ Version 1.1.0 of log4cplus solved this problem, so if
+ this or higher version of log4cplus is used to build
+ BIND 10, it shouldn't happen. Even for older versions
+ it is normally expected to happen rarely unless the log
+ messages are produced very frequently by multiple
+ different processes.
+ </simpara>
+ </note>
+
</section>
<section>
diff --git a/ext/Makefile.am b/ext/Makefile.am
new file mode 100644
index 0000000..eb16b92
--- /dev/null
+++ b/ext/Makefile.am
@@ -0,0 +1,8 @@
+SUBDIRS = . asio
+
+# As we are copying ASIO headers to the installation directory, copy across
+# the licence file as well.
+asio_datadir = $(pkgincludedir)/asio
+asio_data_DATA = LICENSE_1_0.txt
+
+EXTRA_DIST = LICENSE_1_0.txt
diff --git a/ext/asio/Makefile.am b/ext/asio/Makefile.am
new file mode 100644
index 0000000..9813ccc
--- /dev/null
+++ b/ext/asio/Makefile.am
@@ -0,0 +1,6 @@
+SUBDIRS = . asio
+
+# As we are copying across the ASIO files to the installation directory, copy
+# across the README that tells us where we got them from.
+asio_datadir = $(pkgincludedir)/asio
+asio_data_DATA = README
diff --git a/ext/asio/README b/ext/asio/README
index da12a9d..c045eae 100644
--- a/ext/asio/README
+++ b/ext/asio/README
@@ -4,7 +4,18 @@ Downloaded from http://sourceforge.net/projects/asio/files
Project page: http://think-async.com/Asio
Local modifications:
+
+Imported a kqueue bug fix from Asio 1.5.1.
+git commit e4b2c2633ebb3859286e9a4c19e97e17bcac41b3
+See
+http://sourceforge.net/p/asio/git/ci/a50b53864f77fe762f21e44a281235982dd7e733/
+
Added ASIO_DECL to a number of function definitions
git commit c32718be9f5409b6f72d98ddcd0b1ccd4c5c2293
See also the bug report at:
http://sourceforge.net/tracker/?func=detail&aid=3291113&group_id=122478&atid=694037
+
+Imported a fix from Asio 1.5.2.
+git commit cf00216570a36d2e3e688b197deea781bfbe7d8d
+See
+http://sourceforge.net/p/asio/git/ci/4820fd6f0d257a6bb554fcd1f97f170330be0448/log/?path=/asio/include/asio/detail/impl/socket_ops.ipp
diff --git a/ext/asio/asio/Makefile.am b/ext/asio/asio/Makefile.am
new file mode 100644
index 0000000..1be0573
--- /dev/null
+++ b/ext/asio/asio/Makefile.am
@@ -0,0 +1,310 @@
+SUBDIRS = .
+
+# Copy across the BIND 10 copy of ASIO to the installation directory, as some
+# header files used by user-libraries may use parts of it.
+asio_includedir = $(pkgincludedir)/asio
+nobase_asio_include_HEADERS = \
+ basic_datagram_socket.hpp \
+ basic_deadline_timer.hpp \
+ basic_io_object.hpp \
+ basic_raw_socket.hpp \
+ basic_serial_port.hpp \
+ basic_socket.hpp \
+ basic_socket_acceptor.hpp \
+ basic_socket_iostream.hpp \
+ basic_socket_streambuf.hpp \
+ basic_stream_socket.hpp \
+ basic_streambuf.hpp \
+ basic_streambuf_fwd.hpp \
+ buffer.hpp \
+ buffered_read_stream.hpp \
+ buffered_read_stream_fwd.hpp \
+ buffered_stream.hpp \
+ buffered_stream_fwd.hpp \
+ buffered_write_stream.hpp \
+ buffered_write_stream_fwd.hpp \
+ buffers_iterator.hpp \
+ completion_condition.hpp \
+ datagram_socket_service.hpp \
+ deadline_timer.hpp \
+ deadline_timer_service.hpp \
+ detail/array_fwd.hpp \
+ detail/base_from_completion_cond.hpp \
+ detail/bind_handler.hpp \
+ detail/buffer_resize_guard.hpp \
+ detail/buffer_sequence_adapter.hpp \
+ detail/buffered_stream_storage.hpp \
+ detail/call_stack.hpp \
+ detail/completion_handler.hpp \
+ detail/config.hpp \
+ detail/consuming_buffers.hpp \
+ detail/deadline_timer_service.hpp \
+ detail/descriptor_ops.hpp \
+ detail/descriptor_read_op.hpp \
+ detail/descriptor_write_op.hpp \
+ detail/dev_poll_reactor.hpp \
+ detail/dev_poll_reactor_fwd.hpp \
+ detail/epoll_reactor.hpp \
+ detail/epoll_reactor_fwd.hpp \
+ detail/event.hpp \
+ detail/eventfd_select_interrupter.hpp \
+ detail/fd_set_adapter.hpp \
+ detail/fenced_block.hpp \
+ detail/gcc_arm_fenced_block.hpp \
+ detail/gcc_fenced_block.hpp \
+ detail/gcc_hppa_fenced_block.hpp \
+ detail/gcc_sync_fenced_block.hpp \
+ detail/gcc_x86_fenced_block.hpp \
+ detail/handler_alloc_helpers.hpp \
+ detail/handler_invoke_helpers.hpp \
+ detail/hash_map.hpp \
+ detail/impl/descriptor_ops.ipp \
+ detail/impl/dev_poll_reactor.hpp \
+ detail/impl/dev_poll_reactor.ipp \
+ detail/impl/epoll_reactor.hpp \
+ detail/impl/epoll_reactor.ipp \
+ detail/impl/eventfd_select_interrupter.ipp \
+ detail/impl/kqueue_reactor.hpp \
+ detail/impl/kqueue_reactor.ipp \
+ detail/impl/pipe_select_interrupter.ipp \
+ detail/impl/posix_event.ipp \
+ detail/impl/posix_mutex.ipp \
+ detail/impl/posix_thread.ipp \
+ detail/impl/posix_tss_ptr.ipp \
+ detail/impl/reactive_descriptor_service.ipp \
+ detail/impl/reactive_serial_port_service.ipp \
+ detail/impl/reactive_socket_service_base.ipp \
+ detail/impl/resolver_service_base.ipp \
+ detail/impl/select_reactor.hpp \
+ detail/impl/select_reactor.ipp \
+ detail/impl/service_registry.hpp \
+ detail/impl/service_registry.ipp \
+ detail/impl/socket_ops.ipp \
+ detail/impl/socket_select_interrupter.ipp \
+ detail/impl/strand_service.hpp \
+ detail/impl/strand_service.ipp \
+ detail/impl/task_io_service.hpp \
+ detail/impl/task_io_service.ipp \
+ detail/impl/throw_error.ipp \
+ detail/impl/timer_queue.ipp \
+ detail/impl/timer_queue_set.ipp \
+ detail/impl/win_event.ipp \
+ detail/impl/win_iocp_handle_service.ipp \
+ detail/impl/win_iocp_io_service.hpp \
+ detail/impl/win_iocp_io_service.ipp \
+ detail/impl/win_iocp_serial_port_service.ipp \
+ detail/impl/win_iocp_socket_service_base.ipp \
+ detail/impl/win_mutex.ipp \
+ detail/impl/win_thread.ipp \
+ detail/impl/win_tss_ptr.ipp \
+ detail/impl/winsock_init.ipp \
+ detail/io_control.hpp \
+ detail/kqueue_reactor.hpp \
+ detail/kqueue_reactor_fwd.hpp \
+ detail/local_free_on_block_exit.hpp \
+ detail/macos_fenced_block.hpp \
+ detail/mutex.hpp \
+ detail/noncopyable.hpp \
+ detail/null_buffers_op.hpp \
+ detail/null_event.hpp \
+ detail/null_fenced_block.hpp \
+ detail/null_mutex.hpp \
+ detail/null_signal_blocker.hpp \
+ detail/null_thread.hpp \
+ detail/null_tss_ptr.hpp \
+ detail/object_pool.hpp \
+ detail/old_win_sdk_compat.hpp \
+ detail/op_queue.hpp \
+ detail/operation.hpp \
+ detail/pipe_select_interrupter.hpp \
+ detail/pop_options.hpp \
+ detail/posix_event.hpp \
+ detail/posix_fd_set_adapter.hpp \
+ detail/posix_mutex.hpp \
+ detail/posix_signal_blocker.hpp \
+ detail/posix_thread.hpp \
+ detail/posix_tss_ptr.hpp \
+ detail/push_options.hpp \
+ detail/reactive_descriptor_service.hpp \
+ detail/reactive_null_buffers_op.hpp \
+ detail/reactive_serial_port_service.hpp \
+ detail/reactive_socket_accept_op.hpp \
+ detail/reactive_socket_connect_op.hpp \
+ detail/reactive_socket_recv_op.hpp \
+ detail/reactive_socket_recvfrom_op.hpp \
+ detail/reactive_socket_send_op.hpp \
+ detail/reactive_socket_sendto_op.hpp \
+ detail/reactive_socket_service.hpp \
+ detail/reactive_socket_service_base.hpp \
+ detail/reactor.hpp \
+ detail/reactor_fwd.hpp \
+ detail/reactor_op.hpp \
+ detail/reactor_op_queue.hpp \
+ detail/regex_fwd.hpp \
+ detail/resolve_endpoint_op.hpp \
+ detail/resolve_op.hpp \
+ detail/resolver_service.hpp \
+ detail/resolver_service_base.hpp \
+ detail/scoped_lock.hpp \
+ detail/select_interrupter.hpp \
+ detail/select_reactor.hpp \
+ detail/select_reactor_fwd.hpp \
+ detail/service_base.hpp \
+ detail/service_id.hpp \
+ detail/service_registry.hpp \
+ detail/service_registry_fwd.hpp \
+ detail/shared_ptr.hpp \
+ detail/signal_blocker.hpp \
+ detail/signal_init.hpp \
+ detail/socket_holder.hpp \
+ detail/socket_ops.hpp \
+ detail/socket_option.hpp \
+ detail/socket_select_interrupter.hpp \
+ detail/socket_types.hpp \
+ detail/solaris_fenced_block.hpp \
+ detail/strand_service.hpp \
+ detail/task_io_service.hpp \
+ detail/task_io_service_fwd.hpp \
+ detail/task_io_service_operation.hpp \
+ detail/thread.hpp \
+ detail/throw_error.hpp \
+ detail/timer_op.hpp \
+ detail/timer_queue.hpp \
+ detail/timer_queue_base.hpp \
+ detail/timer_queue_fwd.hpp \
+ detail/timer_queue_set.hpp \
+ detail/timer_scheduler.hpp \
+ detail/timer_scheduler_fwd.hpp \
+ detail/tss_ptr.hpp \
+ detail/wait_handler.hpp \
+ detail/weak_ptr.hpp \
+ detail/win_event.hpp \
+ detail/win_fd_set_adapter.hpp \
+ detail/win_fenced_block.hpp \
+ detail/win_iocp_handle_read_op.hpp \
+ detail/win_iocp_handle_service.hpp \
+ detail/win_iocp_handle_write_op.hpp \
+ detail/win_iocp_io_service.hpp \
+ detail/win_iocp_io_service_fwd.hpp \
+ detail/win_iocp_null_buffers_op.hpp \
+ detail/win_iocp_operation.hpp \
+ detail/win_iocp_overlapped_op.hpp \
+ detail/win_iocp_overlapped_ptr.hpp \
+ detail/win_iocp_serial_port_service.hpp \
+ detail/win_iocp_socket_accept_op.hpp \
+ detail/win_iocp_socket_recv_op.hpp \
+ detail/win_iocp_socket_recvfrom_op.hpp \
+ detail/win_iocp_socket_send_op.hpp \
+ detail/win_iocp_socket_service.hpp \
+ detail/win_iocp_socket_service_base.hpp \
+ detail/win_mutex.hpp \
+ detail/win_signal_blocker.hpp \
+ detail/win_thread.hpp \
+ detail/win_tss_ptr.hpp \
+ detail/wince_thread.hpp \
+ detail/winsock_init.hpp \
+ detail/wrapped_handler.hpp \
+ error.hpp \
+ error_code.hpp \
+ handler_alloc_hook.hpp \
+ handler_invoke_hook.hpp \
+ impl/error.ipp \
+ impl/error_code.ipp \
+ impl/io_service.hpp \
+ impl/io_service.ipp \
+ impl/read.hpp \
+ impl/read.ipp \
+ impl/read_at.hpp \
+ impl/read_at.ipp \
+ impl/read_until.hpp \
+ impl/read_until.ipp \
+ impl/serial_port_base.hpp \
+ impl/serial_port_base.ipp \
+ impl/src.cpp \
+ impl/src.hpp \
+ impl/write.hpp \
+ impl/write.ipp \
+ impl/write_at.hpp \
+ impl/write_at.ipp \
+ io_service.hpp \
+ ip/address.hpp \
+ ip/address_v4.hpp \
+ ip/address_v6.hpp \
+ ip/basic_endpoint.hpp \
+ ip/basic_resolver.hpp \
+ ip/basic_resolver_entry.hpp \
+ ip/basic_resolver_iterator.hpp \
+ ip/basic_resolver_query.hpp \
+ ip/detail/endpoint.hpp \
+ ip/detail/impl/endpoint.ipp \
+ ip/detail/socket_option.hpp \
+ ip/host_name.hpp \
+ ip/icmp.hpp \
+ ip/impl/address.hpp \
+ ip/impl/address.ipp \
+ ip/impl/address_v4.hpp \
+ ip/impl/address_v4.ipp \
+ ip/impl/address_v6.hpp \
+ ip/impl/address_v6.ipp \
+ ip/impl/basic_endpoint.hpp \
+ ip/impl/host_name.ipp \
+ ip/multicast.hpp \
+ ip/resolver_query_base.hpp \
+ ip/resolver_service.hpp \
+ ip/tcp.hpp \
+ ip/udp.hpp \
+ ip/unicast.hpp \
+ ip/v6_only.hpp \
+ is_read_buffered.hpp \
+ is_write_buffered.hpp \
+ local/basic_endpoint.hpp \
+ local/connect_pair.hpp \
+ local/datagram_protocol.hpp \
+ local/detail/endpoint.hpp \
+ local/detail/impl/endpoint.ipp \
+ local/stream_protocol.hpp \
+ placeholders.hpp \
+ posix/basic_descriptor.hpp \
+ posix/basic_stream_descriptor.hpp \
+ posix/descriptor_base.hpp \
+ posix/stream_descriptor.hpp \
+ posix/stream_descriptor_service.hpp \
+ raw_socket_service.hpp \
+ read.hpp \
+ read_at.hpp \
+ read_until.hpp \
+ serial_port.hpp \
+ serial_port_base.hpp \
+ serial_port_service.hpp \
+ socket_acceptor_service.hpp \
+ socket_base.hpp \
+ ssl.hpp \
+ ssl/basic_context.hpp \
+ ssl/context.hpp \
+ ssl/context_base.hpp \
+ ssl/context_service.hpp \
+ ssl/detail/openssl_context_service.hpp \
+ ssl/detail/openssl_init.hpp \
+ ssl/detail/openssl_operation.hpp \
+ ssl/detail/openssl_stream_service.hpp \
+ ssl/detail/openssl_types.hpp \
+ ssl/stream.hpp \
+ ssl/stream_base.hpp \
+ ssl/stream_service.hpp \
+ strand.hpp \
+ stream_socket_service.hpp \
+ streambuf.hpp \
+ system_error.hpp \
+ thread.hpp \
+ time_traits.hpp \
+ version.hpp \
+ windows/basic_handle.hpp \
+ windows/basic_random_access_handle.hpp \
+ windows/basic_stream_handle.hpp \
+ windows/overlapped_ptr.hpp \
+ windows/random_access_handle.hpp \
+ windows/random_access_handle_service.hpp \
+ windows/stream_handle.hpp \
+ windows/stream_handle_service.hpp \
+ write.hpp \
+ write_at.hpp
diff --git a/m4macros/ax_boost_for_bind10.m4 b/m4macros/ax_boost_for_bind10.m4
index 577af6b..4e713c9 100644
--- a/m4macros/ax_boost_for_bind10.m4
+++ b/m4macros/ax_boost_for_bind10.m4
@@ -28,6 +28,14 @@ dnl cause build failure; otherwise set to "no"
dnl BOOST_MAPPED_FILE_CXXFLAG set to the compiler flag that would need to
dnl compile managed_mapped_file (can be empty).
dnl It is of no use if "WOULDFAIL" is yes.
+dnl BOOST_STATIC_ASSERT_WOULDFAIL set to "yes" if BOOST_STATIC_ASSERT would
+dnl cause build error; otherwise set to "no"
+
+dnl BOOST_OFFSET_PTR_OLD set to "yes" if the version of boost is older than
+dnl 1.48. Older versions of boost have a bug which
+dnl causes segfaults in offset_ptr implementation when
+dnl compiled by GCC with optimisations enabled.
+dnl See ticket no. 3025 for details.
AC_DEFUN([AX_BOOST_FOR_BIND10], [
AC_LANG_SAVE
@@ -62,6 +70,10 @@ fi
AC_CHECK_HEADERS([boost/shared_ptr.hpp boost/foreach.hpp boost/interprocess/sync/interprocess_upgradable_mutex.hpp boost/date_time/posix_time/posix_time_types.hpp boost/bind.hpp boost/function.hpp],,
AC_MSG_ERROR([Missing required header files.]))
+# clang can cause false positives with -Werror without -Qunused-arguments.
+# it can be triggered if used with ccache.
+AC_CHECK_DECL([__clang__], [CLANG_CXXFLAGS="-Qunused-arguments"], [])
+
# Detect whether Boost tries to use threads by default, and, if not,
# make it sure explicitly. In some systems the automatic detection
# may depend on preceding header files, and if inconsistency happens
@@ -78,6 +90,8 @@ AC_TRY_COMPILE([
# Boost offset_ptr is known to not compile on some platforms, depending on
# boost version, its local configuration, and compiler. Detect it.
+CXXFLAGS_SAVED="$CXXFLAGS"
+CXXFLAGS="$CXXFLAGS $CLANG_CXXFLAGS -Werror"
AC_MSG_CHECKING([Boost offset_ptr compiles])
AC_TRY_COMPILE([
#include <boost/interprocess/offset_ptr.hpp>
@@ -86,12 +100,13 @@ AC_TRY_COMPILE([
BOOST_OFFSET_PTR_WOULDFAIL=no],
[AC_MSG_RESULT(no)
BOOST_OFFSET_PTR_WOULDFAIL=yes])
+CXXFLAGS="$CXXFLAGS_SAVED"
# Detect build failure case known to happen with Boost installed via
# FreeBSD ports
if test "X$GXX" = "Xyes"; then
CXXFLAGS_SAVED="$CXXFLAGS"
- CXXFLAGS="$CXXFLAGS -Werror"
+ CXXFLAGS="$CXXFLAGS $CLANG_CXXFLAGS -Werror"
AC_MSG_CHECKING([Boost numeric_cast compiles with -Werror])
AC_TRY_COMPILE([
@@ -104,9 +119,21 @@ if test "X$GXX" = "Xyes"; then
BOOST_NUMERIC_CAST_WOULDFAIL=yes])
CXXFLAGS="$CXXFLAGS_SAVED"
+
+ AC_MSG_CHECKING([Boost rbtree is old])
+ AC_TRY_COMPILE([
+ #include <boost/version.hpp>
+ #if BOOST_VERSION < 104800
+ #error Too old
+ #endif
+ ],,[AC_MSG_RESULT(no)
+ BOOST_OFFSET_PTR_OLD=no
+ ],[AC_MSG_RESULT(yes)
+ BOOST_OFFSET_PTR_OLD=yes])
else
# This doesn't matter for non-g++
BOOST_NUMERIC_CAST_WOULDFAIL=no
+ BOOST_OFFSET_PTR_OLD=no
fi
# Boost interprocess::managed_mapped_file is highly system dependent and
@@ -117,11 +144,9 @@ BOOST_MAPPED_FILE_CXXFLAG=
CXXFLAGS_SAVED="$CXXFLAGS"
try_flags="no"
if test "X$GXX" = "Xyes"; then
- CXXFLAGS="$CXXFLAGS -Wall -Wextra -Werror"
+ CXXFLAGS="$CXXFLAGS $CLANG_CXXFLAGS -Wall -Wextra -Werror"
try_flags="$try_flags -Wno-error"
fi
-# clang can cause false positives with -Werror without -Qunused-arguments
-AC_CHECK_DECL([__clang__], [CXXFLAGS="$CXXFLAGS -Qunused-arguments"], [])
AC_MSG_CHECKING([Boost managed_mapped_file compiles])
CXXFLAGS_SAVED2="$CXXFLAGS"
@@ -129,7 +154,7 @@ for flag in $try_flags; do
if test "$flag" != no; then
BOOST_MAPPED_FILE_CXXFLAG="$flag"
fi
- CXXFLAGS="$CXXFLAGS $BOOST_MAPPED_FILE_CXXFLAG"
+ CXXFLAGS="$CXXFLAGS $CLANG_CXXFLAGS $BOOST_MAPPED_FILE_CXXFLAG"
AC_TRY_COMPILE([
#include <boost/interprocess/managed_mapped_file.hpp>
],[
@@ -146,10 +171,36 @@ if test $BOOST_MAPPED_FILE_WOULDFAIL = yes; then
AC_MSG_RESULT(no)
fi
+# BOOST_STATIC_ASSERT in versions below Boost 1.54.0 is known to result
+# in warnings with GCC 4.8. Detect it.
+AC_MSG_CHECKING([BOOST_STATIC_ASSERT compiles])
+AC_TRY_COMPILE([
+#include <boost/static_assert.hpp>
+void testfn(void) { BOOST_STATIC_ASSERT(true); }
+],,
+[AC_MSG_RESULT(yes)
+ BOOST_STATIC_ASSERT_WOULDFAIL=no],
+[AC_MSG_RESULT(no)
+ BOOST_STATIC_ASSERT_WOULDFAIL=yes])
+
CXXFLAGS="$CXXFLAGS_SAVED"
AC_SUBST(BOOST_INCLUDES)
+dnl Determine the Boost version, used mainly for config.report.
+AC_MSG_CHECKING([Boost version])
+cat > conftest.cpp << EOF
+#include <boost/version.hpp>
+AUTOCONF_BOOST_LIB_VERSION=BOOST_LIB_VERSION
+EOF
+
+BOOST_VERSION=`$CPP $CPPFLAGS conftest.cpp | grep '^AUTOCONF_BOOST_LIB_VERSION=' | $SED -e 's/^AUTOCONF_BOOST_LIB_VERSION=//' -e 's/_/./g' -e 's/"//g' 2> /dev/null`
+if test -z "$BOOST_VERSION"; then
+ BOOST_VERSION="unknown"
+fi
+$RM -f conftest.cpp
+AC_MSG_RESULT([$BOOST_VERSION])
+
CPPFLAGS="$CPPFLAGS_SAVED"
AC_LANG_RESTORE
])dnl AX_BOOST_FOR_BIND10
diff --git a/m4macros/ax_python_sqlite3.m4 b/m4macros/ax_python_sqlite3.m4
new file mode 100644
index 0000000..f4076ba
--- /dev/null
+++ b/m4macros/ax_python_sqlite3.m4
@@ -0,0 +1,17 @@
+dnl @synopsis AX_PYTHON_SQLITE3
+dnl
+dnl Test for the Python sqlite3 module used by BIND10's datasource
+dnl
+
+AC_DEFUN([AX_PYTHON_SQLITE3], [
+
+# Check for the python sqlite3 module
+AC_MSG_CHECKING(for python sqlite3 module)
+if "$PYTHON" -c 'import sqlite3' 2>/dev/null ; then
+ AC_MSG_RESULT(ok)
+else
+ AC_MSG_RESULT(missing)
+ AC_MSG_ERROR([Missing sqlite3 python module.])
+fi
+
+])dnl AX_PYTHON_SQLITE3
diff --git a/m4macros/ax_sqlite3_for_bind10.m4 b/m4macros/ax_sqlite3_for_bind10.m4
index 4eb7f94..476dcc7 100644
--- a/m4macros/ax_sqlite3_for_bind10.m4
+++ b/m4macros/ax_sqlite3_for_bind10.m4
@@ -13,8 +13,25 @@ dnl in PATH.
AC_DEFUN([AX_SQLITE3_FOR_BIND10], [
PKG_CHECK_MODULES(SQLITE, sqlite3 >= 3.3.9,
- have_sqlite="yes",
- have_sqlite="no (sqlite3 not detected)")
+ [have_sqlite="yes"
+dnl Determine the SQLite version, used mainly for config.report.
+CPPFLAGS_SAVED="$CPPFLAGS"
+CPPFLAGS="${CPPFLAGS} $SQLITE_CFLAGS"
+AC_MSG_CHECKING([SQLite version])
+cat > conftest.c << EOF
+#include <sqlite3.h>
+AUTOCONF_SQLITE_VERSION=SQLITE_VERSION
+EOF
+
+SQLITE_VERSION=`$CPP $CPPFLAGS conftest.c | grep '^AUTOCONF_SQLITE_VERSION=' | $SED -e 's/^AUTOCONF_SQLITE_VERSION=//' -e 's/"//g' 2> /dev/null`
+if test -z "$SQLITE_VERSION"; then
+ SQLITE_VERSION="unknown"
+fi
+$RM -f conftest.c
+AC_MSG_RESULT([$SQLITE_VERSION])
+
+CPPFLAGS="$CPPFLAGS_SAVED"
+ ],have_sqlite="no (sqlite3 not detected)")
# Check for sqlite3 program
AC_PATH_PROG(SQLITE3_PROGRAM, sqlite3, no)
diff --git a/src/Makefile.am b/src/Makefile.am
index 395553c..0e0109a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,4 +1,6 @@
SUBDIRS = lib bin
+# @todo hooks lib could be a configurable switch
+SUBDIRS += hooks
EXTRA_DIST = \
cppcheck-suppress.lst \
diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am
index 0b4c1ae..ea2f1b2 100644
--- a/src/bin/Makefile.am
+++ b/src/bin/Makefile.am
@@ -1,5 +1,16 @@
+if BUILD_EXPERIMENTAL_RESOLVER
+# Build resolver only with --enable-experimental-resolver
+experimental_resolver = resolver
+endif
+
SUBDIRS = bind10 bindctl cfgmgr ddns loadzone msgq cmdctl auth xfrin \
- xfrout usermgr zonemgr stats tests resolver sockcreator dhcp4 dhcp6 \
- dbutil sysinfo
+ xfrout usermgr zonemgr stats tests $(experimental_resolver) \
+ sockcreator dhcp4 dhcp6 d2 dbutil sysinfo
+
+if USE_SHARED_MEMORY
+# Build the memory manager only if we have shared memory.
+# It is useless without it.
+SUBDIRS += memmgr
+endif
check-recursive: all-recursive
diff --git a/src/bin/auth/.gitignore b/src/bin/auth/.gitignore
index c3db95b..c090cba 100644
--- a/src/bin/auth/.gitignore
+++ b/src/bin/auth/.gitignore
@@ -11,3 +11,5 @@
/gen-statisticsitems.py.pre
/statistics.cc
/statistics_items.h
+/s-genstats
+/s-messages
diff --git a/src/bin/auth/Makefile.am b/src/bin/auth/Makefile.am
index 1bb5ee9..81e477b 100644
--- a/src/bin/auth/Makefile.am
+++ b/src/bin/auth/Makefile.am
@@ -20,7 +20,7 @@ CLEANFILES = *.gcno *.gcda auth.spec spec_config.h
CLEANFILES += auth_messages.h auth_messages.cc
CLEANFILES += gen-statisticsitems.py
# auto-generated by gen-statisticsitems.py
-CLEANFILES += statistics.cc statistics_items.h b10-auth.xml tests/statistics_unittest.cc
+CLEANFILES += statistics.cc statistics_items.h b10-auth.xml tests/statistics_unittest.cc s-genstats s-messages
man_MANS = b10-auth.8
DISTCLEANFILES = $(man_MANS)
@@ -45,18 +45,24 @@ statistics_items.h: statistics_items.h.pre statistics_msg_items.def
statistics.cc: statistics.cc.pre statistics_msg_items.def
tests/statistics_unittest.cc: tests/statistics_unittest.cc.pre statistics_msg_items.def
-gen-statisticsitems.py: gen-statisticsitems.py.pre
+gen-statisticsitems.py: gen-statisticsitems.py.pre Makefile
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" gen-statisticsitems.py.pre >$@
chmod +x $@
-auth.spec b10-auth.xml statistics_items.h statistics.cc tests/statistics_unittest.cc: Makefile gen-statisticsitems.py
+auth.spec b10-auth.xml statistics_items.h statistics.cc tests/statistics_unittest.cc: s-genstats
+
+s-genstats: gen-statisticsitems.py
./gen-statisticsitems.py
+ touch $@
spec_config.h: spec_config.h.pre
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" spec_config.h.pre >$@
-auth_messages.h auth_messages.cc: auth_messages.mes
+auth_messages.h auth_messages.cc: s-messages
+
+s-messages: auth_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/auth/auth_messages.mes
+ touch $@
BUILT_SOURCES = spec_config.h auth_messages.h auth_messages.cc
# auto-generated by gen-statisticsitems.py
diff --git a/src/bin/auth/auth_messages.mes b/src/bin/auth/auth_messages.mes
index f0e9e46..e5b655a 100644
--- a/src/bin/auth/auth_messages.mes
+++ b/src/bin/auth/auth_messages.mes
@@ -145,12 +145,40 @@ reconfigure, and has now started this process.
The thread for maintaining data source clients has finished reconfiguring
the data source clients, and is now running with the new configuration.
+% AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_BAD_CLASS invalid RRclass %1 at segment update
+A memory segment update message was sent to the authoritative
+server. But the class contained there is invalid. This means that the
+system is in an inconsistent state and the authoritative server aborts
+to minimize the problem. This is likely caused by a bug in the code.
+
+% AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_ERROR error updating the memory segment: %1
+The authoritative server tried to update the memory segment, but the update
+failed. The authoritative server aborts to avoid system inconsistency. This is
+likely caused by a bug in the code.
+
+% AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_NO_DATASRC there's no data source named %2 in class %1
+The authoritative server was asked to update the memory segment of the
+given data source, but no data source by that name was found. The
+authoritative server aborts because this indicates that the system is in
+an inconsistent state. This is likely caused by a bug in the code.
+
+% AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_UNKNOWN_CLASS unknown class %1 at segment update
+A memory segment update message was sent to the authoritative
+server. The class name for which the update should happen is valid, but
+no client lists are configured for that class. The system is in an
+inconsistent state and the authoritative server aborts. This may be
+caused by a bug in the code.
+
% AUTH_DATASRC_CLIENTS_BUILDER_STARTED data source builder thread started
A separate thread for maintaining data source clients has been started.
% AUTH_DATASRC_CLIENTS_BUILDER_STOPPED data source builder thread stopped
The separate thread for maintaining data source clients has been stopped.
+% AUTH_DATASRC_CLIENTS_BUILDER_WAKE_ERR failed to wake up main thread: %1
+A low-level error happened when trying to send data to the main thread to wake
+it up. Terminating to prevent inconsistent state and possiblu hang ups.
+
% AUTH_DATASRC_CLIENTS_SHUTDOWN_ERROR error on waiting for data source builder thread: %1
This indicates that the separate thread for maintaining data source
clients had been terminated due to an uncaught exception, and the
diff --git a/src/bin/auth/auth_srv.cc b/src/bin/auth/auth_srv.cc
index 6daf400..8dd74c7 100644
--- a/src/bin/auth/auth_srv.cc
+++ b/src/bin/auth/auth_srv.cc
@@ -70,8 +70,6 @@
using namespace std;
-using boost::shared_ptr;
-
using namespace isc;
using namespace isc::cc;
using namespace isc::datasrc;
@@ -277,7 +275,7 @@ public:
AddressList listen_addresses_;
/// The TSIG keyring
- const shared_ptr<TSIGKeyRing>* keyring_;
+ const boost::shared_ptr<TSIGKeyRing>* keyring_;
/// The data source client list manager
auth::DataSrcClientsMgr datasrc_clients_mgr_;
@@ -308,6 +306,8 @@ public:
MessageAttributes& stats_attrs,
const bool done);
+ /// Are we currently subscribed to the SegmentReader group?
+ bool readers_group_subscribed_;
private:
bool xfrout_connected_;
AbstractXfroutClient& xfrout_client_;
@@ -321,8 +321,10 @@ AuthSrvImpl::AuthSrvImpl(AbstractXfroutClient& xfrout_client,
xfrin_session_(NULL),
counters_(),
keyring_(NULL),
+ datasrc_clients_mgr_(io_service_),
ddns_base_forwarder_(ddns_forwarder),
ddns_forwarder_(NULL),
+ readers_group_subscribed_(false),
xfrout_connected_(false),
xfrout_client_(xfrout_client)
{}
@@ -373,27 +375,11 @@ public:
{}
};
-// This is a derived class of \c SimpleCallback, to serve
-// as a callback in the asiolink module. It checks for queued
-// configuration messages, and executes them if found.
-class ConfigChecker : public SimpleCallback {
-public:
- ConfigChecker(AuthSrv* srv) : server_(srv) {}
- virtual void operator()(const IOMessage&) const {
- ModuleCCSession* cfg_session = server_->getConfigSession();
- if (cfg_session != NULL && cfg_session->hasQueuedMsgs()) {
- cfg_session->checkCommand();
- }
- }
-private:
- AuthSrv* server_;
-};
-
AuthSrv::AuthSrv(isc::xfr::AbstractXfroutClient& xfrout_client,
- isc::util::io::BaseSocketSessionForwarder& ddns_forwarder)
+ isc::util::io::BaseSocketSessionForwarder& ddns_forwarder) :
+ dnss_(NULL)
{
impl_ = new AuthSrvImpl(xfrout_client, ddns_forwarder);
- checkin_ = new ConfigChecker(this);
dns_lookup_ = new MessageLookup(this);
dns_answer_ = new MessageAnswer(this);
}
@@ -405,7 +391,6 @@ AuthSrv::stop() {
AuthSrv::~AuthSrv() {
delete impl_;
- delete checkin_;
delete dns_lookup_;
delete dns_answer_;
}
@@ -523,6 +508,8 @@ AuthSrv::processMessage(const IOMessage& io_message, Message& message,
return;
}
+ stats_attrs.setRequestRD(message.getHeaderFlag(Message::HEADERFLAG_RD));
+
const Opcode& opcode = message.getOpcode();
// Get opcode at this point; for all requests regardless of message body
// sanity check.
@@ -664,7 +651,7 @@ AuthSrvImpl::processNormalQuery(const IOMessage& io_message,
try {
const ConstQuestionPtr question = *message.beginQuestion();
- const shared_ptr<datasrc::ClientList>
+ const boost::shared_ptr<datasrc::ClientList>
list(datasrc_holder.findClientList(question->getClass()));
if (list) {
const RRType& qtype = question->getType();
@@ -779,7 +766,7 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
bool is_auth = false;
{
auth::DataSrcClientsMgr::Holder datasrc_holder(datasrc_clients_mgr_);
- const shared_ptr<datasrc::ClientList> dsrc_clients =
+ const boost::shared_ptr<datasrc::ClientList> dsrc_clients =
datasrc_holder.findClientList(question->getClass());
is_auth = dsrc_clients &&
dsrc_clients->find(question->getName(), true, false).exact_match_;
@@ -913,7 +900,7 @@ AuthSrv::setDNSService(isc::asiodns::DNSServiceBase& dnss) {
}
void
-AuthSrv::setTSIGKeyRing(const shared_ptr<TSIGKeyRing>* keyring) {
+AuthSrv::setTSIGKeyRing(const boost::shared_ptr<TSIGKeyRing>* keyring) {
impl_->keyring_ = keyring;
}
@@ -937,3 +924,64 @@ void
AuthSrv::setTCPRecvTimeout(size_t timeout) {
dnss_->setTCPRecvTimeout(timeout);
}
+
+namespace {
+
+bool
+hasMappedSegment(auth::DataSrcClientsMgr& mgr) {
+ auth::DataSrcClientsMgr::Holder holder(mgr);
+ const std::vector<dns::RRClass>& classes(holder.getClasses());
+ BOOST_FOREACH(const dns::RRClass& rrclass, classes) {
+ const boost::shared_ptr<datasrc::ConfigurableClientList>&
+ list(holder.findClientList(rrclass));
+ const std::vector<DataSourceStatus>& states(list->getStatus());
+ BOOST_FOREACH(const datasrc::DataSourceStatus& status, states) {
+ if (status.getSegmentState() != datasrc::SEGMENT_UNUSED &&
+ status.getSegmentType() == "mapped")
+ // We use some segment and it's not a local one, so it
+ // must be remote.
+ return true;
+ }
+ }
+ // No remote segment found in any of the lists
+ return false;
+}
+
+}
+
+void
+AuthSrv::listsReconfigured() {
+ const bool has_remote = hasMappedSegment(impl_->datasrc_clients_mgr_);
+ if (has_remote && !impl_->readers_group_subscribed_) {
+ impl_->config_session_->subscribe("SegmentReader");
+ impl_->config_session_->
+ setUnhandledCallback(boost::bind(&AuthSrv::foreignCommand, this,
+ _1, _2, _3));
+ impl_->readers_group_subscribed_ = true;
+ } else if (!has_remote && impl_->readers_group_subscribed_) {
+ impl_->config_session_->unsubscribe("SegmentReader");
+ impl_->config_session_->
+ setUnhandledCallback(isc::config::ModuleCCSession::
+ UnhandledCallback());
+ impl_->readers_group_subscribed_ = false;
+ }
+}
+
+void
+AuthSrv::reconfigureDone(ConstElementPtr params) {
+ // ACK the segment
+ impl_->config_session_->
+ groupSendMsg(isc::config::createCommand("segment_info_update_ack",
+ params), "MemMgr");
+}
+
+void
+AuthSrv::foreignCommand(const std::string& command, const std::string&,
+ const ConstElementPtr& params)
+{
+ if (command == "segment_info_update") {
+ impl_->datasrc_clients_mgr_.
+ segmentInfoUpdate(params, boost::bind(&AuthSrv::reconfigureDone,
+ this, params));
+ }
+}
diff --git a/src/bin/auth/auth_srv.h b/src/bin/auth/auth_srv.h
index b6e4b45..02d8798 100644
--- a/src/bin/auth/auth_srv.h
+++ b/src/bin/auth/auth_srv.h
@@ -192,9 +192,6 @@ public:
/// \brief Return pointer to the DNS Answer callback function
isc::asiodns::DNSAnswer* getDNSAnswerProvider() const { return (dns_answer_); }
- /// \brief Return pointer to the Checkin callback function
- isc::asiolink::SimpleCallback* getCheckinProvider() const { return (checkin_); }
-
/// \brief Return data source clients manager.
///
/// \throw None
@@ -275,9 +272,19 @@ public:
/// open forever.
void setTCPRecvTimeout(size_t timeout);
+ /// \brief Notify the authoritative server that the client lists were
+ /// reconfigured.
+ ///
+ /// This is to be called when the work thread finishes reconfiguration
+ /// of the data sources. It involeves some book keeping and asking the
+ /// memory manager for segments, if some are remotely mapped.
+ void listsReconfigured();
+
private:
+ void reconfigureDone(isc::data::ConstElementPtr request);
+ void foreignCommand(const std::string& command, const std::string&,
+ const isc::data::ConstElementPtr& params);
AuthSrvImpl* impl_;
- isc::asiolink::SimpleCallback* checkin_;
isc::asiodns::DNSLookup* dns_lookup_;
isc::asiodns::DNSAnswer* dns_answer_;
isc::asiodns::DNSServiceBase* dnss_;
diff --git a/src/bin/auth/b10-auth.xml.pre b/src/bin/auth/b10-auth.xml.pre
index db5be3e..b8e2946 100644
--- a/src/bin/auth/b10-auth.xml.pre
+++ b/src/bin/auth/b10-auth.xml.pre
@@ -20,7 +20,7 @@
<refentry>
<refentryinfo>
- <date>February 5, 2013</date>
+ <date>July 16, 2013</date>
</refentryinfo>
<refmeta>
@@ -81,8 +81,8 @@
<varlistentry>
<term><option>-v</option></term>
<listitem><para>
- Enable verbose logging mode. This enables logging of
- diagnostic messages at the maximum debug level.
+ Enable verbose logging mode. This enables logging of
+ diagnostic messages at the maximum debug level.
</para></listitem>
</varlistentry>
@@ -248,6 +248,27 @@
but remember that if there's any error related to TSIG, some
of the counted opcode may not be trustworthy.
</para>
+
+ <para>
+ The <quote>qryrecursion</quote> counter is limited to queries
+ (requests of opcode 0) even though the RD bit is not specific
+ to queries. In practice, this bit is generally just ignored for
+ other types of requests, while DNS servers behave differently
+ for queries depending on this bit. It is also known that
+ some authoritative-only servers receive a non negligible
+ number of queries with the RD bit being set, so it would be
+ of particular interest to have a specific counters for such
+ requests.
+ </para>
+
+ <para>
+ There are two request counters related to EDNS:
+ <quote>request.edns0</quote> and <quote>request.badednsver</quote>.
+ The latter is a counter of requests with unsupported EDNS version:
+ other than version 0 in the current implementation. Therefore, total
+ number of requests with EDNS is a sum of <quote>request.edns0</quote>
+ and <quote>request.badednsver</quote>.
+ </para>
</note>
</refsect1>
diff --git a/src/bin/auth/datasrc_clients_mgr.h b/src/bin/auth/datasrc_clients_mgr.h
index f75b394..c6eed31 100644
--- a/src/bin/auth/datasrc_clients_mgr.h
+++ b/src/bin/auth/datasrc_clients_mgr.h
@@ -29,6 +29,9 @@
#include <datasrc/client_list.h>
#include <datasrc/memory/zone_writer.h>
+#include <asiolink/io_service.h>
+#include <asiolink/local_socket.h>
+
#include <auth/auth_log.h>
#include <auth/datasrc_config.h>
@@ -36,11 +39,16 @@
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/noncopyable.hpp>
+#include <boost/function.hpp>
+#include <boost/foreach.hpp>
#include <exception>
#include <cassert>
+#include <cerrno>
#include <list>
#include <utility>
+#include <sys/types.h>
+#include <sys/socket.h>
namespace isc {
namespace auth {
@@ -73,17 +81,45 @@ enum CommandID {
LOADZONE, ///< Load a new version of zone into a memory,
/// the argument to the command is a map containing 'class'
/// and 'origin' elements, both should have been validated.
+ SEGMENT_INFO_UPDATE, ///< The memory manager sent an update about segments.
SHUTDOWN, ///< Shutdown the builder; no argument
NUM_COMMANDS
};
+/// \brief Callback to be called when the command is completed.
+typedef boost::function<void ()> FinishedCallback;
+
/// \brief The data type passed from DataSrcClientsMgr to
-/// DataSrcClientsBuilder.
+/// DataSrcClientsBuilder.
///
-/// The first element of the pair is the command ID, and the second element
-/// is its argument. If the command doesn't take an argument it should be
-/// a null pointer.
-typedef std::pair<CommandID, data::ConstElementPtr> Command;
+/// This just holds the data items together, no logic or protection
+/// is present here.
+struct Command {
+ /// \brief Constructor
+ ///
+ /// It just initializes the member variables of the same names
+ /// as the parameters.
+ Command(CommandID id, const data::ConstElementPtr& params,
+ const FinishedCallback& callback) :
+ id(id),
+ params(params),
+ callback(callback)
+ {}
+ /// \brief The command to execute
+ CommandID id;
+ /// \brief Argument of the command.
+ ///
+ /// If the command takes no argument, it should be null pointer.
+ ///
+ /// This may be a null pointer if the command takes no parameters.
+ data::ConstElementPtr params;
+ /// \brief A callback to be called once the command finishes.
+ ///
+ /// This may be an empty boost::function. In such case, no callback
+ /// will be called after completion.
+ FinishedCallback callback;
+};
+
} // namespace datasrc_clientmgr_internal
/// \brief Frontend to the manager object for data source clients.
@@ -113,6 +149,24 @@ private:
boost::shared_ptr<datasrc::ConfigurableClientList> >
ClientListsMap;
+ class FDGuard : boost::noncopyable {
+ public:
+ FDGuard(DataSrcClientsMgrBase *mgr) :
+ mgr_(mgr)
+ {}
+ ~FDGuard() {
+ if (mgr_->read_fd_ != -1) {
+ close(mgr_->read_fd_);
+ }
+ if (mgr_->write_fd_ != -1) {
+ close(mgr_->write_fd_);
+ }
+ }
+ private:
+ DataSrcClientsMgrBase* mgr_;
+ };
+ friend class FDGuard;
+
public:
/// \brief Thread-safe accessor to the data source client lists.
///
@@ -159,6 +213,24 @@ public:
return (it->second);
}
}
+ /// \brief Return list of classes that are present.
+ ///
+ /// Get the list of classes for which there's a client list. It is
+ /// returned in form of a vector, copied from the internals. As the
+ /// number of classes in there is expected to be small, it is not
+ /// a performance issue.
+ ///
+ /// \return The list of classes.
+ /// \throw std::bad_alloc for problems allocating the result.
+ std::vector<dns::RRClass> getClasses() const {
+ std::vector<dns::RRClass> result;
+ for (ClientListsMap::const_iterator it =
+ mgr_.clients_map_->begin(); it != mgr_.clients_map_->end();
+ ++it) {
+ result.push_back(it->first);
+ }
+ return (result);
+ }
private:
DataSrcClientsMgrBase& mgr_;
typename MutexType::Locker locker_;
@@ -176,12 +248,20 @@ public:
///
/// \throw std::bad_alloc internal memory allocation failure.
/// \throw isc::Unexpected general unexpected system errors.
- DataSrcClientsMgrBase() :
+ DataSrcClientsMgrBase(asiolink::IOService& service) :
clients_map_(new ClientListsMap),
- builder_(&command_queue_, &cond_, &queue_mutex_, &clients_map_,
- &map_mutex_),
- builder_thread_(boost::bind(&BuilderType::run, &builder_))
- {}
+ fd_guard_(new FDGuard(this)),
+ read_fd_(-1), write_fd_(-1),
+ builder_(&command_queue_, &callback_queue_, &cond_, &queue_mutex_,
+ &clients_map_, &map_mutex_, createFds()),
+ builder_thread_(boost::bind(&BuilderType::run, &builder_)),
+ wakeup_socket_(service, read_fd_)
+ {
+ // Schedule wakeups when callbacks are pushed.
+ wakeup_socket_.asyncRead(
+ boost::bind(&DataSrcClientsMgrBase::processCallbacks, this, _1),
+ buffer, 1);
+ }
/// \brief The destructor.
///
@@ -220,6 +300,7 @@ public:
AUTH_DATASRC_CLIENTS_SHUTDOWN_UNEXPECTED_ERROR);
}
+ processCallbacks(); // Any leftover callbacks
cleanup(); // see below
}
@@ -234,11 +315,18 @@ public:
/// \brief std::bad_alloc
///
/// \param config_arg The new data source configuration. Must not be NULL.
- void reconfigure(data::ConstElementPtr config_arg) {
+ /// \param callback Called once the reconfigure command completes. It is
+ /// called in the main thread (not in the work one). It should be
+ /// exceptionless.
+ void reconfigure(const data::ConstElementPtr& config_arg,
+ const datasrc_clientmgr_internal::FinishedCallback&
+ callback = datasrc_clientmgr_internal::FinishedCallback())
+ {
if (!config_arg) {
isc_throw(InvalidParameter, "Invalid null config argument");
}
- sendCommand(datasrc_clientmgr_internal::RECONFIGURE, config_arg);
+ sendCommand(datasrc_clientmgr_internal::RECONFIGURE, config_arg,
+ callback);
reconfigureHook(); // for test's customization
}
@@ -257,12 +345,18 @@ public:
/// \param args Element argument that should be a map of the form
/// { "class": "IN", "origin": "example.com" }
/// (but class is optional and will default to IN)
+ /// \param callback Called once the loadZone command completes. It
+ /// is called in the main thread, not in the work thread. It should
+ /// be exceptionless.
///
/// \exception CommandError if the args value is null, or not in
/// the expected format, or contains
/// a bad origin or class string
void
- loadZone(data::ConstElementPtr args) {
+ loadZone(const data::ConstElementPtr& args,
+ const datasrc_clientmgr_internal::FinishedCallback& callback =
+ datasrc_clientmgr_internal::FinishedCallback())
+ {
if (!args) {
isc_throw(CommandError, "loadZone argument empty");
}
@@ -303,7 +397,37 @@ public:
// implement it would be to factor out the code from
// the start of doLoadZone(), and call it here too
- sendCommand(datasrc_clientmgr_internal::LOADZONE, args);
+ sendCommand(datasrc_clientmgr_internal::LOADZONE, args, callback);
+ }
+
+ void segmentInfoUpdate(const data::ConstElementPtr& args,
+ const datasrc_clientmgr_internal::FinishedCallback&
+ callback =
+ datasrc_clientmgr_internal::FinishedCallback()) {
+ // Some minimal validation
+ if (!args) {
+ isc_throw(CommandError, "segmentInfoUpdate argument empty");
+ }
+ if (args->getType() != isc::data::Element::map) {
+ isc_throw(CommandError, "segmentInfoUpdate argument not a map");
+ }
+ const char* params[] = {
+ "data-source-name",
+ "data-source-class",
+ "segment-params",
+ NULL
+ };
+ for (const char** param = params; *param; ++param) {
+ if (!args->contains(*param)) {
+ isc_throw(CommandError,
+ "segmentInfoUpdate argument has no '" << param <<
+ "' value");
+ }
+ }
+
+
+ sendCommand(datasrc_clientmgr_internal::SEGMENT_INFO_UPDATE, args,
+ callback);
}
private:
@@ -317,30 +441,79 @@ private:
void reconfigureHook() {}
void sendCommand(datasrc_clientmgr_internal::CommandID command,
- data::ConstElementPtr arg)
+ const data::ConstElementPtr& arg,
+ const datasrc_clientmgr_internal::FinishedCallback&
+ callback = datasrc_clientmgr_internal::FinishedCallback())
{
// The lock will be held until the end of this method. Only
// push_back has to be protected, but we can avoid having an extra
// block this way.
typename MutexType::Locker locker(queue_mutex_);
command_queue_.push_back(
- datasrc_clientmgr_internal::Command(command, arg));
+ datasrc_clientmgr_internal::Command(command, arg, callback));
cond_.signal();
}
+ int createFds() {
+ int fds[2];
+ int result = socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
+ if (result != 0) {
+ isc_throw(Unexpected, "Can't create socket pair: " <<
+ strerror(errno));
+ }
+ read_fd_ = fds[0];
+ write_fd_ = fds[1];
+ return write_fd_;
+ }
+
+ void processCallbacks(const std::string& error = std::string()) {
+ // Schedule the next read.
+ wakeup_socket_.asyncRead(
+ boost::bind(&DataSrcClientsMgrBase::processCallbacks, this, _1),
+ buffer, 1);
+ if (!error.empty()) {
+ // Generally, there should be no errors (as we are the other end
+ // as well), but check just in case.
+ isc_throw(Unexpected, error);
+ }
+
+ // Steal the callbacks into local copy.
+ std::list<datasrc_clientmgr_internal::FinishedCallback> queue;
+ {
+ typename MutexType::Locker locker(queue_mutex_);
+ queue.swap(callback_queue_);
+ }
+
+ // Execute the callbacks
+ BOOST_FOREACH(const datasrc_clientmgr_internal::FinishedCallback&
+ callback, queue) {
+ callback();
+ }
+ }
+
//
// The following are shared with the builder.
//
// The list is used as a one-way queue: back-in, front-out
std::list<datasrc_clientmgr_internal::Command> command_queue_;
+ // Similar to above, for the callbacks that are ready to be called.
+ // While the command queue is for sending commands from the main thread
+ // to the work thread, this one is for the other direction. Protected
+ // by the same mutex (queue_mutex_).
+ std::list<datasrc_clientmgr_internal::FinishedCallback> callback_queue_;
CondVarType cond_; // condition variable for queue operations
MutexType queue_mutex_; // mutex to protect the queue
datasrc::ClientListMapPtr clients_map_;
// map of actual data source client objects
+ boost::scoped_ptr<FDGuard> fd_guard_; // A guard to close the fds.
+ int read_fd_, write_fd_; // Descriptors for wakeup
MutexType map_mutex_; // mutex to protect the clients map
BuilderType builder_;
ThreadType builder_thread_; // for safety this should be placed last
+ isc::asiolink::LocalSocket wakeup_socket_; // For integration of read_fd_
+ // to the asio loop
+ char buffer[1]; // Buffer for the wakeup socket.
};
namespace datasrc_clientmgr_internal {
@@ -385,12 +558,15 @@ public:
///
/// \throw None
DataSrcClientsBuilderBase(std::list<Command>* command_queue,
+ std::list<FinishedCallback>* callback_queue,
CondVarType* cond, MutexType* queue_mutex,
datasrc::ClientListMapPtr* clients_map,
- MutexType* map_mutex
+ MutexType* map_mutex,
+ int wake_fd
) :
- command_queue_(command_queue), cond_(cond), queue_mutex_(queue_mutex),
- clients_map_(clients_map), map_mutex_(map_mutex)
+ command_queue_(command_queue), callback_queue_(callback_queue),
+ cond_(cond), queue_mutex_(queue_mutex),
+ clients_map_(clients_map), map_mutex_(map_mutex), wake_fd_(wake_fd)
{}
/// \brief The main loop.
@@ -450,6 +626,44 @@ private:
}
}
+ void doSegmentUpdate(const isc::data::ConstElementPtr& arg) {
+ try {
+ const isc::dns::RRClass
+ rrclass(arg->get("data-source-class")->stringValue());
+ const std::string&
+ name(arg->get("data-source-name")->stringValue());
+ const isc::data::ConstElementPtr& segment_params =
+ arg->get("segment-params");
+ typename MutexType::Locker locker(*map_mutex_);
+ const boost::shared_ptr<isc::datasrc::ConfigurableClientList>&
+ list = (**clients_map_)[rrclass];
+ if (!list) {
+ LOG_FATAL(auth_logger,
+ AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_UNKNOWN_CLASS)
+ .arg(rrclass);
+ std::terminate();
+ }
+ if (!list->resetMemorySegment(name,
+ isc::datasrc::memory::ZoneTableSegment::READ_ONLY,
+ segment_params)) {
+ LOG_FATAL(auth_logger,
+ AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_NO_DATASRC)
+ .arg(rrclass).arg(name);
+ std::terminate();
+ }
+ } catch (const isc::dns::InvalidRRClass& irce) {
+ LOG_FATAL(auth_logger,
+ AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_BAD_CLASS)
+ .arg(arg->get("data-source-class"));
+ std::terminate();
+ } catch (const isc::Exception& e) {
+ LOG_FATAL(auth_logger,
+ AUTH_DATASRC_CLIENTS_BUILDER_SEGMENT_ERROR)
+ .arg(e.what());
+ std::terminate();
+ }
+ }
+
void doLoadZone(const isc::data::ConstElementPtr& arg);
boost::shared_ptr<datasrc::memory::ZoneWriter> getZoneWriter(
datasrc::ConfigurableClientList& client_list,
@@ -457,10 +671,12 @@ private:
// The following are shared with the manager
std::list<Command>* command_queue_;
+ std::list<FinishedCallback> *callback_queue_;
CondVarType* cond_;
MutexType* queue_mutex_;
datasrc::ClientListMapPtr* clients_map_;
MutexType* map_mutex_;
+ int wake_fd_;
};
// Shortcut typedef for normal use
@@ -494,6 +710,31 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::run() {
AUTH_DATASRC_CLIENTS_BUILDER_COMMAND_ERROR).
arg(e.what());
}
+ if (current_commands.front().callback) {
+ // Lock the queue
+ typename MutexType::Locker locker(*queue_mutex_);
+ callback_queue_->
+ push_back(current_commands.front().callback);
+ // Wake up the other end. If it would block, there are data
+ // and it'll wake anyway.
+ int result = send(wake_fd_, "w", 1, MSG_DONTWAIT);
+ if (result == -1 &&
+ (errno != EWOULDBLOCK && errno != EAGAIN)) {
+ // Note: the strerror might not be thread safe, as
+ // subsequent call to it might change the returned
+ // string. But that is unlikely and strerror_r is
+ // not portable and we are going to terminate anyway,
+ // so that's better than nothing.
+ //
+ // Also, this error handler is not tested. It should
+ // be generally impossible to happen, so it is hard
+ // to trigger in controlled way.
+ LOG_FATAL(auth_logger,
+ AUTH_DATASRC_CLIENTS_BUILDER_WAKE_ERR).
+ arg(strerror(errno));
+ std::terminate();
+ }
+ }
current_commands.pop_front();
}
}
@@ -515,23 +756,26 @@ bool
DataSrcClientsBuilderBase<MutexType, CondVarType>::handleCommand(
const Command& command)
{
- const CommandID cid = command.first;
+ const CommandID cid = command.id;
if (cid >= NUM_COMMANDS) {
// This shouldn't happen except for a bug within this file.
isc_throw(Unexpected, "internal bug: invalid command, ID: " << cid);
}
const boost::array<const char*, NUM_COMMANDS> command_desc = {
- {"NOOP", "RECONFIGURE", "LOADZONE", "SHUTDOWN"}
+ {"NOOP", "RECONFIGURE", "LOADZONE", "SEGMENT_INFO_UPDATE", "SHUTDOWN"}
};
LOG_DEBUG(auth_logger, DBGLVL_TRACE_BASIC,
AUTH_DATASRC_CLIENTS_BUILDER_COMMAND).arg(command_desc.at(cid));
- switch (command.first) {
+ switch (command.id) {
case RECONFIGURE:
- doReconfigure(command.second);
+ doReconfigure(command.params);
break;
case LOADZONE:
- doLoadZone(command.second);
+ doLoadZone(command.params);
+ break;
+ case SEGMENT_INFO_UPDATE:
+ doSegmentUpdate(command.params);
break;
case SHUTDOWN:
return (false);
@@ -623,7 +867,7 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::getZoneWriter(
datasrc::ConfigurableClientList::ZoneWriterPair writerpair;
{
typename MutexType::Locker locker(*map_mutex_);
- writerpair = client_list.getCachedZoneWriter(origin);
+ writerpair = client_list.getCachedZoneWriter(origin, false);
}
switch (writerpair.first) {
@@ -639,12 +883,21 @@ DataSrcClientsBuilderBase<MutexType, CondVarType>::getZoneWriter(
AUTH_DATASRC_CLIENTS_BUILDER_LOAD_ZONE_NOCACHE)
.arg(origin).arg(rrclass);
break; // return NULL below
+ case datasrc::ConfigurableClientList::CACHE_NOT_WRITABLE:
+ // This is an internal error. Auth server should skip reloading zones
+ // on non writable caches.
+ isc_throw(InternalCommandError, "failed to load zone " << origin
+ << "/" << rrclass << ": internal failure, in-memory cache "
+ "is not writable");
case datasrc::ConfigurableClientList::CACHE_DISABLED:
// This is an internal error. Auth server must have the cache
// enabled.
isc_throw(InternalCommandError, "failed to load zone " << origin
<< "/" << rrclass << ": internal failure, in-memory cache "
"is somehow disabled");
+ default: // other cases can really never happen
+ isc_throw(Unexpected, "Impossible result in getting data source "
+ "ZoneWriter: " << writerpair.first);
}
return (boost::shared_ptr<datasrc::memory::ZoneWriter>());
diff --git a/src/bin/auth/main.cc b/src/bin/auth/main.cc
index dc03be2..d6b1119 100644
--- a/src/bin/auth/main.cc
+++ b/src/bin/auth/main.cc
@@ -109,9 +109,11 @@ datasrcConfigHandler(AuthSrv* server, bool* first_time,
assert(config_session != NULL);
*first_time = false;
server->getDataSrcClientsMgr().reconfigure(
- config_session->getRemoteConfigValue("data_sources", "classes"));
+ config_session->getRemoteConfigValue("data_sources", "classes"),
+ boost::bind(&AuthSrv::listsReconfigured, server));
} else if (config->contains("classes")) {
- server->getDataSrcClientsMgr().reconfigure(config->get("classes"));
+ server->getDataSrcClientsMgr().reconfigure(config->get("classes"),
+ boost::bind(&AuthSrv::listsReconfigured, server));
}
}
@@ -173,12 +175,11 @@ main(int argc, char* argv[]) {
auth_server = auth_server_.get();
LOG_INFO(auth_logger, AUTH_SERVER_CREATED);
- SimpleCallback* checkin = auth_server->getCheckinProvider();
IOService& io_service = auth_server->getIOService();
DNSLookup* lookup = auth_server->getDNSLookupProvider();
DNSAnswer* answer = auth_server->getDNSAnswerProvider();
- DNSService dns_service(io_service, checkin, lookup, answer);
+ DNSService dns_service(io_service, lookup, answer);
auth_server->setDNSService(dns_service);
LOG_DEBUG(auth_logger, DBG_AUTH_START, AUTH_DNS_SERVICES_CREATED);
diff --git a/src/bin/auth/query.cc b/src/bin/auth/query.cc
index 770f624..87f8e91 100644
--- a/src/bin/auth/query.cc
+++ b/src/bin/auth/query.cc
@@ -255,7 +255,7 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
if (nsec->getRdataCount() == 0) {
isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
}
-
+
ConstZoneFinderContextPtr fcontext =
finder.find(*qname_, RRType::NSEC(),
dnssec_opt_ | ZoneFinder::NO_WILDCARD);
@@ -386,6 +386,12 @@ Query::process(datasrc::ClientList& client_list,
response_->setHeaderFlag(Message::HEADERFLAG_AA, false);
response_->setRcode(Rcode::REFUSED());
return;
+ } else if (!result.finder_) {
+ // We found a matching zone in a data source but its data are not
+ // available.
+ response_->setHeaderFlag(Message::HEADERFLAG_AA, false);
+ response_->setRcode(Rcode::SERVFAIL());
+ return;
}
ZoneFinder& zfinder = *result.finder_;
diff --git a/src/bin/auth/statistics.cc.pre b/src/bin/auth/statistics.cc.pre
index 21141b0..b5418a0 100644
--- a/src/bin/auth/statistics.cc.pre
+++ b/src/bin/auth/statistics.cc.pre
@@ -26,6 +26,8 @@
#include <boost/optional.hpp>
+#include <stdint.h>
+
using namespace isc::dns;
using namespace isc::auth;
using namespace isc::statistics;
@@ -53,8 +55,8 @@ fillNodes(const Counter& counter,
fillNodes(counter, type_tree[i].sub_counters, sub_counters);
} else {
trees->set(type_tree[i].name,
- Element::create(static_cast<long int>(
- counter.get(type_tree[i].counter_id)))
+ Element::create(static_cast<int64_t>(
+ counter.get(type_tree[i].counter_id) & 0x7fffffffffffffffLL))
);
}
}
@@ -138,7 +140,14 @@ Counters::incRequest(const MessageAttributes& msgattrs) {
// if a short message which does not contain DNS header is received, or
// a response message (i.e. QR bit is set) is received.
if (opcode) {
- server_msg_counter_.inc(opcode_to_msgcounter[opcode.get().getCode()]);
+ server_msg_counter_.inc(opcode_to_msgcounter[opcode->getCode()]);
+
+ if (opcode.get() == Opcode::QUERY()) {
+ // Recursion Desired bit
+ if (msgattrs.requestHasRD()) {
+ server_msg_counter_.inc(MSG_QRYRECURSION);
+ }
+ }
}
// TSIG
diff --git a/src/bin/auth/statistics.h b/src/bin/auth/statistics.h
index 52f9bad..2ab987d 100644
--- a/src/bin/auth/statistics.h
+++ b/src/bin/auth/statistics.h
@@ -66,6 +66,8 @@ private:
enum BitAttributes {
REQ_WITH_EDNS_0, // request with EDNS ver.0
REQ_WITH_DNSSEC_OK, // DNSSEC OK (DO) bit is set in request
+ REQ_WITH_RD, // Recursion Desired (RD) bit is set in
+ // request
REQ_TSIG_SIGNED, // request is signed with valid TSIG
REQ_BADSIG, // request is signed but bad signature
RES_IS_TRUNCATED, // response is truncated
@@ -170,6 +172,22 @@ public:
bit_attributes_[REQ_WITH_DNSSEC_OK] = with_dnssec_ok;
}
+ /// \brief Return Recursion Desired (RD) bit of the request.
+ ///
+ /// \return true if Recursion Desired (RD) bit of the request is set
+ /// \throw None
+ bool requestHasRD() const {
+ return (bit_attributes_[REQ_WITH_RD]);
+ }
+
+ /// \brief Set Recursion Desired (RD) bit of the request.
+ ///
+ /// \param with_rd true if Recursion Desired (RD)bit of the request is set
+ /// \throw None
+ void setRequestRD(const bool with_rd) {
+ bit_attributes_[REQ_WITH_RD] = with_rd;
+ }
+
/// \brief Return whether the request is TSIG signed or not.
///
/// \return true if the request is TSIG signed
diff --git a/src/bin/auth/statistics_msg_items.def b/src/bin/auth/statistics_msg_items.def
index d8d3597..05d96c9 100644
--- a/src/bin/auth/statistics_msg_items.def
+++ b/src/bin/auth/statistics_msg_items.def
@@ -31,6 +31,7 @@ qrynoauthans MSG_QRYNOAUTHANS Number of queries received by the b10-auth server
qryreferral MSG_QRYREFERRAL Number of queries received by the b10-auth server resulted in referral answer.
qrynxrrset MSG_QRYNXRRSET Number of queries received by the b10-auth server resulted in NoError and AA bit is set in the response, but the number of answer RR == 0.
authqryrej MSG_QRYREJECT Number of authoritative queries rejected by the b10-auth server.
+qryrecursion MSG_QRYRECURSION Number of queries received by the b10-auth server with "Recursion Desired" (RD) bit was set.
rcode msg_counter_rcode Rcode statistics =
noerror MSG_RCODE_NOERROR Number of requests received by the b10-auth server resulted in RCODE = 0 (NoError).
formerr MSG_RCODE_FORMERR Number of requests received by the b10-auth server resulted in RCODE = 1 (FormErr).
diff --git a/src/bin/auth/tests/auth_srv_unittest.cc b/src/bin/auth/tests/auth_srv_unittest.cc
index d63dc63..9a58692 100644
--- a/src/bin/auth/tests/auth_srv_unittest.cc
+++ b/src/bin/auth/tests/auth_srv_unittest.cc
@@ -39,6 +39,9 @@
#include <auth/statistics_items.h>
#include <auth/datasrc_config.h>
+#include <config/tests/fake_session.h>
+#include <config/ccsession.h>
+
#include <util/unittests/mock_socketsession.h>
#include <dns/tests/unittest_util.h>
#include <testutils/dnsmessage_test.h>
@@ -265,14 +268,15 @@ updateDatabase(AuthSrv& server, const char* params) {
void
updateInMemory(AuthSrv& server, const char* origin, const char* filename,
- bool with_static = true)
+ bool with_static = true, bool mapped = false)
{
string spec_txt = "{"
"\"IN\": [{"
" \"type\": \"MasterFiles\","
" \"params\": {"
" \"" + string(origin) + "\": \"" + string(filename) + "\""
- " },"
+ " }," +
+ string(mapped ? "\"cache-type\": \"mapped\"," : "") +
" \"cache-enable\": true"
"}]";
if (with_static) {
@@ -1210,6 +1214,38 @@ TEST_F(AuthSrvTest, updateWithInMemoryClient) {
opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
}
+TEST_F(AuthSrvTest, emptyZone) {
+ // Similar to the previous setup, but the configuration has an error
+ // (zone file doesn't exist) and the query should result in SERVFAIL.
+ // Here we check the rcode other header parameters, and statistics.
+
+ const ConstElementPtr config(Element::fromJSON("{"
+ "\"IN\": [{"
+ " \"type\": \"MasterFiles\","
+ " \"params\": {\"example.com\": \"nosuchfile.zone\"},"
+ " \"cache-enable\": true"
+ "}]}"));
+ installDataSrcClientLists(server, configureDataSource(config));
+ createDataFromFile("examplequery_fromWire.wire");
+ server.processMessage(*io_message, *parse_message, *response_obuffer,
+ &dnsserv);
+ EXPECT_TRUE(dnsserv.hasAnswer());
+ headerCheck(*parse_message, default_qid, Rcode::SERVFAIL(),
+ opcode.getCode(), QR_FLAG, 1, 0, 0, 0);
+
+ checkAllRcodeCountersZeroExcept(Rcode::SERVFAIL(), 1);
+ ConstElementPtr stats = server.getStatistics()->get("zones")->
+ get("_SERVER_");
+ std::map<std::string, int> expect;
+ expect["request.v4"] = 1;
+ expect["request.udp"] = 1;
+ expect["opcode.query"] = 1;
+ expect["responses"] = 1;
+ expect["qrynoauthans"] = 1;
+ expect["rcode.servfail"] = 1;
+ checkStatisticsCounters(stats, expect);
+}
+
TEST_F(AuthSrvTest, queryWithInMemoryClientNoDNSSEC) {
// In this example, we do simple check that query is handled from the
// query handler class, and confirm it returns no error and a non empty
@@ -2106,4 +2142,46 @@ TEST_F(AuthSrvTest, loadZoneCommand) {
sendCommand(server, "loadzone", args, 0);
}
+// Test that the auth server subscribes to the segment readers group when
+// there's a remotely mapped segment.
+#ifdef USE_SHARED_MEMORY
+TEST_F(AuthSrvTest, postReconfigure) {
+#else
+TEST_F(AuthSrvTest, DISABLED_postReconfigure) {
+#endif
+ FakeSession session(ElementPtr(new ListElement),
+ ElementPtr(new ListElement),
+ ElementPtr(new ListElement));
+ const string specfile(string(TEST_OWN_DATA_DIR) + "/spec.spec");
+ session.getMessages()->add(isc::config::createAnswer());
+ isc::config::ModuleCCSession mccs(specfile, session, NULL, NULL, false,
+ false);
+ server.setConfigSession(&mccs);
+ // First, no lists are there, so no reason to subscribe
+ server.listsReconfigured();
+ EXPECT_FALSE(session.haveSubscription("SegmentReader", "*"));
+ // Enable remote segment
+ updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false, true);
+ {
+ DataSrcClientsMgr &mgr(server.getDataSrcClientsMgr());
+ DataSrcClientsMgr::Holder holder(mgr);
+ EXPECT_EQ(SEGMENT_WAITING, holder.findClientList(RRClass::IN())->
+ getStatus()[0].getSegmentState());
+ }
+ server.listsReconfigured();
+ EXPECT_TRUE(session.haveSubscription("SegmentReader", "*"));
+ // Set the segment to local again
+ updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE);
+ {
+ DataSrcClientsMgr &mgr(server.getDataSrcClientsMgr());
+ DataSrcClientsMgr::Holder holder(mgr);
+ EXPECT_EQ(SEGMENT_INUSE, holder.findClientList(RRClass::IN())->
+ getStatus()[0].getSegmentState());
+ EXPECT_EQ("local", holder.findClientList(RRClass::IN())->
+ getStatus()[0].getSegmentType());
+ }
+ server.listsReconfigured();
+ EXPECT_FALSE(session.haveSubscription("SegmentReader", "*"));
+}
+
}
diff --git a/src/bin/auth/tests/datasrc_clients_builder_unittest.cc b/src/bin/auth/tests/datasrc_clients_builder_unittest.cc
index 01ac47e..6f85adb 100644
--- a/src/bin/auth/tests/datasrc_clients_builder_unittest.cc
+++ b/src/bin/auth/tests/datasrc_clients_builder_unittest.cc
@@ -12,6 +12,8 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <config.h>
+
#include <util/unittests/check_valgrind.h>
#include <dns/name.h>
@@ -34,9 +36,14 @@
#include <boost/function.hpp>
+#include <sys/types.h>
+#include <sys/socket.h>
+
#include <cstdlib>
#include <string>
#include <sstream>
+#include <cerrno>
+#include <unistd.h>
using isc::data::ConstElementPtr;
using namespace isc::dns;
@@ -52,17 +59,29 @@ protected:
DataSrcClientsBuilderTest() :
clients_map(new std::map<RRClass,
boost::shared_ptr<ConfigurableClientList> >),
- builder(&command_queue, &cond, &queue_mutex, &clients_map, &map_mutex),
+ write_end(-1), read_end(-1),
+ builder(&command_queue, &callback_queue, &cond, &queue_mutex,
+ &clients_map, &map_mutex, generateSockets()),
cond(command_queue, delayed_command_queue), rrclass(RRClass::IN()),
- shutdown_cmd(SHUTDOWN, ConstElementPtr()),
- noop_cmd(NOOP, ConstElementPtr())
+ shutdown_cmd(SHUTDOWN, ConstElementPtr(), FinishedCallback()),
+ noop_cmd(NOOP, ConstElementPtr(), FinishedCallback())
{}
+ ~ DataSrcClientsBuilderTest() {
+
+ }
+
+ void TearDown() {
+ // Some tests create this file. Delete it if it exists.
+ unlink(TEST_DATA_BUILDDIR "/test1.zone.image");
+ }
void configureZones(); // used for loadzone related tests
ClientListMapPtr clients_map; // configured clients
std::list<Command> command_queue; // test command queue
std::list<Command> delayed_command_queue; // commands available after wait
+ std::list<FinishedCallback> callback_queue; // Callbacks from commands
+ int write_end, read_end;
TestDataSrcClientsBuilder builder;
TestCondVar cond;
TestMutex queue_mutex;
@@ -70,6 +89,15 @@ protected:
const RRClass rrclass;
const Command shutdown_cmd;
const Command noop_cmd;
+private:
+ int generateSockets() {
+ int pair[2];
+ int result = socketpair(AF_UNIX, SOCK_STREAM, 0, pair);
+ assert(result == 0);
+ write_end = pair[0];
+ read_end = pair[1];
+ return write_end;
+ }
};
TEST_F(DataSrcClientsBuilderTest, runSingleCommand) {
@@ -80,6 +108,45 @@ TEST_F(DataSrcClientsBuilderTest, runSingleCommand) {
EXPECT_EQ(0, cond.wait_count); // no wait because command queue is not empty
EXPECT_EQ(1, queue_mutex.lock_count);
EXPECT_EQ(1, queue_mutex.unlock_count);
+ // No callback scheduled, none called.
+ EXPECT_TRUE(callback_queue.empty());
+ // Not woken up.
+ char c;
+ int result = recv(read_end, &c, 1, MSG_DONTWAIT);
+ EXPECT_EQ(-1, result);
+ EXPECT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK);
+}
+
+// Just to have a valid function callback to pass
+void emptyCallsback() {}
+
+// Check a command finished callback is passed
+TEST_F(DataSrcClientsBuilderTest, commandFinished) {
+ command_queue.push_back(Command(SHUTDOWN, ConstElementPtr(),
+ emptyCallsback));
+ builder.run();
+ EXPECT_EQ(0, cond.wait_count); // no wait because command queue is not empty
+ // Once for picking up data, once for putting the callback there
+ EXPECT_EQ(2, queue_mutex.lock_count);
+ EXPECT_EQ(2, queue_mutex.unlock_count);
+ // There's one callback in the queue
+ ASSERT_EQ(1, callback_queue.size());
+ // Not using EXPECT_EQ, as that produces warning in printing out the result
+ EXPECT_TRUE(emptyCallsback == callback_queue.front());
+ // And we are woken up.
+ char c;
+ int result = recv(read_end, &c, 1, MSG_DONTWAIT);
+ EXPECT_EQ(1, result);
+}
+
+// Test that low-level errors with the synchronization socket
+// (an unexpected condition) is detected and program aborted.
+TEST_F(DataSrcClientsBuilderTest, finishedCrash) {
+ command_queue.push_back(Command(SHUTDOWN, ConstElementPtr(),
+ emptyCallsback));
+ // Break the socket
+ close(write_end);
+ EXPECT_DEATH_IF_SUPPORTED({builder.run();}, "");
}
TEST_F(DataSrcClientsBuilderTest, runMultiCommands) {
@@ -136,7 +203,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
// the error handling
// A command structure we'll modify to send different commands
- Command reconfig_cmd(RECONFIGURE, ConstElementPtr());
+ Command reconfig_cmd(RECONFIGURE, ConstElementPtr(), FinishedCallback());
// Initially, no clients should be there
EXPECT_TRUE(clients_map->empty());
@@ -164,7 +231,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
"}"
);
- reconfig_cmd.second = good_config;
+ reconfig_cmd.params = good_config;
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_EQ(1, clients_map->size());
EXPECT_EQ(1, map_mutex.lock_count);
@@ -175,7 +242,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
// If a 'bad' command argument got here, the config validation should
// have failed already, but still, the handler should return true,
// and the clients_map should not be updated.
- reconfig_cmd.second = Element::create("{ \"foo\": \"bar\" }");
+ reconfig_cmd.params = Element::create("{ \"foo\": \"bar\" }");
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_EQ(working_config_clients, clients_map);
// Building failed, so map mutex should not have been locked again
@@ -183,7 +250,7 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
// The same for a configuration that has bad data for the type it
// specifies
- reconfig_cmd.second = bad_config;
+ reconfig_cmd.params = bad_config;
builder.handleCommand(reconfig_cmd);
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_EQ(working_config_clients, clients_map);
@@ -192,21 +259,21 @@ TEST_F(DataSrcClientsBuilderTest, reconfigure) {
// The same goes for an empty parameter (it should at least be
// an empty map)
- reconfig_cmd.second = ConstElementPtr();
+ reconfig_cmd.params = ConstElementPtr();
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_EQ(working_config_clients, clients_map);
EXPECT_EQ(1, map_mutex.lock_count);
// Reconfigure again with the same good clients, the result should
// be a different map than the original, but not an empty one.
- reconfig_cmd.second = good_config;
+ reconfig_cmd.params = good_config;
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_NE(working_config_clients, clients_map);
EXPECT_EQ(1, clients_map->size());
EXPECT_EQ(2, map_mutex.lock_count);
// And finally, try an empty config to disable all datasource clients
- reconfig_cmd.second = Element::createMap();
+ reconfig_cmd.params = Element::createMap();
EXPECT_TRUE(builder.handleCommand(reconfig_cmd));
EXPECT_EQ(0, clients_map->size());
EXPECT_EQ(3, map_mutex.lock_count);
@@ -222,7 +289,8 @@ TEST_F(DataSrcClientsBuilderTest, shutdown) {
TEST_F(DataSrcClientsBuilderTest, badCommand) {
// out-of-range command ID
EXPECT_THROW(builder.handleCommand(Command(NUM_COMMANDS,
- ConstElementPtr())),
+ ConstElementPtr(),
+ FinishedCallback())),
isc::Unexpected);
}
@@ -306,7 +374,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZone) {
const Command loadzone_cmd(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"test1.example\"}"));
+ " \"origin\": \"test1.example\"}"),
+ FinishedCallback());
EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
// loadZone involves two critical sections: one for getting the zone
@@ -367,7 +436,8 @@ TEST_F(DataSrcClientsBuilderTest,
// Now send the command to reload it
const Command loadzone_cmd(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"example.org\"}"));
+ " \"origin\": \"example.org\"}"),
+ FinishedCallback());
EXPECT_TRUE(builder.handleCommand(loadzone_cmd));
// And now it should be present too.
EXPECT_EQ(ZoneFinder::SUCCESS,
@@ -378,7 +448,8 @@ TEST_F(DataSrcClientsBuilderTest,
// An error case: the zone has no configuration. (note .com here)
const Command nozone_cmd(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"example.com\"}"));
+ " \"origin\": \"example.com\"}"),
+ FinishedCallback());
EXPECT_THROW(builder.handleCommand(nozone_cmd),
TestDataSrcClientsBuilder::InternalCommandError);
// The previous zone is not hurt in any way
@@ -401,11 +472,29 @@ TEST_F(DataSrcClientsBuilderTest,
builder.handleCommand(
Command(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"example.org\"}")));
+ " \"origin\": \"example.org\"}"),
+ FinishedCallback()));
// Only one mutex was needed because there was no actual reload.
EXPECT_EQ(orig_lock_count + 1, map_mutex.lock_count);
EXPECT_EQ(orig_unlock_count + 1, map_mutex.unlock_count);
+ // zone doesn't exist in the data source
+ const ConstElementPtr config_nozone(Element::fromJSON("{"
+ "\"IN\": [{"
+ " \"type\": \"sqlite3\","
+ " \"params\": {\"database_file\": \"" + test_db + "\"},"
+ " \"cache-enable\": true,"
+ " \"cache-zones\": [\"nosuchzone.example\"]"
+ "}]}"));
+ clients_map = configureDataSource(config_nozone);
+ EXPECT_THROW(
+ builder.handleCommand(
+ Command(LOADZONE, Element::fromJSON(
+ "{\"class\": \"IN\","
+ " \"origin\": \"nosuchzone.example\"}"),
+ FinishedCallback())),
+ TestDataSrcClientsBuilder::InternalCommandError);
+
// basically impossible case: in-memory cache is completely disabled.
// In this implementation of manager-builder, this should never happen,
// but it catches it like other configuration error and keeps going.
@@ -423,7 +512,8 @@ TEST_F(DataSrcClientsBuilderTest,
EXPECT_THROW(builder.handleCommand(
Command(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"example.org\"}"))),
+ " \"origin\": \"example.org\"}"),
+ FinishedCallback())),
TestDataSrcClientsBuilder::InternalCommandError);
}
@@ -436,13 +526,21 @@ TEST_F(DataSrcClientsBuilderTest, loadBrokenZone) {
// there's an error in the new zone file. reload will be rejected.
const Command loadzone_cmd(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"test1.example\"}"));
+ " \"origin\": \"test1.example\"}"),
+ FinishedCallback());
EXPECT_THROW(builder.handleCommand(loadzone_cmd),
TestDataSrcClientsBuilder::InternalCommandError);
zoneChecks(clients_map, rrclass); // zone shouldn't be replaced
}
TEST_F(DataSrcClientsBuilderTest, loadUnreadableZone) {
+ // If the test is run as the root user, it will fail as insufficient
+ // permissions will not stop the root user from using a file.
+ if (getuid() == 0) {
+ std::cerr << "Skipping test as it's run as the root user" << std::endl;
+ return;
+ }
+
configureZones();
// install the zone file as unreadable
@@ -451,7 +549,8 @@ TEST_F(DataSrcClientsBuilderTest, loadUnreadableZone) {
TEST_DATA_BUILDDIR "/test1.zone.copied"));
const Command loadzone_cmd(LOADZONE, Element::fromJSON(
"{\"class\": \"IN\","
- " \"origin\": \"test1.example\"}"));
+ " \"origin\": \"test1.example\"}"),
+ FinishedCallback());
EXPECT_THROW(builder.handleCommand(loadzone_cmd),
TestDataSrcClientsBuilder::InternalCommandError);
zoneChecks(clients_map, rrclass); // zone shouldn't be replaced
@@ -464,7 +563,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneWithoutDataSrc) {
Command(LOADZONE,
Element::fromJSON(
"{\"class\": \"IN\", "
- " \"origin\": \"test1.example\"}"))),
+ " \"origin\": \"test1.example\"}"),
+ FinishedCallback())),
TestDataSrcClientsBuilder::InternalCommandError);
}
@@ -474,7 +574,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
if (!isc::util::unittests::runningOnValgrind()) {
// null arg: this causes assertion failure
EXPECT_DEATH_IF_SUPPORTED({
- builder.handleCommand(Command(LOADZONE, ElementPtr()));
+ builder.handleCommand(Command(LOADZONE, ElementPtr(),
+ FinishedCallback()));
}, "");
}
@@ -483,7 +584,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
Command(LOADZONE,
Element::fromJSON(
"{\"origin\": \"test1.example\","
- " \"class\": \"no_such_class\"}"))),
+ " \"class\": \"no_such_class\"}"),
+ FinishedCallback())),
InvalidRRClass);
// not a string
@@ -491,7 +593,8 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
Command(LOADZONE,
Element::fromJSON(
"{\"origin\": \"test1.example\","
- " \"class\": 1}"))),
+ " \"class\": 1}"),
+ FinishedCallback())),
isc::data::TypeError);
// class or origin is missing: result in assertion failure
@@ -499,29 +602,158 @@ TEST_F(DataSrcClientsBuilderTest, loadZoneInvalidParams) {
EXPECT_DEATH_IF_SUPPORTED({
builder.handleCommand(Command(LOADZONE,
Element::fromJSON(
- "{\"class\": \"IN\"}")));
+ "{\"class\": \"IN\"}"),
+ FinishedCallback()));
}, "");
}
- // zone doesn't exist in the data source
- EXPECT_THROW(
- builder.handleCommand(
- Command(LOADZONE,
- Element::fromJSON(
- "{\"class\": \"IN\", \"origin\": \"xx\"}"))),
- TestDataSrcClientsBuilder::InternalCommandError);
-
// origin is bogus
EXPECT_THROW(builder.handleCommand(
Command(LOADZONE,
Element::fromJSON(
- "{\"class\": \"IN\", \"origin\": \"...\"}"))),
+ "{\"class\": \"IN\", \"origin\": \"...\"}"),
+ FinishedCallback())),
EmptyLabel);
EXPECT_THROW(builder.handleCommand(
Command(LOADZONE,
Element::fromJSON(
- "{\"origin\": 10, \"class\": 1}"))),
+ "{\"origin\": 10, \"class\": 1}"),
+ FinishedCallback())),
isc::data::TypeError);
}
+// This works only if mapped memory segment is compiled.
+// Note also that this test case may fail as we make b10-auth more aware
+// of shared-memory cache.
+TEST_F(DataSrcClientsBuilderTest,
+#ifdef USE_SHARED_MEMORY
+ loadInNonWritableCache
+#else
+ DISABLED_loadInNonWritableCache
+#endif
+ )
+{
+ const ConstElementPtr config = Element::fromJSON(
+ "{"
+ "\"IN\": [{"
+ " \"type\": \"MasterFiles\","
+ " \"params\": {"
+ " \"test1.example\": \"" +
+ std::string(TEST_DATA_BUILDDIR "/test1.zone.copied") + "\"},"
+ " \"cache-enable\": true,"
+ " \"cache-type\": \"mapped\""
+ "}]}");
+ clients_map = configureDataSource(config);
+
+ EXPECT_THROW(builder.handleCommand(
+ Command(LOADZONE,
+ Element::fromJSON(
+ "{\"origin\": \"test1.example\","
+ " \"class\": \"IN\"}"),
+ FinishedCallback())),
+ TestDataSrcClientsBuilder::InternalCommandError);
+}
+
+// Test the SEGMENT_INFO_UPDATE command. This test is little bit
+// indirect. It doesn't seem possible to fake the client list inside
+// easily. So we create a real image to load and load it. Then we check
+// the segment is used.
+TEST_F(DataSrcClientsBuilderTest,
+#ifdef USE_SHARED_MEMORY
+ segmentInfoUpdate
+#else
+ DISABLED_segmentInfoUpdate
+#endif
+ )
+{
+ // First, prepare the file image to be mapped
+ const ConstElementPtr config = Element::fromJSON(
+ "{"
+ "\"IN\": [{"
+ " \"type\": \"MasterFiles\","
+ " \"params\": {"
+ " \"test1.example\": \""
+ TEST_DATA_BUILDDIR "/test1.zone.copied\"},"
+ " \"cache-enable\": true,"
+ " \"cache-type\": \"mapped\""
+ "}]}");
+ const ConstElementPtr segment_config = Element::fromJSON(
+ "{"
+ " \"mapped-file\": \""
+ TEST_DATA_BUILDDIR "/test1.zone.image" "\"}");
+ clients_map = configureDataSource(config);
+ {
+ const boost::shared_ptr<ConfigurableClientList> list =
+ (*clients_map)[RRClass::IN()];
+ list->resetMemorySegment("MasterFiles",
+ memory::ZoneTableSegment::CREATE,
+ segment_config);
+ const ConfigurableClientList::ZoneWriterPair result =
+ list->getCachedZoneWriter(isc::dns::Name("test1.example"), false,
+ "MasterFiles");
+ ASSERT_EQ(ConfigurableClientList::ZONE_SUCCESS, result.first);
+ result.second->load();
+ result.second->install();
+ // not absolutely necessary, but just in case
+ result.second->cleanup();
+ } // Release this list. That will release the file with the image too,
+ // so we can map it read only from somewhere else.
+
+ // Create a new map, with the same configuration, but without the segments
+ // set
+ clients_map = configureDataSource(config);
+ const boost::shared_ptr<ConfigurableClientList> list =
+ (*clients_map)[RRClass::IN()];
+ EXPECT_EQ(SEGMENT_WAITING, list->getStatus()[0].getSegmentState());
+ // Send the command
+ const ElementPtr command_args = Element::fromJSON(
+ "{"
+ " \"data-source-name\": \"MasterFiles\","
+ " \"data-source-class\": \"IN\""
+ "}");
+ command_args->set("segment-params", segment_config);
+ builder.handleCommand(Command(SEGMENT_INFO_UPDATE, command_args,
+ FinishedCallback()));
+ // The segment is now used.
+ EXPECT_EQ(SEGMENT_INUSE, list->getStatus()[0].getSegmentState());
+
+ // Some invalid inputs (wrong class, different name of data source, etc).
+
+ // Copy the confing and modify
+ const ElementPtr bad_name = Element::fromJSON(command_args->toWire());
+ // Set bad name
+ bad_name->set("data-source-name", Element::create("bad"));
+ EXPECT_DEATH_IF_SUPPORTED({
+ builder.handleCommand(Command(SEGMENT_INFO_UPDATE, bad_name,
+ FinishedCallback()));
+ }, "");
+
+ // Another copy with wrong class
+ const ElementPtr bad_class = Element::fromJSON(command_args->toWire());
+ // Set bad class
+ bad_class->set("data-source-class", Element::create("bad"));
+ // Aborts (we are out of sync somehow).
+ EXPECT_DEATH_IF_SUPPORTED({
+ builder.handleCommand(Command(SEGMENT_INFO_UPDATE, bad_class,
+ FinishedCallback()));
+ }, "");
+
+ // Class CH is valid, but not present.
+ bad_class->set("data-source-class", Element::create("CH"));
+ EXPECT_DEATH_IF_SUPPORTED({
+ builder.handleCommand(Command(SEGMENT_INFO_UPDATE, bad_class,
+ FinishedCallback()));
+ }, "");
+
+ // And break the segment params
+ const ElementPtr bad_params = Element::fromJSON(command_args->toWire());
+ bad_params->set("segment-params",
+ Element::fromJSON("{\"mapped-file\": \"/bad/file\"}"));
+
+ EXPECT_DEATH_IF_SUPPORTED({
+ builder.handleCommand(Command(SEGMENT_INFO_UPDATE, bad_class,
+ FinishedCallback()));
+ }, "");
+}
+
} // unnamed namespace
diff --git a/src/bin/auth/tests/datasrc_clients_mgr_unittest.cc b/src/bin/auth/tests/datasrc_clients_mgr_unittest.cc
index c37ef11..565deb5 100644
--- a/src/bin/auth/tests/datasrc_clients_mgr_unittest.cc
+++ b/src/bin/auth/tests/datasrc_clients_mgr_unittest.cc
@@ -38,13 +38,13 @@ void
shutdownCheck() {
// Check for common points on shutdown. The manager should have acquired
// the lock, put a SHUTDOWN command to the queue, and should have signaled
- // the builder.
- EXPECT_EQ(1, FakeDataSrcClientsBuilder::queue_mutex->lock_count);
+ // the builder. It should check again for the callback queue, with the lock
+ EXPECT_EQ(2, FakeDataSrcClientsBuilder::queue_mutex->lock_count);
EXPECT_EQ(1, FakeDataSrcClientsBuilder::cond->signal_count);
EXPECT_EQ(1, FakeDataSrcClientsBuilder::command_queue->size());
const Command& cmd = FakeDataSrcClientsBuilder::command_queue->front();
- EXPECT_EQ(SHUTDOWN, cmd.first);
- EXPECT_FALSE(cmd.second); // no argument
+ EXPECT_EQ(SHUTDOWN, cmd.id);
+ EXPECT_FALSE(cmd.params); // no argument
// Finally, the manager should wait for the thread to terminate.
EXPECT_TRUE(FakeDataSrcClientsBuilder::thread_waited);
@@ -130,8 +130,8 @@ TEST(DataSrcClientsMgrTest, reconfigure) {
// touch or refer to the map, so it shouldn't acquire the map lock.
checkSharedMembers(1, 1, 0, 0, 1, 1);
const Command& cmd1 = FakeDataSrcClientsBuilder::command_queue->front();
- EXPECT_EQ(RECONFIGURE, cmd1.first);
- EXPECT_EQ(reconfigure_arg, cmd1.second);
+ EXPECT_EQ(RECONFIGURE, cmd1.id);
+ EXPECT_EQ(reconfigure_arg, cmd1.params);
// Non-null, but semantically invalid argument. The manager doesn't do
// this check, so it should result in the same effect.
@@ -140,8 +140,8 @@ TEST(DataSrcClientsMgrTest, reconfigure) {
mgr.reconfigure(reconfigure_arg);
checkSharedMembers(2, 2, 0, 0, 2, 1);
const Command& cmd2 = FakeDataSrcClientsBuilder::command_queue->front();
- EXPECT_EQ(RECONFIGURE, cmd2.first);
- EXPECT_EQ(reconfigure_arg, cmd2.second);
+ EXPECT_EQ(RECONFIGURE, cmd2.id);
+ EXPECT_EQ(reconfigure_arg, cmd2.params);
// Passing NULL argument is immediately rejected
EXPECT_THROW(mgr.reconfigure(ConstElementPtr()), isc::InvalidParameter);
@@ -156,6 +156,7 @@ TEST(DataSrcClientsMgrTest, holder) {
TestDataSrcClientsMgr::Holder holder(mgr);
EXPECT_FALSE(holder.findClientList(RRClass::IN()));
EXPECT_FALSE(holder.findClientList(RRClass::CH()));
+ EXPECT_TRUE(holder.getClasses().empty());
// map should be protected here
EXPECT_EQ(1, FakeDataSrcClientsBuilder::map_mutex->lock_count);
EXPECT_EQ(0, FakeDataSrcClientsBuilder::map_mutex->unlock_count);
@@ -174,6 +175,7 @@ TEST(DataSrcClientsMgrTest, holder) {
TestDataSrcClientsMgr::Holder holder(mgr);
EXPECT_TRUE(holder.findClientList(RRClass::IN()));
EXPECT_TRUE(holder.findClientList(RRClass::CH()));
+ EXPECT_EQ(2, holder.getClasses().size());
}
// We need to clear command queue by hand
FakeDataSrcClientsBuilder::command_queue->clear();
@@ -188,6 +190,7 @@ TEST(DataSrcClientsMgrTest, holder) {
TestDataSrcClientsMgr::Holder holder(mgr);
EXPECT_TRUE(holder.findClientList(RRClass::IN()));
EXPECT_FALSE(holder.findClientList(RRClass::CH()));
+ EXPECT_EQ(RRClass::IN(), holder.getClasses()[0]);
}
// Duplicate lock acquisition is prohibited (only test mgr can detect
@@ -245,10 +248,76 @@ TEST(DataSrcClientsMgrTest, reload) {
EXPECT_EQ(3, FakeDataSrcClientsBuilder::command_queue->size());
}
+TEST(DataSrcClientsMgrTest, segmentUpdate) {
+ TestDataSrcClientsMgr mgr;
+ EXPECT_TRUE(FakeDataSrcClientsBuilder::started);
+ EXPECT_TRUE(FakeDataSrcClientsBuilder::command_queue->empty());
+
+ isc::data::ElementPtr args =
+ isc::data::Element::fromJSON("{\"data-source-class\": \"IN\","
+ " \"data-source-name\": \"sqlite3\","
+ " \"segment-params\": {}}");
+ mgr.segmentInfoUpdate(args);
+ EXPECT_EQ(1, FakeDataSrcClientsBuilder::command_queue->size());
+ // Some invalid inputs
+ EXPECT_THROW(mgr.segmentInfoUpdate(isc::data::Element::fromJSON(
+ "{\"data-source-class\": \"IN\","
+ " \"data-source-name\": \"sqlite3\"}")), CommandError);
+ EXPECT_THROW(mgr.segmentInfoUpdate(isc::data::Element::fromJSON(
+ "{\"data-source-name\": \"sqlite3\","
+ " \"segment-params\": {}}")), CommandError);
+ EXPECT_THROW(mgr.segmentInfoUpdate(isc::data::Element::fromJSON(
+ "{\"data-source-class\": \"IN\","
+ " \"segment-params\": {}}")), CommandError);
+ EXPECT_EQ(1, FakeDataSrcClientsBuilder::command_queue->size());
+}
+
+void
+callback(bool* called, int *tag_target, int tag_value) {
+ *called = true;
+ *tag_target = tag_value;
+}
+
+// Test we can wake up the main thread by writing to the file descriptor and
+// that the callbacks are executed and removed when woken up.
+TEST(DataSrcClientsMgrTest, wakeup) {
+ bool called = false;
+ int tag;
+ {
+ TestDataSrcClientsMgr mgr;
+ // There's some real file descriptor (or something that looks so)
+ ASSERT_GT(FakeDataSrcClientsBuilder::wakeup_fd, 0);
+ // Push a callback in and wake the manager
+ FakeDataSrcClientsBuilder::callback_queue->
+ push_back(boost::bind(callback, &called, &tag, 1));
+ EXPECT_EQ(1, write(FakeDataSrcClientsBuilder::wakeup_fd, "w", 1));
+ mgr.run_one();
+ EXPECT_TRUE(called);
+ EXPECT_EQ(1, tag);
+ EXPECT_TRUE(FakeDataSrcClientsBuilder::callback_queue->empty());
+
+ called = false;
+ // If we wake up and don't push anything, it doesn't break.
+ EXPECT_EQ(1, write(FakeDataSrcClientsBuilder::wakeup_fd, "w", 1));
+ mgr.run_one();
+ EXPECT_FALSE(called);
+
+ // When we terminate, it should process whatever is left
+ // of the callbacks. So push and terminate (and don't directly
+ // wake).
+ FakeDataSrcClientsBuilder::callback_queue->
+ push_back(boost::bind(callback, &called, &tag, 2));
+ }
+ EXPECT_TRUE(called);
+ EXPECT_EQ(2, tag);
+ EXPECT_TRUE(FakeDataSrcClientsBuilder::callback_queue->empty());
+}
+
TEST(DataSrcClientsMgrTest, realThread) {
// Using the non-test definition with a real thread. Just checking
// no disruption happens.
- DataSrcClientsMgr mgr;
+ isc::asiolink::IOService service;
+ DataSrcClientsMgr mgr(service);
}
} // unnamed namespace
diff --git a/src/bin/auth/tests/datasrc_config_unittest.cc b/src/bin/auth/tests/datasrc_config_unittest.cc
index b555aa6..ca7e86e 100644
--- a/src/bin/auth/tests/datasrc_config_unittest.cc
+++ b/src/bin/auth/tests/datasrc_config_unittest.cc
@@ -30,7 +30,6 @@ using namespace isc::config;
using namespace isc::data;
using namespace isc::dns;
using namespace std;
-using namespace boost;
namespace {
@@ -57,7 +56,7 @@ private:
ConstElementPtr configuration_;
};
-typedef shared_ptr<FakeList> ListPtr;
+typedef boost::shared_ptr<FakeList> ListPtr;
// Forward declaration. We need precise definition of DatasrcConfigTest
// to complete this function.
@@ -77,8 +76,8 @@ datasrcConfigHandler(DatasrcConfigTest* fake_server, const std::string&,
class DatasrcConfigTest : public ::testing::Test {
public:
- void setDataSrcClientLists(shared_ptr<std::map<dns::RRClass, ListPtr> >
- new_lists)
+ void setDataSrcClientLists(boost::shared_ptr<std::map<dns::RRClass,
+ ListPtr> > new_lists)
{
lists_.clear(); // first empty it
@@ -159,7 +158,7 @@ testConfigureDataSource(DatasrcConfigTest& test,
{
// We use customized (faked lists) for the List type. This makes it
// possible to easily look that they were called.
- shared_ptr<std::map<dns::RRClass, ListPtr> > lists =
+ boost::shared_ptr<std::map<dns::RRClass, ListPtr> > lists =
configureDataSourceGeneric<FakeList>(config);
test.setDataSrcClientLists(lists);
}
diff --git a/src/bin/auth/tests/query_unittest.cc b/src/bin/auth/tests/query_unittest.cc
index 9bf1358..f374a87 100644
--- a/src/bin/auth/tests/query_unittest.cc
+++ b/src/bin/auth/tests/query_unittest.cc
@@ -80,6 +80,12 @@ public:
return (FindResult());
}
}
+ virtual ConstZoneTableAccessorPtr
+ getZoneTableAccessor(const std::string&, bool) const {
+ isc_throw(isc::NotImplemented,
+ "getZoneTableAccessor not implemented for SingletonList");
+ }
+
private:
DataSourceClient& client_;
};
@@ -133,6 +139,12 @@ const char* const unsigned_delegation_nsec3_txt =
"q81r598950igr1eqvc60aedlq66425b5.example.com. 3600 IN NSEC3 1 1 12 "
"aabbccdd 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom NS RRSIG\n";
+// Name of an "empty" zone: used to simulate the case of
+// configured-but-available zone (due to load errors, etc).
+// Each tested data source client is expected to have this zone (SQLite3
+// currently doesn't have this concept so it's skipped)
+const char* const EMPTY_ZONE_NAME = "empty.example.org";
+
// A helper function that generates a textual representation of RRSIG RDATA
// for the given covered type. The resulting RRSIG may not necessarily make
// sense in terms of the DNSSEC protocol, but for our testing purposes it's
@@ -799,11 +811,14 @@ createDataSrcClientList(DataSrcType type, DataSourceClient& client) {
return (boost::shared_ptr<ClientList>(new SingletonList(client)));
case INMEMORY:
list.reset(new ConfigurableClientList(RRClass::IN()));
+ // Configure one normal zone and one "empty" zone.
list->configure(isc::data::Element::fromJSON(
"[{\"type\": \"MasterFiles\","
" \"cache-enable\": true, "
" \"params\": {\"example.com\": \"" +
- string(TEST_OWN_DATA_BUILDDIR "/example.zone") +
+ string(TEST_OWN_DATA_BUILDDIR "/example.zone\",") +
+ + "\"" + EMPTY_ZONE_NAME + "\": \"" +
+ string(TEST_OWN_DATA_BUILDDIR "/nosuchfile.zone") +
"\"}}]"), true);
return (list);
case SQLITE3:
@@ -834,39 +849,38 @@ createDataSrcClientList(DataSrcType type, DataSourceClient& client) {
class MockClient : public DataSourceClient {
public:
virtual FindResult findZone(const isc::dns::Name& origin) const {
- const Name r_origin(origin.reverse());
- std::map<Name, ZoneFinderPtr>::const_iterator it =
- zone_finders_.lower_bound(r_origin);
-
- if (it != zone_finders_.end()) {
- const NameComparisonResult result =
- origin.compare((it->first).reverse());
- if (result.getRelation() == NameComparisonResult::EQUAL) {
- return (FindResult(result::SUCCESS, it->second));
- } else if (result.getRelation() == NameComparisonResult::SUBDOMAIN) {
- return (FindResult(result::PARTIALMATCH, it->second));
- }
- }
+ // Identify the next (strictly) larger name than the given 'origin' in
+ // the map. Its predecessor (if any) is the longest matching name
+ // if it's either an exact match or a super domain; otherwise there's
+ // no match in the map. See also datasrc/tests/mock_client.cc.
- // If it is at the beginning of the map, then the name was not
- // found (we have already handled the element the iterator
- // points to).
- if (it == zone_finders_.begin()) {
+ // Eliminate the case of empty map to simply the rest of the code
+ if (zone_finders_.empty()) {
return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
}
- // Check if the previous element is a partial match.
- --it;
- const NameComparisonResult result =
- origin.compare((it->first).reverse());
- if (result.getRelation() == NameComparisonResult::SUBDOMAIN) {
- return (FindResult(result::PARTIALMATCH, it->second));
+ std::map<Name, ZoneFinderPtr>::const_iterator it =
+ zone_finders_.upper_bound(origin);
+ if (it == zone_finders_.begin()) { // no predecessor
+ return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
}
- return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
+ --it; // get the predecessor
+ const result::ResultFlags flags =
+ it->second ? result::FLAGS_DEFAULT : result::ZONE_EMPTY;
+ const NameComparisonResult compar(it->first.compare(origin));
+ switch (compar.getRelation()) {
+ case NameComparisonResult::EQUAL:
+ return (FindResult(result::SUCCESS, it->second, flags));
+ case NameComparisonResult::SUPERDOMAIN:
+ return (FindResult(result::PARTIALMATCH, it->second, flags));
+ default:
+ return (FindResult(result::NOTFOUND, ZoneFinderPtr()));
+ }
}
- virtual ZoneUpdaterPtr getUpdater(const isc::dns::Name&, bool, bool) const {
+ virtual ZoneUpdaterPtr getUpdater(const isc::dns::Name&, bool, bool) const
+ {
isc_throw(isc::NotImplemented,
"Updater isn't supported in the MockClient");
}
@@ -878,18 +892,21 @@ public:
}
result::Result addZone(ZoneFinderPtr finder) {
- // Use the reverse of the name as the key, so we can quickly
- // find partial matches in the map.
- zone_finders_[finder->getOrigin().reverse()] = finder;
+ zone_finders_[finder->getOrigin()] = finder;
+ return (result::SUCCESS);
+ }
+
+ // "configure" a zone with no data. This will cause the ZONE_EMPTY flag
+ // on in finZone().
+ result::Result addEmptyZone(const Name& zone_name) {
+ zone_finders_[zone_name] = ZoneFinderPtr();
return (result::SUCCESS);
}
private:
// Note that because we no longer have the old RBTree, and the new
// in-memory DomainTree is not useful as it returns const nodes, we
- // use a std::map instead. In this map, the key is a name stored in
- // reverse order of labels to aid in finding partial matches
- // quickly.
+ // use a std::map instead.
std::map<Name, ZoneFinderPtr> zone_finders_;
};
@@ -916,9 +933,10 @@ protected:
response.setRcode(Rcode::NOERROR());
response.setOpcode(Opcode::QUERY());
- // create and add a matching zone.
+ // create and add a matching zone. One is a "broken, empty" zone.
mock_finder = new MockZoneFinder();
mock_client.addZone(ZoneFinderPtr(mock_finder));
+ mock_client.addEmptyZone(Name(EMPTY_ZONE_NAME));
}
virtual void SetUp() {
@@ -949,6 +967,12 @@ protected:
setNSEC3HashCreator(NULL);
}
+ bool isEmptyZoneSupported() const {
+ // Not all data sources support the concept of empty zones.
+ // Specifically for this test, SQLite3-based data source doesn't.
+ return (GetParam() != SQLITE3);
+ }
+
void enableNSEC3(const vector<string>& rrsets_to_add) {
boost::shared_ptr<ConfigurableClientList> new_list;
switch (GetParam()) {
@@ -1144,11 +1168,29 @@ TEST_P(QueryTest, noZone) {
// REFUSED.
MockClient empty_mock_client;
SingletonList empty_list(empty_mock_client);
- EXPECT_NO_THROW(query.process(empty_list, qname, qtype,
- response));
+ EXPECT_NO_THROW(query.process(empty_list, qname, qtype, response));
EXPECT_EQ(Rcode::REFUSED(), response.getRcode());
}
+TEST_P(QueryTest, emptyZone) {
+ // Query for an "empty (broken)" zone. If the concept is supported by
+ // the underlying data source, the result should be SERVFAIL; otherwise
+ // it would be handled as a nonexistent zone, resulting in REFUSED.
+ const Rcode expected_rcode =
+ isEmptyZoneSupported() ? Rcode::SERVFAIL() : Rcode::REFUSED();
+
+ query.process(*list_, Name(EMPTY_ZONE_NAME), qtype, response);
+ responseCheck(response, expected_rcode, 0, 0, 0, 0, NULL, NULL, NULL);
+
+ // Same for the partial match case
+ response.clear(isc::dns::Message::RENDER);
+ response.setRcode(Rcode::NOERROR());
+ response.setOpcode(Opcode::QUERY());
+ query.process(*list_, Name(string("www.") + EMPTY_ZONE_NAME), qtype,
+ response);
+ responseCheck(response, expected_rcode, 0, 0, 0, 0, NULL, NULL, NULL);
+}
+
TEST_P(QueryTest, exactMatch) {
EXPECT_NO_THROW(query.process(*list_, qname, qtype, response));
// find match rrset
@@ -1400,7 +1442,6 @@ TEST_F(QueryTestForMockOnly, badSecureDelegation) {
qtype, response));
}
-
TEST_P(QueryTest, nxdomain) {
EXPECT_NO_THROW(query.process(*list_,
Name("nxdomain.example.com"), qtype,
diff --git a/src/bin/auth/tests/statistics_unittest.cc.pre b/src/bin/auth/tests/statistics_unittest.cc.pre
index cf6f29a..654bcd9 100644
--- a/src/bin/auth/tests/statistics_unittest.cc.pre
+++ b/src/bin/auth/tests/statistics_unittest.cc.pre
@@ -361,6 +361,64 @@ TEST_F(CountersTest, incrementTSIG) {
}
}
+TEST_F(CountersTest, incrementRD) {
+ Message response(Message::RENDER);
+ MessageAttributes msgattrs;
+ std::map<std::string, int> expect;
+
+ // Test these patterns:
+ // OpCode Recursion Desired
+ // ---------------------------
+ // 0 (Query) false
+ // 0 (Query) true
+ // 2 (Status) false
+ // 2 (Status) true
+ // Make sure the counter will be incremented only for the requests with
+ // OpCode=Query and Recursion Desired (RD) bit=1.
+ int count_opcode_query = 0;
+ int count_opcode_status = 0;
+ for (int i = 0; i < 4; ++i) {
+ const bool is_recursion_desired = i & 1;
+ const uint8_t opcode_code = i & 0x2;
+ const Opcode opcode(opcode_code);
+ buildSkeletonMessage(msgattrs);
+ msgattrs.setRequestRD(is_recursion_desired);
+ msgattrs.setRequestOpCode(opcode);
+
+ response.setRcode(Rcode::REFUSED());
+ response.addQuestion(Question(Name("example.com"),
+ RRClass::IN(), RRType::AAAA()));
+ response.setHeaderFlag(Message::HEADERFLAG_QR);
+
+ counters.inc(msgattrs, response, true);
+
+ if (opcode == Opcode::QUERY()) {
+ ++count_opcode_query;
+ } else {
+ ++count_opcode_status;
+ }
+
+ expect.clear();
+ expect["opcode.query"] = count_opcode_query;
+ expect["opcode.status"] = count_opcode_status;
+ expect["request.v4"] = i+1;
+ expect["request.udp"] = i+1;
+ expect["request.edns0"] = i+1;
+ expect["request.dnssec_ok"] = i+1;
+ expect["responses"] = i+1;
+ // qryrecursion will (only) be incremented if i == 1: OpCode=Query and
+ // RD bit=1
+ expect["qryrecursion"] = (i == 0) ? 0 : 1;
+ expect["rcode.refused"] = i+1;
+ // these counters are for queries; the value will be equal to the
+ // number of requests with OpCode=Query
+ expect["qrynoauthans"] = count_opcode_query;
+ expect["authqryrej"] = count_opcode_query;
+ checkStatisticsCounters(counters.get()->get("zones")->get("_SERVER_"),
+ expect);
+ }
+}
+
TEST_F(CountersTest, incrementOpcode) {
Message response(Message::RENDER);
MessageAttributes msgattrs;
diff --git a/src/bin/auth/tests/test_datasrc_clients_mgr.cc b/src/bin/auth/tests/test_datasrc_clients_mgr.cc
index 82937c0..80bc97c 100644
--- a/src/bin/auth/tests/test_datasrc_clients_mgr.cc
+++ b/src/bin/auth/tests/test_datasrc_clients_mgr.cc
@@ -26,7 +26,9 @@ namespace datasrc_clientmgr_internal {
// Define static DataSrcClientsBuilder member variables.
bool FakeDataSrcClientsBuilder::started = false;
std::list<Command>* FakeDataSrcClientsBuilder::command_queue = NULL;
+std::list<FinishedCallback>* FakeDataSrcClientsBuilder::callback_queue = NULL;
std::list<Command> FakeDataSrcClientsBuilder::command_queue_copy;
+std::list<FinishedCallback> FakeDataSrcClientsBuilder::callback_queue_copy;
TestCondVar* FakeDataSrcClientsBuilder::cond = NULL;
TestCondVar FakeDataSrcClientsBuilder::cond_copy;
TestMutex* FakeDataSrcClientsBuilder::queue_mutex = NULL;
@@ -38,6 +40,7 @@ bool FakeDataSrcClientsBuilder::thread_waited = false;
FakeDataSrcClientsBuilder::ExceptionFromWait
FakeDataSrcClientsBuilder::thread_throw_on_wait =
FakeDataSrcClientsBuilder::NOTHROW;
+int FakeDataSrcClientsBuilder::wakeup_fd = -1;
template<>
void
@@ -58,7 +61,7 @@ TestDataSrcClientsBuilder::doNoop() {
template<>
void
-TestDataSrcClientsMgr::cleanup() {
+TestDataSrcClientsMgrBase::cleanup() {
using namespace datasrc_clientmgr_internal;
// Make copy of some of the manager's member variables and reset the
// corresponding pointers. The currently pointed objects are in the
@@ -73,17 +76,21 @@ TestDataSrcClientsMgr::cleanup() {
FakeDataSrcClientsBuilder::cond_copy = cond_;
FakeDataSrcClientsBuilder::cond =
&FakeDataSrcClientsBuilder::cond_copy;
+ FakeDataSrcClientsBuilder::callback_queue_copy =
+ *FakeDataSrcClientsBuilder::callback_queue;
+ FakeDataSrcClientsBuilder::callback_queue =
+ &FakeDataSrcClientsBuilder::callback_queue_copy;
}
template<>
void
-TestDataSrcClientsMgr::reconfigureHook() {
+TestDataSrcClientsMgrBase::reconfigureHook() {
using namespace datasrc_clientmgr_internal;
// Simply replace the local map, ignoring bogus config value.
- assert(command_queue_.front().first == RECONFIGURE);
+ assert(command_queue_.front().id == RECONFIGURE);
try {
- clients_map_ = configureDataSource(command_queue_.front().second);
+ clients_map_ = configureDataSource(command_queue_.front().params);
} catch (...) {}
}
diff --git a/src/bin/auth/tests/test_datasrc_clients_mgr.h b/src/bin/auth/tests/test_datasrc_clients_mgr.h
index 9b1a367..34097da 100644
--- a/src/bin/auth/tests/test_datasrc_clients_mgr.h
+++ b/src/bin/auth/tests/test_datasrc_clients_mgr.h
@@ -20,6 +20,8 @@
#include <auth/datasrc_clients_mgr.h>
#include <datasrc/datasrc_config.h>
+#include <asiolink/io_service.h>
+
#include <boost/function.hpp>
#include <list>
@@ -131,15 +133,18 @@ public:
// true iff a builder has started.
static bool started;
- // These three correspond to the resource shared with the manager.
+ // These five correspond to the resource shared with the manager.
// xxx_copy will be set in the manager's destructor to record the
// final state of the manager.
static std::list<Command>* command_queue;
+ static std::list<FinishedCallback>* callback_queue;
static TestCondVar* cond;
static TestMutex* queue_mutex;
+ static int wakeup_fd;
static isc::datasrc::ClientListMapPtr* clients_map;
static TestMutex* map_mutex;
static std::list<Command> command_queue_copy;
+ static std::list<FinishedCallback> callback_queue_copy;
static TestCondVar cond_copy;
static TestMutex queue_mutex_copy;
@@ -153,15 +158,18 @@ public:
FakeDataSrcClientsBuilder(
std::list<Command>* command_queue,
+ std::list<FinishedCallback>* callback_queue,
TestCondVar* cond,
TestMutex* queue_mutex,
isc::datasrc::ClientListMapPtr* clients_map,
- TestMutex* map_mutex)
+ TestMutex* map_mutex, int wakeup_fd)
{
FakeDataSrcClientsBuilder::started = false;
FakeDataSrcClientsBuilder::command_queue = command_queue;
+ FakeDataSrcClientsBuilder::callback_queue = callback_queue;
FakeDataSrcClientsBuilder::cond = cond;
FakeDataSrcClientsBuilder::queue_mutex = queue_mutex;
+ FakeDataSrcClientsBuilder::wakeup_fd = wakeup_fd;
FakeDataSrcClientsBuilder::clients_map = clients_map;
FakeDataSrcClientsBuilder::map_mutex = map_mutex;
FakeDataSrcClientsBuilder::thread_waited = false;
@@ -201,18 +209,30 @@ typedef DataSrcClientsMgrBase<
datasrc_clientmgr_internal::TestThread,
datasrc_clientmgr_internal::FakeDataSrcClientsBuilder,
datasrc_clientmgr_internal::TestMutex,
- datasrc_clientmgr_internal::TestCondVar> TestDataSrcClientsMgr;
+ datasrc_clientmgr_internal::TestCondVar> TestDataSrcClientsMgrBase;
// A specialization of manager's "cleanup" called at the end of the
// destructor. We use this to record the final values of some of the class
// member variables.
template<>
void
-TestDataSrcClientsMgr::cleanup();
+TestDataSrcClientsMgrBase::cleanup();
template<>
void
-TestDataSrcClientsMgr::reconfigureHook();
+TestDataSrcClientsMgrBase::reconfigureHook();
+
+// A (hackish) trick how to not require the IOService to be passed from the
+// tests. We can't create the io service as a member, because it would
+// get initialized too late.
+class TestDataSrcClientsMgr :
+ public asiolink::IOService,
+ public TestDataSrcClientsMgrBase {
+public:
+ TestDataSrcClientsMgr() :
+ TestDataSrcClientsMgrBase(*static_cast<asiolink::IOService*>(this))
+ {}
+};
} // namespace auth
} // namespace isc
diff --git a/src/bin/bind10/init.py.in b/src/bin/bind10/init.py.in
index efc0b04..67491b3 100755
--- a/src/bin/bind10/init.py.in
+++ b/src/bin/bind10/init.py.in
@@ -78,6 +78,7 @@ from isc.log_messages.init_messages import *
import isc.bind10.component
import isc.bind10.special_component
import isc.bind10.socket_cache
+import isc.util.traceback_handler
import libutil_io_python
import tempfile
@@ -89,7 +90,8 @@ logger = isc.log.Logger("init")
DBG_PROCESS = logger.DBGLVL_TRACE_BASIC
DBG_COMMANDS = logger.DBGLVL_TRACE_DETAIL
-# Messages sent over the unix domain socket to indicate if it is followed by a real socket
+# Messages sent over the unix domain socket to indicate if it is followed by a
+# real socket
CREATOR_SOCKET_OK = b"1\n"
CREATOR_SOCKET_UNAVAILABLE = b"0\n"
@@ -200,7 +202,8 @@ class Init:
verbose=False, nokill=False, setuid=None, setgid=None,
username=None, cmdctl_port=None, wait_time=10):
"""
- Initialize the Init of BIND. This is a singleton (only one can run).
+ Initialize the Init of BIND. This is a singleton (only one can
+ run).
The msgq_socket_file specifies the UNIX domain socket file that the
msgq process listens on. If verbose is True, then b10-init reports
@@ -223,12 +226,13 @@ class Init:
self.component_config = {}
# Some time in future, it may happen that a single component has
# multple processes (like a pipeline-like component). If so happens,
- # name "components" may be inappropriate. But as the code isn't probably
- # completely ready for it, we leave it at components for now. We also
- # want to support multiple instances of a single component. If it turns
- # out that we'll have a single component with multiple same processes
- # or if we start multiple components with the same configuration (we do
- # this now, but it might change) is an open question.
+ # name "components" may be inappropriate. But as the code isn't
+ # probably completely ready for it, we leave it at components for
+ # now. We also want to support multiple instances of a single
+ # component. If it turns out that we'll have a single component with
+ # multiple same processes or if we start multiple components with the
+ # same configuration (we do this now, but it might change) is an open
+ # question.
self.components = {}
# Simply list of components that died and need to wait for a
# restart. Components manage their own restart schedule now
@@ -333,6 +337,7 @@ class Init:
self.__propagate_component_config(new_config['components'])
return isc.config.ccsession.create_answer(0)
except Exception as e:
+ logger.error(BIND10_RECONFIGURE_ERROR, e)
return isc.config.ccsession.create_answer(1, str(e))
def get_processes(self):
@@ -351,7 +356,8 @@ class Init:
def command_handler(self, command, args):
logger.debug(DBG_COMMANDS, BIND10_RECEIVED_COMMAND, command)
- answer = isc.config.ccsession.create_answer(1, "command not implemented")
+ answer = isc.config.ccsession.create_answer(1,
+ "command not implemented")
if type(command) != str:
answer = isc.config.ccsession.create_answer(1, "bad command")
else:
@@ -440,7 +446,8 @@ class Init:
if pid is None:
logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS, self.curproc)
else:
- logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS_PID, self.curproc, pid)
+ logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS_PID, self.curproc,
+ pid)
def process_running(self, msg, who):
"""
@@ -483,7 +490,7 @@ class Init:
self.log_starting("b10-msgq")
msgq_proc = self._make_process_info("b10-msgq", ["b10-msgq"],
self.c_channel_env,
- True, not self.verbose)
+ not self.verbose, not self.verbose)
msgq_proc.spawn()
self.log_started(msgq_proc.pid)
@@ -499,7 +506,8 @@ class Init:
if msgq_proc.process:
msgq_proc.process.kill()
logger.error(BIND10_CONNECTING_TO_CC_FAIL)
- raise CChannelConnectError("Unable to connect to c-channel after 5 seconds")
+ raise CChannelConnectError("Unable to connect to c-channel " +
+ "after 5 seconds")
# try to connect, and if we can't wait a short while
try:
@@ -507,13 +515,43 @@ class Init:
except isc.cc.session.SessionError:
time.sleep(0.1)
- # Subscribe to the message queue. The only messages we expect to receive
- # on this channel are once relating to process startup.
+ # Subscribe to the message queue. The only messages we expect to
+ # receive on this channel are once relating to process startup.
if self.cc_session is not None:
self.cc_session.group_subscribe("Init")
return msgq_proc
+ def wait_msgq(self):
+ """
+ Wait for the message queue to fully start. It does so only after
+ the config manager connects to it. We know it is ready when it
+ starts answering commands.
+
+ We don't add a specific command for it here, an error response is
+ as good as positive one to know it is alive.
+ """
+ # We do 10 times shorter sleep here (since the start should be fast
+ # now), so we have 10 times more attempts.
+ time_remaining = self.wait_time * 10
+ retry = True
+ while time_remaining > 0 and retry:
+ try:
+ self.ccs.rpc_call('AreYouThere?', 'Msgq')
+ # We don't expect this to succeed. If it does, it's programmer
+ # error
+ raise Exception("Non-existing RPC call succeeded")
+ except isc.config.RPCRecipientMissing:
+ retry = True # Not there yet
+ time.sleep(0.1)
+ time_remaining -= 1
+ except isc.config.RPCError:
+ retry = False # It doesn't like the RPC, so it's alive now
+
+ if retry: # Still not started
+ raise ProcessStartError("Msgq didn't complete the second stage " +
+ "of startup")
+
def start_cfgmgr(self):
"""
Starts the configuration manager process
@@ -536,14 +574,16 @@ class Init:
# time to wait can be set on the command line.
time_remaining = self.wait_time
msg, env = self.cc_session.group_recvmsg()
- while time_remaining > 0 and not self.process_running(msg, "ConfigManager"):
+ while time_remaining > 0 and not self.process_running(msg,
+ "ConfigManager"):
logger.debug(DBG_PROCESS, BIND10_WAIT_CFGMGR)
time.sleep(1)
time_remaining = time_remaining - 1
msg, env = self.cc_session.group_recvmsg()
if not self.process_running(msg, "ConfigManager"):
- raise ProcessStartError("Configuration manager process has not started")
+ raise ProcessStartError("Configuration manager process has not " +
+ "started")
return bind_cfgd
@@ -558,6 +598,13 @@ class Init:
process, the log_starting/log_started methods are not used.
"""
logger.info(BIND10_STARTING_CC)
+
+ # Unsubscribe from the other CC session first, because we only
+ # monitor one and msgq expects all data sent to us to be read,
+ # or it will close its side of the socket.
+ if self.cc_session is not None:
+ self.cc_session.group_unsubscribe("Init")
+
self.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler,
@@ -567,7 +614,8 @@ class Init:
# A couple of utility methods for starting processes...
- def start_process(self, name, args, c_channel_env, port=None, address=None):
+ def start_process(self, name, args, c_channel_env, port=None,
+ address=None):
"""
Given a set of command arguments, start the process and output
appropriate log messages. If the start is successful, the process
@@ -612,9 +660,9 @@ class Init:
# The next few methods start up the rest of the BIND-10 processes.
# Although many of these methods are little more than a call to
- # start_simple, they are retained (a) for testing reasons and (b) as a place
- # where modifications can be made if the process start-up sequence changes
- # for a given process.
+ # start_simple, they are retained (a) for testing reasons and (b) as a
+ # place where modifications can be made if the process start-up sequence
+ # changes for a given process.
def start_auth(self):
"""
@@ -666,6 +714,10 @@ class Init:
# inside the configurator.
self.start_ccsession(self.c_channel_env)
+ # Make sure msgq is fully started before proceeding to the rest
+ # of the components.
+ self.wait_msgq()
+
# Extract the parameters associated with Init. This can only be
# done after the CC Session is started. Note that the logging
# configuration may override the "-v" switch set on the command line.
@@ -689,7 +741,12 @@ class Init:
try:
self.cc_session = isc.cc.Session(self.msgq_socket_file)
logger.fatal(BIND10_MSGQ_ALREADY_RUNNING)
- return "b10-msgq already running, or socket file not cleaned , cannot start"
+ if self.msgq_socket_file is not None:
+ socket_name = "socket file '" + self.msg_socket_file + "'"
+ else:
+ socket_name = "default socket file"
+ return "b10-msgq already running, or " + socket_name +\
+ " not cleaned - cannot start"
except isc.cc.session.SessionError:
# this is the case we want, where the msgq is not running
pass
@@ -719,9 +776,14 @@ class Init:
it might want to choose if it is for this one).
"""
logger.info(BIND10_STOP_PROCESS, process)
- self.cc_session.group_sendmsg(isc.config.ccsession.
- create_command('shutdown', {'pid': pid}),
- recipient, recipient)
+ try:
+ self.cc_session.group_sendmsg(isc.config.ccsession.
+ create_command('shutdown',
+ {'pid': pid}),
+ recipient, recipient)
+ except:
+ logger.error(BIND10_COMPONENT_SHUTDOWN_ERROR, process)
+ raise
def component_shutdown(self, exitcode=0):
"""
@@ -948,8 +1010,8 @@ class Init:
def set_creator(self, creator):
"""
- Registeres a socket creator into the b10-init. The socket creator is not
- used directly, but through a cache. The cache is created in this
+ Registeres a socket creator into the b10-init. The socket creator is
+ not used directly, but through a cache. The cache is created in this
method.
If called more than once, it raises a ValueError.
@@ -1121,9 +1183,12 @@ def parse_args(args=sys.argv[1:], Parser=OptionParser):
parser = Parser(version=VERSION)
parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
type="string", default=None,
- help="UNIX domain socket file the b10-msgq daemon will use")
+ help="UNIX domain socket file the b10-msgq daemon " +
+ "will use")
parser.add_option("-i", "--no-kill", action="store_true", dest="nokill",
- default=False, help="do not send SIGTERM and SIGKILL signals to modules during shutdown")
+ default=False,
+ help="do not send SIGTERM and SIGKILL signals to " +
+ "modules during shutdown")
parser.add_option("-u", "--user", dest="user", type="string", default=None,
help="Change user after startup (must run as root)")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
@@ -1147,7 +1212,9 @@ def parse_args(args=sys.argv[1:], Parser=OptionParser):
default=None,
help="file to dump the PID of the BIND 10 process")
parser.add_option("-w", "--wait", dest="wait_time", type="int",
- default=10, help="Time (in seconds) to wait for config manager to start up")
+ default=10,
+ help="Time (in seconds) to wait for config manager to "
+ "start up")
(options, args) = parser.parse_args(args)
@@ -1319,4 +1386,4 @@ def main():
sys.exit(b10_init.exitcode)
if __name__ == "__main__":
- main()
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/bind10/init_messages.mes b/src/bin/bind10/init_messages.mes
index 267790d..21cd142 100644
--- a/src/bin/bind10/init_messages.mes
+++ b/src/bin/bind10/init_messages.mes
@@ -325,3 +325,10 @@ the configuration manager to start up. The total length of time Init
will wait for the configuration manager before reporting an error is
set with the command line --wait switch, which has a default value of
ten seconds.
+
+% BIND10_RECONFIGURE_ERROR Error applying new config: %1
+A new configuration was received, but there was an error doing the
+re-configuration.
+
+% BIND10_COMPONENT_SHUTDOWN_ERROR An error occured stopping component %1
+An attempt to gracefully shutdown a component failed.
diff --git a/src/bin/bind10/run_bind10.sh.in b/src/bin/bind10/run_bind10.sh.in
index a22f300..09c9708 100755
--- a/src/bin/bind10/run_bind10.sh.in
+++ b/src/bin/bind10/run_bind10.sh.in
@@ -20,7 +20,7 @@ export PYTHON_EXEC
BIND10_PATH=@abs_top_builddir@/src/bin/bind10
-PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/ddns:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:$PATH
+PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/ddns:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:@abs_top_builddir@/src/bin/memmgr:$PATH
export PATH
PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs
diff --git a/src/bin/bind10/tests/init_test.py.in b/src/bin/bind10/tests/init_test.py.in
index 8ac6458..af41eb6 100644
--- a/src/bin/bind10/tests/init_test.py.in
+++ b/src/bin/bind10/tests/init_test.py.in
@@ -16,7 +16,8 @@
# Most of the time, we omit the "init" for brevity. Sometimes,
# we want to be explicit about what we do, like when hijacking a library
# call used by the b10-init.
-from init import Init, ProcessInfo, parse_args, dump_pid, unlink_pid_file, _BASETIME
+from init import Init, ProcessInfo, parse_args, dump_pid, unlink_pid_file, \
+ _BASETIME
import init
# XXX: environment tests are currently disabled, due to the preprocessor
@@ -941,6 +942,7 @@ class TestStartStopProcessesInit(unittest.TestCase):
init.start_ccsession = lambda _: start_ccsession()
# We need to return the original _read_bind10_config
init._read_bind10_config = lambda: Init._read_bind10_config(init)
+ init.wait_msgq = lambda: None
init.start_all_components()
self.check_started(init, True, start_auth, start_resolver)
self.check_environment_unchanged()
@@ -967,6 +969,7 @@ class TestStartStopProcessesInit(unittest.TestCase):
init = MockInit()
self.check_preconditions(init)
+ init.wait_msgq = lambda: None
init.start_all_components()
init.runnable = True
init.config_handler(self.construct_config(False, False))
@@ -1028,6 +1031,7 @@ class TestStartStopProcessesInit(unittest.TestCase):
init = MockInit()
self.check_preconditions(init)
+ init.wait_msgq = lambda: None
init.start_all_components()
init.runnable = True
@@ -1066,6 +1070,7 @@ class TestStartStopProcessesInit(unittest.TestCase):
init = MockInit()
self.check_preconditions(init)
+ init.wait_msgq = lambda: None
init.start_all_components()
init.config_handler(self.construct_config(False, False))
self.check_started_dhcp(init, False, False)
@@ -1075,6 +1080,7 @@ class TestStartStopProcessesInit(unittest.TestCase):
init = MockInit()
self.check_preconditions(init)
# v6 only enabled
+ init.wait_msgq = lambda: None
init.start_all_components()
init.runnable = True
init._Init_started = True
@@ -1347,6 +1353,7 @@ class TestInitComponents(unittest.TestCase):
# Start it
orig = init._component_configurator.startup
init._component_configurator.startup = self.__unary_hook
+ init.wait_msgq = lambda: None
init.start_all_components()
init._component_configurator.startup = orig
self.__check_core(self.__param)
@@ -1499,6 +1506,7 @@ class TestInitComponents(unittest.TestCase):
pass
init.ccs = CC()
init.ccs.get_full_config = lambda: {'components': self.__compconfig}
+ init.wait_msgq = lambda: None
init.start_all_components()
self.__check_extended(self.__param)
@@ -1643,7 +1651,7 @@ class TestInitComponents(unittest.TestCase):
pi = init.start_msgq()
self.assertEqual('b10-msgq', pi.name)
self.assertEqual(['b10-msgq'], pi.args)
- self.assertTrue(pi.dev_null_stdout)
+ self.assertEqual(pi.dev_null_stdout, not verbose)
self.assertEqual(pi.dev_null_stderr, not verbose)
self.assertEqual({'FOO': 'an env string'}, pi.env)
@@ -1768,6 +1776,51 @@ class TestInitComponents(unittest.TestCase):
# this is set by ProcessInfo.spawn()
self.assertEqual(42147, pi.pid)
+ def test_wait_msgq(self):
+ """
+ Test we can wait for msgq to provide its own alias.
+
+ It is not available the first time, the second it is.
+ """
+ class RpcSession:
+ def __init__(self):
+ # Not yet called
+ self.called = 0
+
+ def rpc_call(self, command, recipient):
+ self.called += 1
+ if self.called == 1:
+ raise isc.config.RPCRecipientMissing("Not yet")
+ elif self.called == 2:
+ raise isc.config.RPCError(1, "What?")
+ else:
+ raise Exception("Called too many times")
+
+ init = MockInitSimple()
+ init.wait_time = 1
+ init.ccs = RpcSession()
+ init.wait_msgq()
+ self.assertEqual(2, init.ccs.called)
+
+ def test_wait_msgq_fail(self):
+ """
+ Test the wait_msgq fails in case the msgq does not appear
+ after so many attempts.
+ """
+ class RpcSession:
+ def __init__(self):
+ self.called = 0
+
+ def rpc_call(self, command, recipient):
+ self.called += 1
+ raise isc.config.RPCRecipientMissing("Not yet")
+
+ b10init = MockInitSimple()
+ b10init.wait_time = 1
+ b10init.ccs = RpcSession()
+ self.assertRaises(init.ProcessStartError, b10init.wait_msgq)
+ self.assertEqual(10, b10init.ccs.called)
+
def test_start_cfgmgr(self):
'''Test that b10-cfgmgr is started.'''
class DummySession():
diff --git a/src/bin/bindctl/bindcmd.py b/src/bin/bindctl/bindcmd.py
index 03b5d6b..bcae95c 100644
--- a/src/bin/bindctl/bindcmd.py
+++ b/src/bin/bindctl/bindcmd.py
@@ -272,6 +272,11 @@ WARNING: The Python readline module isn't available, so some command line
else:
self._print('Login failed: either the user name or password is '
'invalid.\n')
+
+ # If this was not an interactive session do not prompt for login info.
+ if not sys.stdin.isatty():
+ return False
+
while True:
count = count + 1
if count > 3:
diff --git a/src/bin/bindctl/bindctl_main.py.in b/src/bin/bindctl/bindctl_main.py.in
index 875b06e..68a6237 100755
--- a/src/bin/bindctl/bindctl_main.py.in
+++ b/src/bin/bindctl/bindctl_main.py.in
@@ -26,6 +26,7 @@ from bindctl import command_sets
import pprint
from optparse import OptionParser, OptionValueError
import isc.util.process
+import isc.util.traceback_handler
isc.util.process.rename()
@@ -150,7 +151,7 @@ def set_bindctl_options(parser):
default=None, action='store',
help='Directory to store the password CSV file')
-if __name__ == '__main__':
+def main():
parser = OptionParser(version = VERSION)
set_bindctl_options(parser)
(options, args) = parser.parse_args()
@@ -161,3 +162,6 @@ if __name__ == '__main__':
command_sets.prepare_execute_commands(tool)
result = tool.run()
sys.exit(result)
+
+if __name__ == '__main__':
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/cfgmgr/b10-cfgmgr.py.in b/src/bin/cfgmgr/b10-cfgmgr.py.in
index 06b9b0f..4bc56df 100755
--- a/src/bin/cfgmgr/b10-cfgmgr.py.in
+++ b/src/bin/cfgmgr/b10-cfgmgr.py.in
@@ -30,6 +30,7 @@ import isc.log
isc.log.init("b10-cfgmgr", buffer=True)
from isc.config.cfgmgr import ConfigManager, ConfigManagerDataReadError, logger
from isc.log_messages.cfgmgr_messages import *
+import isc.util.traceback_handler
isc.util.process.rename()
@@ -128,4 +129,4 @@ def main():
return 0
if __name__ == "__main__":
- sys.exit(main())
+ sys.exit(isc.util.traceback_handler.traceback_handler(main))
diff --git a/src/bin/cfgmgr/plugins/tests/datasrc_test.py b/src/bin/cfgmgr/plugins/tests/datasrc_test.py
index 3c714c6..546e534 100644
--- a/src/bin/cfgmgr/plugins/tests/datasrc_test.py
+++ b/src/bin/cfgmgr/plugins/tests/datasrc_test.py
@@ -96,15 +96,6 @@ class DatasrcTest(unittest.TestCase):
"params": {}
}]})
- def test_dstype_bad(self):
- """
- The configuration is correct by the spec, but it would be rejected
- by the client list. Check we reject it.
- """
- self.reject({"IN": [{
- "type": "No such type"
- }]})
-
def test_invalid_mem_params(self):
"""
The client list skips in-memory sources. So we check it locally that
diff --git a/src/bin/cmdctl/Makefile.am b/src/bin/cmdctl/Makefile.am
index 72a400a..a2d04a3 100644
--- a/src/bin/cmdctl/Makefile.am
+++ b/src/bin/cmdctl/Makefile.am
@@ -57,12 +57,19 @@ b10_certgen_CXXFLAGS = $(BOTAN_INCLUDES)
b10_certgen_LDFLAGS = $(BOTAN_LIBS)
# Generate the initial certificates immediately
-cmdctl-certfile.pem: b10-certgen
- ./b10-certgen -q -w
-
cmdctl-keyfile.pem: b10-certgen
./b10-certgen -q -w
+# This is a hack, as b10-certgen creates both cmdctl-keyfile.pem and
+# cmdctl-certfile.pem, and in a parallel make, making these targets
+# simultaneously may result in corrupted files. With GNU make, there is
+# a non-portable way of working around this with pattern rules, but we
+# adopt this hack instead. The downside is that cmdctl-certfile.pem will
+# not be re-generated if cmdctl-keyfile.pem exists and is older. See
+# Trac ticket #2962.
+cmdctl-certfile.pem: cmdctl-keyfile.pem
+ touch $(builddir)/cmdctl-keyfile.pem
+
if INSTALL_CONFIGURATIONS
# Below we intentionally use ${INSTALL} -m 640 instead of $(INSTALL_DATA)
@@ -71,7 +78,7 @@ install-data-local:
$(mkinstalldirs) $(DESTDIR)/@sysconfdir@/@PACKAGE@
for f in $(CERTFILES) ; do \
if test ! -f $(DESTDIR)$(sysconfdir)/@PACKAGE@/$$f; then \
- ${INSTALL} -m 640 $(srcdir)/$$f $(DESTDIR)$(sysconfdir)/@PACKAGE@/ ; \
+ ${INSTALL} -m 640 $$f $(DESTDIR)$(sysconfdir)/@PACKAGE@/ ; \
fi ; \
done
diff --git a/src/bin/cmdctl/b10-cmdctl.xml b/src/bin/cmdctl/b10-cmdctl.xml
index 871265c..c66837c 100644
--- a/src/bin/cmdctl/b10-cmdctl.xml
+++ b/src/bin/cmdctl/b10-cmdctl.xml
@@ -169,8 +169,6 @@
The configuration command is:
</para>
-<!-- NOTE: print_settings is not documented since I think will be removed -->
-
<para>
<command>shutdown</command> exits <command>b10-cmdctl</command>.
This has an optional <varname>pid</varname> argument to
diff --git a/src/bin/cmdctl/cmdctl.py.in b/src/bin/cmdctl/cmdctl.py.in
index b1ee903..2e56629 100755
--- a/src/bin/cmdctl/cmdctl.py.in
+++ b/src/bin/cmdctl/cmdctl.py.in
@@ -36,11 +36,11 @@ import re
import ssl, socket
import isc
import pprint
-import select
import csv
import random
import time
import signal
+import errno
from isc.config import ccsession
import isc.cc.proto_defs
import isc.util.process
@@ -49,6 +49,7 @@ from optparse import OptionParser, OptionValueError
from hashlib import sha1
from isc.util import socketserver_mixin
from isc.log_messages.cmdctl_messages import *
+import isc.util.traceback_handler
isc.log.init("b10-cmdctl", buffer=True)
logger = isc.log.Logger("cmdctl")
@@ -371,8 +372,6 @@ class CommandControl():
self._httpserver.shutdown()
self._serving = False
- elif command == 'print_settings':
- answer = ccsession.create_answer(0, self._cmdctl_config_data)
else:
answer = ccsession.create_answer(1, 'unknown command: ' + command)
@@ -525,7 +524,17 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_STARTED,
server_address[0], server_address[1])
except socket.error as err:
- raise CmdctlException("Error creating server, because: %s \n" % str(err))
+ if err.errno == errno.EADDRINUSE:
+ raise CmdctlException(("Error creating server, because " +\
+ "port %d on address %s is " +\
+ "already in use. Please stop " +\
+ "the application that uses it or " +\
+ "see the guide about using a " +\
+ "different port for b10-cmdctl.") % \
+ (server_address[1], \
+ server_address[0]))
+ else:
+ raise CmdctlException("Error creating server, because: %s" % str(err))
self.user_sessions = {}
self.idle_timeout = idle_timeout
@@ -602,12 +611,13 @@ class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
# error)
return ssl_sock
except ssl.SSLError as err:
+ self.close_request(sock)
logger.error(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
- except (CmdctlException, IOError) as cce:
+ raise
+ except (CmdctlException, IOError, socket.error) as cce:
+ self.close_request(sock)
logger.error(CMDCTL_SSL_SETUP_FAILURE_READING_CERT, cce)
- self.close_request(sock)
- # raise socket error to finish the request
- raise socket.error
+ raise
def get_request(self):
'''Get client request socket and wrap it in SSL context. '''
@@ -677,7 +687,7 @@ def set_cmd_options(parser):
parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
help="display more about what is going on")
-if __name__ == '__main__':
+def main():
set_signal_handler()
parser = OptionParser(version = __version__)
set_cmd_options(parser)
@@ -703,3 +713,6 @@ if __name__ == '__main__':
logger.info(CMDCTL_EXITING)
sys.exit(result)
+
+if __name__ == '__main__':
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/cmdctl/cmdctl.spec.pre.in b/src/bin/cmdctl/cmdctl.spec.pre.in
index d04e2e3..87aeb11 100644
--- a/src/bin/cmdctl/cmdctl.spec.pre.in
+++ b/src/bin/cmdctl/cmdctl.spec.pre.in
@@ -24,11 +24,6 @@
],
"commands": [
{
- "command_name": "print_settings",
- "command_description": "Print some_string and some_int to stdout",
- "command_args": []
- },
- {
"command_name": "shutdown",
"command_description": "shutdown cmdctl",
"command_args": [
diff --git a/src/bin/cmdctl/tests/b10-certgen_test.py b/src/bin/cmdctl/tests/b10-certgen_test.py
index 56630bc..8054aa1 100644
--- a/src/bin/cmdctl/tests/b10-certgen_test.py
+++ b/src/bin/cmdctl/tests/b10-certgen_test.py
@@ -200,6 +200,8 @@ class TestCertGenTool(unittest.TestCase):
# No such file
self.run_check(105, None, None, [self.TOOL, '-c', 'foo'])
+ @unittest.skipIf(os.getuid() == 0,
+ 'test cannot be run as root user')
def test_permissions(self):
"""
Test some combinations of correct and bad permissions.
diff --git a/src/bin/cmdctl/tests/cmdctl_test.py b/src/bin/cmdctl/tests/cmdctl_test.py
index 4a6b0e3..7af8b0d 100644
--- a/src/bin/cmdctl/tests/cmdctl_test.py
+++ b/src/bin/cmdctl/tests/cmdctl_test.py
@@ -15,7 +15,7 @@
import unittest
-import socket
+import ssl, socket
import tempfile
import time
import stat
@@ -462,10 +462,22 @@ class TestCommandControl(unittest.TestCase):
answer = self.cmdctl.command_handler('unknown-command', None)
self._check_answer(answer, 1, 'unknown command: unknown-command')
- answer = self.cmdctl.command_handler('print_settings', None)
+ # Send a real command. Mock stuff so the shutdown command doesn't
+ # cause an exception.
+ class ModuleCC:
+ def send_stopping():
+ pass
+ self.cmdctl._module_cc = ModuleCC
+ called = []
+ class Server:
+ def shutdown():
+ called.append('shutdown')
+ self.cmdctl._httpserver = Server
+ answer = self.cmdctl.command_handler('shutdown', None)
rcode, msg = ccsession.parse_answer(answer)
self.assertEqual(rcode, 0)
- self.assertTrue(msg != None)
+ self.assertIsNone(msg)
+ self.assertEqual(['shutdown'], called)
def test_command_handler_spec_update(self):
# Should not be present
@@ -543,10 +555,10 @@ class TestCommandControl(unittest.TestCase):
self.assertEqual(1, rcode)
# Send a command to cmdctl itself. Should be the same effect.
- rcode, value = self.cmdctl.send_command('Cmdctl', 'print_settings',
+ rcode, value = self.cmdctl.send_command('Cmdctl', 'shutdown',
None)
self.assertEqual(2, len(self.cmdctl.sent_messages))
- self.assertEqual(({'command': ['print_settings']}, 'Cmdctl'),
+ self.assertEqual(({'command': ['shutdown']}, 'Cmdctl'),
self.cmdctl.sent_messages[-1])
self.assertEqual(1, rcode)
@@ -680,11 +692,15 @@ class TestSecureHTTPServer(unittest.TestCase):
# Just some file that we know exists
file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
check_file(file_name)
- with UnreadableFile(file_name):
- self.assertRaises(CmdctlException, check_file, file_name)
self.assertRaises(CmdctlException, check_file, '/local/not-exist')
self.assertRaises(CmdctlException, check_file, '/')
+ @unittest.skipIf(os.getuid() == 0,
+ 'test cannot be run as root user')
+ def test_check_file_for_unreadable(self):
+ file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
+ with UnreadableFile(file_name):
+ self.assertRaises(CmdctlException, check_file, file_name)
def test_check_key_and_cert(self):
keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
@@ -702,6 +718,15 @@ class TestSecureHTTPServer(unittest.TestCase):
self.assertRaises(CmdctlException, self.server._check_key_and_cert,
'/', certfile)
+ # All OK (also happens to check the context code above works)
+ self.server._check_key_and_cert(keyfile, certfile)
+
+ @unittest.skipIf(os.getuid() == 0,
+ 'test cannot be run as root user')
+ def test_check_key_and_cert_for_unreadable(self):
+ keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
+ certfile = BUILD_FILE_PATH + 'cmdctl-certfile.pem'
+
# no read permission
with UnreadableFile(certfile):
self.assertRaises(CmdctlException,
@@ -713,21 +738,17 @@ class TestSecureHTTPServer(unittest.TestCase):
self.server._check_key_and_cert,
keyfile, certfile)
- # All OK (also happens to check the context code above works)
- self.server._check_key_and_cert(keyfile, certfile)
-
def test_wrap_sock_in_ssl_context(self):
sock = socket.socket()
- # Bad files should result in a socket.error raised by our own
- # code in the basic file checks
- self.assertRaises(socket.error,
+ # Bad files should result in a CmdctlException in the basic file
+ # checks
+ self.assertRaises(CmdctlException,
self.server._wrap_socket_in_ssl_context,
sock,
'no_such_file', 'no_such_file')
- # Using a non-certificate file would cause an SSLError, which
- # is caught by our code which then raises a basic socket.error
+ # Using a non-certificate file would cause an SSLError
self.assertRaises(socket.error,
self.server._wrap_socket_in_ssl_context,
sock,
@@ -747,7 +768,7 @@ class TestSecureHTTPServer(unittest.TestCase):
orig_check_func = self.server._check_key_and_cert
try:
self.server._check_key_and_cert = lambda x,y: None
- self.assertRaises(socket.error,
+ self.assertRaises(IOError,
self.server._wrap_socket_in_ssl_context,
sock,
'no_such_file', 'no_such_file')
diff --git a/src/bin/d2/.gitignore b/src/bin/d2/.gitignore
new file mode 100644
index 0000000..9726436
--- /dev/null
+++ b/src/bin/d2/.gitignore
@@ -0,0 +1,7 @@
+/b10-dhcp-ddns
+/b10-dhcp-ddns.8
+/d2_messages.cc
+/d2_messages.h
+/spec_config.h
+/spec_config.h.pre
+/s-messages
diff --git a/src/bin/d2/Makefile.am b/src/bin/d2/Makefile.am
new file mode 100644
index 0000000..1da489c
--- /dev/null
+++ b/src/bin/d2/Makefile.am
@@ -0,0 +1,89 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += -I$(top_srcdir)/src/bin -I$(top_builddir)/src/bin
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+if USE_CLANGPP
+# Disable unused parameter warning caused by some Boost headers when compiling with clang
+AM_CXXFLAGS += -Wno-unused-parameter
+endif
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+
+CLEANFILES = *.gcno *.gcda spec_config.h d2_messages.h d2_messages.cc s-messages
+
+man_MANS = b10-dhcp-ddns.8
+DISTCLEANFILES = $(man_MANS)
+EXTRA_DIST = $(man_MANS) b10-dhcp-ddns.xml dhcp-ddns.spec
+
+if GENERATE_DOCS
+b10-dhcp-ddns.8: b10-dhcp-ddns.xml
+ @XSLTPROC@ --novalid --xinclude --nonet -o $@ \
+ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl \
+ $(srcdir)/b10-dhcp-ddns.xml
+
+else
+
+$(man_MANS):
+ @echo Man generation disabled. Creating dummy $@. Configure with --enable-generate-docs to enable it.
+ @echo Man generation disabled. Remove this file, configure with --enable-generate-docs, and rebuild BIND 10 > $@
+
+endif
+
+spec_config.h: spec_config.h.pre
+ $(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" spec_config.h.pre >$@
+
+d2_messages.h d2_messages.cc: s-messages
+
+s-messages: d2_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/d2/d2_messages.mes
+ touch $@
+
+BUILT_SOURCES = spec_config.h d2_messages.h d2_messages.cc
+
+pkglibexec_PROGRAMS = b10-dhcp-ddns
+
+b10_dhcp_ddns_SOURCES = main.cc
+b10_dhcp_ddns_SOURCES += d2_asio.h
+b10_dhcp_ddns_SOURCES += d2_log.cc d2_log.h
+b10_dhcp_ddns_SOURCES += d2_process.cc d2_process.h
+b10_dhcp_ddns_SOURCES += d_controller.cc d_controller.h
+b10_dhcp_ddns_SOURCES += d2_controller.cc d2_controller.h
+b10_dhcp_ddns_SOURCES += d_cfg_mgr.cc d_cfg_mgr.h
+b10_dhcp_ddns_SOURCES += d2_config.cc d2_config.h
+b10_dhcp_ddns_SOURCES += d2_cfg_mgr.cc d2_cfg_mgr.h
+b10_dhcp_ddns_SOURCES += d2_queue_mgr.cc d2_queue_mgr.h
+b10_dhcp_ddns_SOURCES += d2_update_message.cc d2_update_message.h
+b10_dhcp_ddns_SOURCES += d2_update_mgr.cc d2_update_mgr.h
+b10_dhcp_ddns_SOURCES += d2_zone.cc d2_zone.h
+b10_dhcp_ddns_SOURCES += dns_client.cc dns_client.h
+b10_dhcp_ddns_SOURCES += labeled_value.cc labeled_value.h
+b10_dhcp_ddns_SOURCES += nc_add.cc nc_add.h
+b10_dhcp_ddns_SOURCES += nc_remove.cc nc_remove.h
+b10_dhcp_ddns_SOURCES += nc_trans.cc nc_trans.h
+b10_dhcp_ddns_SOURCES += state_model.cc state_model.h
+
+nodist_b10_dhcp_ddns_SOURCES = d2_messages.h d2_messages.cc
+EXTRA_DIST += d2_messages.mes
+
+b10_dhcp_ddns_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+b10_dhcp_ddns_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+
+b10_dhcp_ddnsdir = $(pkgdatadir)
+b10_dhcp_ddns_DATA = dhcp-ddns.spec
diff --git a/src/bin/d2/b10-dhcp-ddns.xml b/src/bin/d2/b10-dhcp-ddns.xml
new file mode 100644
index 0000000..b77ff17
--- /dev/null
+++ b/src/bin/d2/b10-dhcp-ddns.xml
@@ -0,0 +1,121 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+ [<!ENTITY mdash "—">]>
+<!--
+ - Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or distribute this software for any
+ - purpose with or without fee is hereby granted, provided that the above
+ - copyright notice and this permission notice appear in all copies.
+ -
+ - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ - PERFORMANCE OF THIS SOFTWARE.
+-->
+
+<refentry>
+
+ <refentryinfo>
+ <date>May 15, 2013</date>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>b10-dhcp-ddns</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo>BIND10</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>b10-dhcp-ddns</refname>
+ <refpurpose>DHCP-DDNS process in BIND 10 architecture</refpurpose>
+ </refnamediv>
+
+ <docinfo>
+ <copyright>
+ <year>2013</year>
+ <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+ </copyright>
+ </docinfo>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>b10-dhcp-ddns</command>
+ <arg><option>-v</option></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>b10-dhcp-ddns</command>
+ <arg><option>-s</option></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+
+ <refsect1>
+ <title>DESCRIPTION</title>
+ <para>
+ The <command>b10-dhcp-ddns</command> service processes requests to
+ to update DNS mapping based on DHCP lease change events. The service
+ may run either as a BIND10 module (integrated mode) or as a individual
+ process (stand-alone mode) dependent upon command line arguments. The
+ default is integrated mode. Stand alone operation is strictly for
+ development purposes and is not suited for production.
+ </para>
+
+ </refsect1>
+
+ <refsect1>
+ <title>ARGUMENTS</title>
+
+ <para>The arguments are as follows:</para>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><option>-v</option></term>
+ <listitem><para>
+ Verbose mode sets the logging level to debug. This is primarily
+ for development purposes in stand-alone mode.
+ </para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>-s</option></term>
+ <listitem><para>
+ Causes the process to run without attempting to connect to the
+ BIND10 message queue. This is for development purposes.
+ </para></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>b10-dhcp-ddns</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>,
+ <citerefentry>
+ <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>HISTORY</title>
+ <para>
+ The <command>b10-dhcp-ddns</command> process was first coded in
+ May 2013 by the ISC Kea/Dhcp team.
+ </para>
+ </refsect1>
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->
diff --git a/src/bin/d2/d2_asio.h b/src/bin/d2/d2_asio.h
new file mode 100644
index 0000000..c9458f6
--- /dev/null
+++ b/src/bin/d2/d2_asio.h
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_ASIO_H
+#define D2_ASIO_H
+
+#include <asiolink/asiolink.h>
+
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Defines a smart pointer to an IOService instance.
+typedef boost::shared_ptr<isc::asiolink::IOService> IOServicePtr;
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/d2_cfg_mgr.cc b/src/bin/d2/d2_cfg_mgr.cc
new file mode 100644
index 0000000..efa4de5
--- /dev/null
+++ b/src/bin/d2/d2_cfg_mgr.cc
@@ -0,0 +1,213 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/d2_cfg_mgr.h>
+#include <util/encode/hex.h>
+
+#include <boost/foreach.hpp>
+
+namespace isc {
+namespace d2 {
+
+namespace {
+
+typedef std::vector<uint8_t> ByteAddress;
+
+} // end of unnamed namespace
+
+// *********************** D2CfgContext *************************
+
+D2CfgContext::D2CfgContext()
+ : forward_mgr_(new DdnsDomainListMgr("forward_mgr")),
+ reverse_mgr_(new DdnsDomainListMgr("reverse_mgr")),
+ keys_(new TSIGKeyInfoMap()) {
+}
+
+D2CfgContext::D2CfgContext(const D2CfgContext& rhs) : DCfgContextBase(rhs) {
+ if (rhs.forward_mgr_) {
+ forward_mgr_.reset(new DdnsDomainListMgr(rhs.forward_mgr_->getName()));
+ forward_mgr_->setDomains(rhs.forward_mgr_->getDomains());
+ }
+
+ if (rhs.reverse_mgr_) {
+ reverse_mgr_.reset(new DdnsDomainListMgr(rhs.reverse_mgr_->getName()));
+ reverse_mgr_->setDomains(rhs.reverse_mgr_->getDomains());
+ }
+
+ keys_ = rhs.keys_;
+}
+
+D2CfgContext::~D2CfgContext() {
+}
+
+// *********************** D2CfgMgr *************************
+
+const char* D2CfgMgr::IPV4_REV_ZONE_SUFFIX = "in-addr.arpa.";
+
+const char* D2CfgMgr::IPV6_REV_ZONE_SUFFIX = "ip6.arpa.";
+
+D2CfgMgr::D2CfgMgr() : DCfgMgrBase(DCfgContextBasePtr(new D2CfgContext())) {
+ // TSIG keys need to parse before the Domains, so we can catch Domains
+ // that specify undefined keys. Create the necessary parsing order now.
+ addToParseOrder("interface");
+ addToParseOrder("ip_address");
+ addToParseOrder("port");
+ addToParseOrder("tsig_keys");
+ addToParseOrder("forward_ddns");
+ addToParseOrder("reverse_ddns");
+}
+
+D2CfgMgr::~D2CfgMgr() {
+}
+
+bool
+D2CfgMgr::matchForward(const std::string& fqdn, DdnsDomainPtr& domain) {
+ if (fqdn.empty()) {
+ // This is a programmatic error and should not happen.
+ isc_throw(D2CfgError, "matchForward passed an empty fqdn");
+ }
+
+ // Fetch the forward manager from the D2 context.
+ DdnsDomainListMgrPtr mgr = getD2CfgContext()->getForwardMgr();
+
+ // Call the manager's match method and return the result.
+ return (mgr->matchDomain(fqdn, domain));
+}
+
+bool
+D2CfgMgr::matchReverse(const std::string& ip_address, DdnsDomainPtr& domain) {
+ // Note, reverseIpAddress will throw if the ip_address is invalid.
+ std::string reverse_address = reverseIpAddress(ip_address);
+
+ // Fetch the reverse manager from the D2 context.
+ DdnsDomainListMgrPtr mgr = getD2CfgContext()->getReverseMgr();
+
+ return (mgr->matchDomain(reverse_address, domain));
+}
+
+std::string
+D2CfgMgr::reverseIpAddress(const std::string& address) {
+ try {
+ // Convert string address into an IOAddress and invoke the
+ // appropriate reverse method.
+ isc::asiolink::IOAddress ioaddr(address);
+ if (ioaddr.isV4()) {
+ return (reverseV4Address(ioaddr));
+ }
+
+ return (reverseV6Address(ioaddr));
+
+ } catch (const isc::Exception& ex) {
+ isc_throw(D2CfgError, "D2CfgMgr cannot reverse address: "
+ << address << " : " << ex.what());
+ }
+}
+
+std::string
+D2CfgMgr::reverseV4Address(const isc::asiolink::IOAddress& ioaddr) {
+ if (!ioaddr.isV4()) {
+ isc_throw(D2CfgError, "D2CfgMgr address is not IPv4 address :"
+ << ioaddr);
+ }
+
+ // Get the address in byte vector form.
+ const ByteAddress bytes = ioaddr.toBytes();
+
+ // Walk backwards through vector outputting each octet and a dot.
+ std::ostringstream stream;
+
+ // We have to set the following variable to get
+ // const_reverse_iterator type of rend(), otherwise Solaris GCC
+ // complains on operator!= by trying to use the non-const variant.
+ const ByteAddress::const_reverse_iterator end = bytes.rend();
+
+ for (ByteAddress::const_reverse_iterator rit = bytes.rbegin();
+ rit != end;
+ ++rit)
+ {
+ stream << static_cast<unsigned int>(*rit) << ".";
+ }
+
+ // Tack on the suffix and we're done.
+ stream << IPV4_REV_ZONE_SUFFIX;
+ return(stream.str());
+}
+
+std::string
+D2CfgMgr::reverseV6Address(const isc::asiolink::IOAddress& ioaddr) {
+ if (!ioaddr.isV6()) {
+ isc_throw(D2CfgError, "D2Cfg address is not IPv6 address: " << ioaddr);
+ }
+
+ // Turn the address into a string of digits.
+ const ByteAddress bytes = ioaddr.toBytes();
+ const std::string digits = isc::util::encode::encodeHex(bytes);
+
+ // Walk backwards through string outputting each digits and a dot.
+ std::ostringstream stream;
+
+ // We have to set the following variable to get
+ // const_reverse_iterator type of rend(), otherwise Solaris GCC
+ // complains on operator!= by trying to use the non-const variant.
+ const std::string::const_reverse_iterator end = digits.rend();
+
+ for (std::string::const_reverse_iterator rit = digits.rbegin();
+ rit != end;
+ ++rit)
+ {
+ stream << static_cast<char>(*rit) << ".";
+ }
+
+ // Tack on the suffix and we're done.
+ stream << IPV6_REV_ZONE_SUFFIX;
+ return(stream.str());
+}
+
+
+isc::dhcp::ParserPtr
+D2CfgMgr::createConfigParser(const std::string& config_id) {
+ // Get D2 specific context.
+ D2CfgContextPtr context = getD2CfgContext();
+
+ // Create parser instance based on element_id.
+ isc::dhcp::DhcpConfigParser* parser = NULL;
+ if ((config_id == "interface") ||
+ (config_id == "ip_address")) {
+ parser = new isc::dhcp::StringParser(config_id,
+ context->getStringStorage());
+ } else if (config_id == "port") {
+ parser = new isc::dhcp::Uint32Parser(config_id,
+ context->getUint32Storage());
+ } else if (config_id == "forward_ddns") {
+ parser = new DdnsDomainListMgrParser("forward_mgr",
+ context->getForwardMgr(),
+ context->getKeys());
+ } else if (config_id == "reverse_ddns") {
+ parser = new DdnsDomainListMgrParser("reverse_mgr",
+ context->getReverseMgr(),
+ context->getKeys());
+ } else if (config_id == "tsig_keys") {
+ parser = new TSIGKeyInfoListParser("tsig_key_list", context->getKeys());
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: D2CfgMgr parameter not supported: "
+ << config_id);
+ }
+
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
diff --git a/src/bin/d2/d2_cfg_mgr.h b/src/bin/d2/d2_cfg_mgr.h
new file mode 100644
index 0000000..ec19d4c
--- /dev/null
+++ b/src/bin/d2/d2_cfg_mgr.h
@@ -0,0 +1,237 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_CFG_MGR_H
+#define D2_CFG_MGR_H
+
+#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <d2/d2_asio.h>
+#include <d2/d_cfg_mgr.h>
+#include <d2/d2_config.h>
+
+#include <stdint.h>
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+class D2CfgContext;
+/// @brief Pointer to a configuration context.
+typedef boost::shared_ptr<D2CfgContext> D2CfgContextPtr;
+
+/// @brief DHCP-DDNS Configuration Context
+///
+/// Implements the storage container for configuration context.
+/// It provides a single enclosure for the storage of configuration parameters
+/// and any other DHCP-DDNS specific information that needs to be accessible
+/// during configuration parsing as well as to the application as a whole.
+/// It is derived from the context base class, DCfgContextBase.
+class D2CfgContext : public DCfgContextBase {
+public:
+ /// @brief Constructor
+ D2CfgContext();
+
+ /// @brief Destructor
+ virtual ~D2CfgContext();
+
+ /// @brief Creates a clone of this context object.
+ ///
+ /// @return returns a pointer to the new clone.
+ virtual DCfgContextBasePtr clone() {
+ return (DCfgContextBasePtr(new D2CfgContext(*this)));
+ }
+
+ /// @brief Fetches the forward DNS domain list manager.
+ ///
+ /// @return returns a pointer to the forward manager.
+ DdnsDomainListMgrPtr getForwardMgr() {
+ return (forward_mgr_);
+ }
+
+ /// @brief Fetches the reverse DNS domain list manager.
+ ///
+ /// @return returns a pointer to the reverse manager.
+ DdnsDomainListMgrPtr getReverseMgr() {
+ return (reverse_mgr_);
+ }
+
+ /// @brief Fetches the map of TSIG keys.
+ ///
+ /// @return returns a pointer to the key map.
+ TSIGKeyInfoMapPtr getKeys() {
+ return (keys_);
+ }
+
+protected:
+ /// @brief Copy constructor for use by derivations in clone().
+ D2CfgContext(const D2CfgContext& rhs);
+
+private:
+ /// @brief Private assignment operator to avoid potential for slicing.
+ D2CfgContext& operator=(const D2CfgContext& rhs);
+
+ /// @brief Forward domain list manager.
+ DdnsDomainListMgrPtr forward_mgr_;
+
+ /// @brief Reverse domain list manager.
+ DdnsDomainListMgrPtr reverse_mgr_;
+
+ /// @brief Storage for the map of TSIGKeyInfos
+ TSIGKeyInfoMapPtr keys_;
+};
+
+/// @brief Defines a pointer for DdnsDomain instances.
+typedef boost::shared_ptr<DdnsDomainListMgr> DdnsDomainListMgrPtr;
+
+
+
+/// @brief DHCP-DDNS Configuration Manager
+///
+/// Provides the mechanisms for managing the DHCP-DDNS application's
+/// configuration. This includes services for parsing sets of configuration
+/// values, storing the parsed information in its converted form,
+/// and retrieving the information on demand.
+class D2CfgMgr : public DCfgMgrBase {
+public:
+ /// @brief Reverse zone suffix added to IPv4 addresses for reverse lookups
+ /// @todo This should be configurable.
+ static const char* IPV4_REV_ZONE_SUFFIX;
+
+ /// @brief Reverse zone suffix added to IPv6 addresses for reverse lookups
+ /// @todo This should be configurable.
+ static const char* IPV6_REV_ZONE_SUFFIX;
+
+ /// @brief Constructor
+ D2CfgMgr();
+
+ /// @brief Destructor
+ virtual ~D2CfgMgr();
+
+ /// @brief Convenience method that returns the D2 configuration context.
+ ///
+ /// @return returns a pointer to the configuration context.
+ D2CfgContextPtr getD2CfgContext() {
+ return (boost::dynamic_pointer_cast<D2CfgContext>(getContext()));
+ }
+
+ /// @brief Matches a given FQDN to a forward domain.
+ ///
+ /// This calls the matchDomain method of the forward domain manager to
+ /// match the given FQDN to a forward domain.
+ ///
+ /// @param fqdn is the name for which to look.
+ /// @param domain receives the matching domain. Note that it will be reset
+ /// upon entry and only set if a match is subsequently found.
+ ///
+ /// @return returns true if a match is found, false otherwise.
+ /// @throw throws D2CfgError if given an invalid fqdn.
+ bool matchForward(const std::string& fqdn, DdnsDomainPtr& domain);
+
+ /// @brief Matches a given IP address to a reverse domain.
+ ///
+ /// This calls the matchDomain method of the reverse domain manager to
+ /// match the given IPv4 or IPv6 address to a reverse domain.
+ ///
+ /// @param ip_address is the name for which to look.
+ /// @param domain receives the matching domain. Note that it will be reset
+ /// upon entry and only set if a match is subsequently found.
+ ///
+ /// @return returns true if a match is found, false otherwise.
+ /// @throw throws D2CfgError if given an invalid fqdn.
+ bool matchReverse(const std::string& ip_address, DdnsDomainPtr& domain);
+
+ /// @brief Generate a reverse order string for the given IP address
+ ///
+ /// This method creates a string containing the given IP address
+ /// contents in reverse order. This format is used for matching
+ /// against reverse DDNS domains in DHCP_DDNS configuration.
+ /// After reversing the syllables of the address, it appends the
+ /// appropriate suffix.
+ ///
+ /// @param address string containing a valid IPv4 or IPv6 address.
+ ///
+ /// @return a std::string containing the reverse order address.
+ ///
+ /// @throw D2CfgError if given an invalid address.
+ static std::string reverseIpAddress(const std::string& address);
+
+ /// @brief Generate a reverse order string for the given IP address
+ ///
+ /// This method creates a string containing the given IP address
+ /// contents in reverse order. This format is used for matching
+ /// against reverse DDNS domains in DHCP_DDNS configuration.
+ /// After reversing the syllables of the address, it appends the
+ /// appropriate suffix.
+ ///
+ /// Example:
+ /// input: 192.168.1.15
+ /// output: 15.1.168.192.in-addr.arpa.
+ ///
+ /// @param ioaddr is the IPv4 IOaddress to convert
+ ///
+ /// @return a std::string containing the reverse order address.
+ ///
+ /// @throw D2CfgError if not given an IPv4 address.
+ static std::string reverseV4Address(const isc::asiolink::IOAddress& ioaddr);
+
+ /// @brief Generate a reverse order string for the given IP address
+ ///
+ /// This method creates a string containing the given IPv6 address
+ /// contents in reverse order. This format is used for matching
+ /// against reverse DDNS domains in DHCP_DDNS configuration.
+ /// After reversing the syllables of the address, it appends the
+ /// appropriate suffix.
+ ///
+ /// IPv6 example:
+ /// input: 2001:db8:302:99::
+ /// output:
+ ///0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.9.0.0.2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.
+ ///
+ /// @param ioaddr string containing a valid IPv6 address.
+ ///
+ /// @return a std::string containing the reverse order address.
+ ///
+ /// @throw D2CfgError if not given an IPv6 address.
+ static std::string reverseV6Address(const isc::asiolink::IOAddress& ioaddr);
+
+protected:
+ /// @brief Given an element_id returns an instance of the appropriate
+ /// parser.
+ ///
+ /// It is responsible for top-level or outermost DHCP-DDNS configuration
+ /// elements (see dhcp-ddns.spec):
+ /// 1. interface
+ /// 2. ip_address
+ /// 3. port
+ /// 4. forward_ddns
+ /// 5. reverse_ddns
+ ///
+ /// @param element_id is the string name of the element as it will appear
+ /// in the configuration set.
+ ///
+ /// @return returns a ParserPtr to the parser instance.
+ /// @throw throws DCfgMgrBaseError if an error occurs.
+ virtual isc::dhcp::ParserPtr
+ createConfigParser(const std::string& element_id);
+};
+
+/// @brief Defines a shared pointer to D2CfgMgr.
+typedef boost::shared_ptr<D2CfgMgr> D2CfgMgrPtr;
+
+
+}; // end of isc::d2 namespace
+}; // end of isc namespace
+
+#endif // D2_CFG_MGR_H
diff --git a/src/bin/d2/d2_config.cc b/src/bin/d2/d2_config.cc
new file mode 100644
index 0000000..ca1aa4d
--- /dev/null
+++ b/src/bin/d2/d2_config.cc
@@ -0,0 +1,649 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/d2_cfg_mgr.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <exceptions/exceptions.h>
+#include <asiolink/io_error.h>
+
+#include <boost/foreach.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+// *********************** TSIGKeyInfo *************************
+
+TSIGKeyInfo::TSIGKeyInfo(const std::string& name, const std::string& algorithm,
+ const std::string& secret)
+ :name_(name), algorithm_(algorithm), secret_(secret) {
+}
+
+TSIGKeyInfo::~TSIGKeyInfo() {
+}
+
+
+// *********************** DnsServerInfo *************************
+
+const char* DnsServerInfo::EMPTY_IP_STR = "0.0.0.0";
+
+DnsServerInfo::DnsServerInfo(const std::string& hostname,
+ isc::asiolink::IOAddress ip_address, uint32_t port,
+ bool enabled)
+ :hostname_(hostname), ip_address_(ip_address), port_(port),
+ enabled_(enabled) {
+}
+
+DnsServerInfo::~DnsServerInfo() {
+}
+
+std::string
+DnsServerInfo::toText() const {
+ std::ostringstream stream;
+ stream << (getIpAddress().toText()) << " port:" << getPort();
+ return (stream.str());
+}
+
+
+std::ostream&
+operator<<(std::ostream& os, const DnsServerInfo& server) {
+ os << server.toText();
+ return (os);
+}
+
+// *********************** DdnsDomain *************************
+
+DdnsDomain::DdnsDomain(const std::string& name, const std::string& key_name,
+ DnsServerInfoStoragePtr servers)
+ : name_(name), key_name_(key_name), servers_(servers) {
+}
+
+DdnsDomain::~DdnsDomain() {
+}
+
+// *********************** DdnsDomainLstMgr *************************
+
+const char* DdnsDomainListMgr::wildcard_domain_name_ = "*";
+
+DdnsDomainListMgr::DdnsDomainListMgr(const std::string& name) : name_(name),
+ domains_(new DdnsDomainMap()) {
+}
+
+
+DdnsDomainListMgr::~DdnsDomainListMgr () {
+}
+
+void
+DdnsDomainListMgr::setDomains(DdnsDomainMapPtr domains) {
+ if (!domains) {
+ isc_throw(D2CfgError,
+ "DdnsDomainListMgr::setDomains: Domain list may not be null");
+ }
+
+ domains_ = domains;
+
+ // Look for the wild card domain. If present, set the member variable
+ // to remember it. This saves us from having to look for it every time
+ // we attempt a match.
+ DdnsDomainMap::iterator gotit = domains_->find(wildcard_domain_name_);
+ if (gotit != domains_->end()) {
+ wildcard_domain_ = gotit->second;
+ }
+}
+
+bool
+DdnsDomainListMgr::matchDomain(const std::string& fqdn, DdnsDomainPtr& domain) {
+ // First check the case of one domain to rule them all.
+ if ((size() == 1) && (wildcard_domain_)) {
+ domain = wildcard_domain_;
+ return (true);
+ }
+
+ // Iterate over the domain map looking for the domain which matches
+ // the longest portion of the given fqdn.
+
+ size_t req_len = fqdn.size();
+ size_t match_len = 0;
+ DdnsDomainMapPair map_pair;
+ DdnsDomainPtr best_match;
+ BOOST_FOREACH (map_pair, *domains_) {
+ std::string domain_name = map_pair.first;
+ size_t dom_len = domain_name.size();
+
+ // If the domain name is longer than the fqdn, then it cant be match.
+ if (req_len < dom_len) {
+ continue;
+ }
+
+ // If the lengths are identical and the names match we're done.
+ if (req_len == dom_len) {
+ if (fqdn == domain_name) {
+ // exact match, done
+ domain = map_pair.second;
+ return (true);
+ }
+ } else {
+ // The fqdn is longer than the domain name. Adjust the start
+ // point of comparison by the excess in length. Only do the
+ // comparison if the adjustment lands on a boundary. This
+ // prevents "onetwo.net" from matching "two.net".
+ size_t offset = req_len - dom_len;
+ if ((fqdn[offset - 1] == '.') &&
+ (fqdn.compare(offset, std::string::npos, domain_name) == 0)) {
+ // Fqdn contains domain name, keep it if its better than
+ // any we have matched so far.
+ if (dom_len > match_len) {
+ match_len = dom_len;
+ best_match = map_pair.second;
+ }
+ }
+ }
+ }
+
+ if (!best_match) {
+ // There's no match. If they specified a wild card domain use it
+ // otherwise there's no domain for this entry.
+ if (wildcard_domain_) {
+ domain = wildcard_domain_;
+ return (true);
+ }
+
+ LOG_WARN(dctl_logger, DHCP_DDNS_NO_MATCH).arg(fqdn);
+ return (false);
+ }
+
+ domain = best_match;
+ return (true);
+}
+
+// *************************** PARSERS ***********************************
+
+// *********************** TSIGKeyInfoParser *************************
+
+TSIGKeyInfoParser::TSIGKeyInfoParser(const std::string& entry_name,
+ TSIGKeyInfoMapPtr keys)
+ : entry_name_(entry_name), keys_(keys), local_scalars_() {
+ if (!keys_) {
+ isc_throw(D2CfgError, "TSIGKeyInfoParser ctor:"
+ " key storage cannot be null");
+ }
+}
+
+TSIGKeyInfoParser::~TSIGKeyInfoParser() {
+}
+
+void
+TSIGKeyInfoParser::build(isc::data::ConstElementPtr key_config) {
+ isc::dhcp::ConfigPair config_pair;
+ // For each element in the key configuration:
+ // 1. Create a parser for the element.
+ // 2. Invoke the parser's build method passing in the element's
+ // configuration.
+ // 3. Invoke the parser's commit method to store the element's parsed
+ // data to the parser's local storage.
+ BOOST_FOREACH (config_pair, key_config->mapValue()) {
+ isc::dhcp::ParserPtr parser(createConfigParser(config_pair.first));
+ parser->build(config_pair.second);
+ parser->commit();
+ }
+}
+
+isc::dhcp::ParserPtr
+TSIGKeyInfoParser::createConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ // Based on the configuration id of the element, create the appropriate
+ // parser. Scalars are set to use the parser's local scalar storage.
+ if ((config_id == "name") ||
+ (config_id == "algorithm") ||
+ (config_id == "secret")) {
+ parser = new isc::dhcp::StringParser(config_id,
+ local_scalars_.getStringStorage());
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: TSIGKeyInfo parameter not supported: "
+ << config_id);
+ }
+
+ // Return the new parser instance.
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+void
+TSIGKeyInfoParser::commit() {
+ std::string name;
+ std::string algorithm;
+ std::string secret;
+
+ // Fetch the key configuration's parsed scalar values from parser's
+ // local storage.
+ local_scalars_.getParam("name", name);
+ local_scalars_.getParam("algorithm", algorithm);
+ local_scalars_.getParam("secret", secret);
+
+ // @todo Validation here is very superficial. This will expand as TSIG
+ // Key use is more fully implemented.
+
+ // Name cannot be blank.
+ if (name.empty()) {
+ isc_throw(D2CfgError, "TSIG Key Info must specify name");
+ }
+
+ // Algorithm cannot be blank.
+ if (algorithm.empty()) {
+ isc_throw(D2CfgError, "TSIG Key Info must specify algorithm");
+ }
+
+ // Secret cannot be blank.
+ if (secret.empty()) {
+ isc_throw(D2CfgError, "TSIG Key Info must specify secret");
+ }
+
+ // Currently, the premise is that key storage is always empty prior to
+ // parsing so we are always adding keys never replacing them. Duplicates
+ // are not allowed and should be flagged as a configuration error.
+ if (keys_->find(name) != keys_->end()) {
+ isc_throw(D2CfgError, "Duplicate TSIG key specified:" << name);
+ }
+
+ TSIGKeyInfoPtr key_info(new TSIGKeyInfo(name, algorithm, secret));
+
+ // Add the new TSIGKeyInfo to the key storage.
+ (*keys_)[name]=key_info;
+}
+
+// *********************** TSIGKeyInfoListParser *************************
+
+TSIGKeyInfoListParser::TSIGKeyInfoListParser(const std::string& list_name,
+ TSIGKeyInfoMapPtr keys)
+ :list_name_(list_name), keys_(keys), local_keys_(new TSIGKeyInfoMap()),
+ parsers_() {
+ if (!keys_) {
+ isc_throw(D2CfgError, "TSIGKeyInfoListParser ctor:"
+ " key storage cannot be null");
+ }
+}
+
+TSIGKeyInfoListParser::~TSIGKeyInfoListParser(){
+}
+
+void
+TSIGKeyInfoListParser::
+build(isc::data::ConstElementPtr key_list){
+ int i = 0;
+ isc::data::ConstElementPtr key_config;
+ // For each key element in the key list:
+ // 1. Create a parser for the key element.
+ // 2. Invoke the parser's build method passing in the key's
+ // configuration.
+ // 3. Add the parser to a local collection of parsers.
+ BOOST_FOREACH(key_config, key_list->listValue()) {
+ // Create a name for the parser based on its position in the list.
+ std::string entry_name = boost::lexical_cast<std::string>(i++);
+ isc::dhcp::ParserPtr parser(new TSIGKeyInfoParser(entry_name,
+ local_keys_));
+ parser->build(key_config);
+ parsers_.push_back(parser);
+ }
+}
+
+void
+TSIGKeyInfoListParser::commit() {
+ // Invoke commit on each server parser. This will cause each one to
+ // create it's server instance and commit it to storage.
+ BOOST_FOREACH(isc::dhcp::ParserPtr parser, parsers_) {
+ parser->commit();
+ }
+
+ // Now that we know we have a valid list, commit that list to the
+ // area given to us during construction (i.e. to the d2 context).
+ *keys_ = *local_keys_;
+}
+
+// *********************** DnsServerInfoParser *************************
+
+DnsServerInfoParser::DnsServerInfoParser(const std::string& entry_name,
+ DnsServerInfoStoragePtr servers)
+ : entry_name_(entry_name), servers_(servers), local_scalars_() {
+ if (!servers_) {
+ isc_throw(D2CfgError, "DnsServerInfoParser ctor:"
+ " server storage cannot be null");
+ }
+}
+
+DnsServerInfoParser::~DnsServerInfoParser() {
+}
+
+void
+DnsServerInfoParser::build(isc::data::ConstElementPtr server_config) {
+ isc::dhcp::ConfigPair config_pair;
+ // For each element in the server configuration:
+ // 1. Create a parser for the element.
+ // 2. Invoke the parser's build method passing in the element's
+ // configuration.
+ // 3. Invoke the parser's commit method to store the element's parsed
+ // data to the parser's local storage.
+ BOOST_FOREACH (config_pair, server_config->mapValue()) {
+ isc::dhcp::ParserPtr parser(createConfigParser(config_pair.first));
+ parser->build(config_pair.second);
+ parser->commit();
+ }
+
+}
+
+isc::dhcp::ParserPtr
+DnsServerInfoParser::createConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ // Based on the configuration id of the element, create the appropriate
+ // parser. Scalars are set to use the parser's local scalar storage.
+ if ((config_id == "hostname") ||
+ (config_id == "ip_address")) {
+ parser = new isc::dhcp::StringParser(config_id,
+ local_scalars_.getStringStorage());
+ } else if (config_id == "port") {
+ parser = new isc::dhcp::Uint32Parser(config_id,
+ local_scalars_.getUint32Storage());
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: DnsServerInfo parameter not supported: "
+ << config_id);
+ }
+
+ // Return the new parser instance.
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+void
+DnsServerInfoParser::commit() {
+ std::string hostname;
+ std::string ip_address;
+ uint32_t port = DnsServerInfo::STANDARD_DNS_PORT;
+
+ // Fetch the server configuration's parsed scalar values from parser's
+ // local storage.
+ local_scalars_.getParam("hostname", hostname, DCfgContextBase::OPTIONAL);
+ local_scalars_.getParam("ip_address", ip_address,
+ DCfgContextBase::OPTIONAL);
+ local_scalars_.getParam("port", port, DCfgContextBase::OPTIONAL);
+
+ // The configuration must specify one or the other.
+ if (hostname.empty() == ip_address.empty()) {
+ isc_throw(D2CfgError, "Dns Server must specify one or the other"
+ " of hostname and IP address");
+ }
+
+ DnsServerInfoPtr serverInfo;
+ if (!hostname.empty()) {
+ // When hostname is specified, create a valid, blank IOAddress and
+ // then create the DnsServerInfo.
+ isc::asiolink::IOAddress io_addr(DnsServerInfo::EMPTY_IP_STR);
+ serverInfo.reset(new DnsServerInfo(hostname, io_addr, port));
+ } else {
+ try {
+ // Create an IOAddress from the IP address string given and then
+ // create the DnsServerInfo.
+ isc::asiolink::IOAddress io_addr(ip_address);
+ serverInfo.reset(new DnsServerInfo(hostname, io_addr, port));
+ } catch (const isc::asiolink::IOError& ex) {
+ isc_throw(D2CfgError, "Invalid IP address:" << ip_address);
+ }
+ }
+
+ // Add the new DnsServerInfo to the server storage.
+ servers_->push_back(serverInfo);
+}
+
+// *********************** DnsServerInfoListParser *************************
+
+DnsServerInfoListParser::DnsServerInfoListParser(const std::string& list_name,
+ DnsServerInfoStoragePtr servers)
+ :list_name_(list_name), servers_(servers), parsers_() {
+ if (!servers_) {
+ isc_throw(D2CfgError, "DdnsServerInfoListParser ctor:"
+ " server storage cannot be null");
+ }
+}
+
+DnsServerInfoListParser::~DnsServerInfoListParser(){
+}
+
+void
+DnsServerInfoListParser::
+build(isc::data::ConstElementPtr server_list){
+ int i = 0;
+ isc::data::ConstElementPtr server_config;
+ // For each server element in the server list:
+ // 1. Create a parser for the server element.
+ // 2. Invoke the parser's build method passing in the server's
+ // configuration.
+ // 3. Add the parser to a local collection of parsers.
+ BOOST_FOREACH(server_config, server_list->listValue()) {
+ // Create a name for the parser based on its position in the list.
+ std::string entry_name = boost::lexical_cast<std::string>(i++);
+ isc::dhcp::ParserPtr parser(new DnsServerInfoParser(entry_name,
+ servers_));
+ parser->build(server_config);
+ parsers_.push_back(parser);
+ }
+}
+
+void
+DnsServerInfoListParser::commit() {
+ // Domains must have at least one server.
+ if (parsers_.size() == 0) {
+ isc_throw (D2CfgError, "Server List must contain at least one server");
+ }
+
+ // Invoke commit on each server parser. This will cause each one to
+ // create it's server instance and commit it to storage.
+ BOOST_FOREACH(isc::dhcp::ParserPtr parser, parsers_) {
+ parser->commit();
+ }
+}
+
+// *********************** DdnsDomainParser *************************
+
+DdnsDomainParser::DdnsDomainParser(const std::string& entry_name,
+ DdnsDomainMapPtr domains,
+ TSIGKeyInfoMapPtr keys)
+ : entry_name_(entry_name), domains_(domains), keys_(keys),
+ local_servers_(new DnsServerInfoStorage()), local_scalars_() {
+ if (!domains_) {
+ isc_throw(D2CfgError,
+ "DdnsDomainParser ctor, domain storage cannot be null");
+ }
+}
+
+
+DdnsDomainParser::~DdnsDomainParser() {
+}
+
+void
+DdnsDomainParser::build(isc::data::ConstElementPtr domain_config) {
+ // For each element in the domain configuration:
+ // 1. Create a parser for the element.
+ // 2. Invoke the parser's build method passing in the element's
+ // configuration.
+ // 3. Invoke the parser's commit method to store the element's parsed
+ // data to the parser's local storage.
+ isc::dhcp::ConfigPair config_pair;
+ BOOST_FOREACH(config_pair, domain_config->mapValue()) {
+ isc::dhcp::ParserPtr parser(createConfigParser(config_pair.first));
+ parser->build(config_pair.second);
+ parser->commit();
+ }
+}
+
+isc::dhcp::ParserPtr
+DdnsDomainParser::createConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ // Based on the configuration id of the element, create the appropriate
+ // parser. Scalars are set to use the parser's local scalar storage.
+ if ((config_id == "name") ||
+ (config_id == "key_name")) {
+ parser = new isc::dhcp::StringParser(config_id,
+ local_scalars_.getStringStorage());
+ } else if (config_id == "dns_servers") {
+ // Server list parser is given in our local server storage. It will pass
+ // this down to its server parsers and is where they will write their
+ // server instances upon commit.
+ parser = new DnsServerInfoListParser(config_id, local_servers_);
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: DdnsDomain parameter not supported: "
+ << config_id);
+ }
+
+ // Return the new domain parser instance.
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+void
+DdnsDomainParser::commit() {
+ std::string name;
+ std::string key_name;
+
+ // Domain name is not optional. The get will throw if its not there.
+ local_scalars_.getParam("name", name);
+
+ // Blank domain names are not allowed.
+ if (name.empty()) {
+ isc_throw(D2CfgError, "Domain name cannot be blank");
+ }
+
+ // Currently, the premise is that domain storage is always empty
+ // prior to parsing so always adding domains never replacing them.
+ // Duplicates are not allowed and should be flagged as a configuration
+ // error.
+ if (domains_->find(name) != domains_->end()) {
+ isc_throw(D2CfgError, "Duplicate domain specified:" << name);
+ }
+
+ // Key name is optional. If it is not blank, then validate it against
+ // the defined list of keys.
+ local_scalars_.getParam("key_name", key_name, DCfgContextBase::OPTIONAL);
+ if (!key_name.empty()) {
+ if ((!keys_) || (keys_->find(key_name) == keys_->end())) {
+ isc_throw(D2CfgError, "DdnsDomain :" << name <<
+ " specifies and undefined key:" << key_name);
+ }
+ }
+
+ // Instantiate the new domain and add it to domain storage.
+ DdnsDomainPtr domain(new DdnsDomain(name, key_name, local_servers_));
+
+ // Add the new domain to the domain storage.
+ (*domains_)[name]=domain;
+}
+
+// *********************** DdnsDomainListParser *************************
+
+DdnsDomainListParser::DdnsDomainListParser(const std::string& list_name,
+ DdnsDomainMapPtr domains,
+ TSIGKeyInfoMapPtr keys)
+ :list_name_(list_name), domains_(domains), keys_(keys), parsers_() {
+ if (!domains_) {
+ isc_throw(D2CfgError, "DdnsDomainListParser ctor:"
+ " domain storage cannot be null");
+ }
+}
+
+DdnsDomainListParser::~DdnsDomainListParser(){
+}
+
+void
+DdnsDomainListParser::
+build(isc::data::ConstElementPtr domain_list){
+ // For each domain element in the domain list:
+ // 1. Create a parser for the domain element.
+ // 2. Invoke the parser's build method passing in the domain's
+ // configuration.
+ // 3. Add the parser to the local collection of parsers.
+ int i = 0;
+ isc::data::ConstElementPtr domain_config;
+ BOOST_FOREACH(domain_config, domain_list->listValue()) {
+ std::string entry_name = boost::lexical_cast<std::string>(i++);
+ isc::dhcp::ParserPtr parser(new DdnsDomainParser(entry_name,
+ domains_, keys_));
+ parser->build(domain_config);
+ parsers_.push_back(parser);
+ }
+}
+
+void
+DdnsDomainListParser::commit() {
+ // Invoke commit on each server parser. This will cause each one to
+ // create it's server instance and commit it to storage.
+ BOOST_FOREACH(isc::dhcp::ParserPtr parser, parsers_) {
+ parser->commit();
+ }
+}
+
+
+// *********************** DdnsDomainListMgrParser *************************
+
+DdnsDomainListMgrParser::DdnsDomainListMgrParser(const std::string& entry_name,
+ DdnsDomainListMgrPtr mgr, TSIGKeyInfoMapPtr keys)
+ : entry_name_(entry_name), mgr_(mgr), keys_(keys),
+ local_domains_(new DdnsDomainMap()), local_scalars_() {
+}
+
+
+DdnsDomainListMgrParser::~DdnsDomainListMgrParser() {
+}
+
+void
+DdnsDomainListMgrParser::build(isc::data::ConstElementPtr domain_config) {
+ // For each element in the domain manager configuration:
+ // 1. Create a parser for the element.
+ // 2. Invoke the parser's build method passing in the element's
+ // configuration.
+ // 3. Invoke the parser's commit method to store the element's parsed
+ // data to the parser's local storage.
+ isc::dhcp::ConfigPair config_pair;
+ BOOST_FOREACH(config_pair, domain_config->mapValue()) {
+ isc::dhcp::ParserPtr parser(createConfigParser(config_pair.first));
+ parser->build(config_pair.second);
+ parser->commit();
+ }
+}
+
+isc::dhcp::ParserPtr
+DdnsDomainListMgrParser::createConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ if (config_id == "ddns_domains") {
+ // Domain list parser is given our local domain storage. It will pass
+ // this down to its domain parsers and is where they will write their
+ // domain instances upon commit.
+ parser = new DdnsDomainListParser(config_id, local_domains_, keys_);
+ } else {
+ isc_throw(NotImplemented, "parser error: "
+ "DdnsDomainListMgr parameter not supported: " << config_id);
+ }
+
+ // Return the new domain parser instance.
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+void
+DdnsDomainListMgrParser::commit() {
+ // Add the new domain to the domain storage.
+ mgr_->setDomains(local_domains_);
+}
+
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
diff --git a/src/bin/d2/d2_config.h b/src/bin/d2/d2_config.h
new file mode 100644
index 0000000..9212ee2
--- /dev/null
+++ b/src/bin/d2/d2_config.h
@@ -0,0 +1,959 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_CONFIG_H
+#define D2_CONFIG_H
+
+#include <cc/data.h>
+#include <d2/d2_asio.h>
+#include <d2/d_cfg_mgr.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <exceptions/exceptions.h>
+
+#include <boost/foreach.hpp>
+
+#include <stdint.h>
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+/// @file d2_config.h
+/// @brief A collection of classes for housing and parsing the application
+/// configuration necessary for the DHCP-DDNS application (aka D2).
+///
+/// This file contains the class declarations for the class hierarchy created
+/// from the D2 configuration and the parser classes used to create it.
+/// The application configuration consists of a set of scalar parameters,
+/// a list of TSIG keys, and two managed lists of domains: one list for
+/// forward domains and one list for reverse domains.
+///
+/// The key list consists of one or more TSIG keys, each entry described by
+/// a name, the algorithm method name, and its secret key component.
+///
+/// @todo NOTE that TSIG configuration parsing is functional, the use of
+/// TSIG Keys during the actual DNS update transactions is not. This will be
+/// implemented in a future release.
+///
+/// Each managed domain list consists of a list one or more domains and is
+/// represented by the class DdnsDomainListMgr.
+///
+/// Each domain consists of a set of scalars parameters and a list of DNS
+/// servers which support that domain. Among its scalars, is key_name, which
+/// is the name of the TSIG Key to use for with this domain. This value should
+/// map to one of the TSIG Keys in the key list. Domains are represented by
+/// the class, DdnsDomain.
+///
+/// Each server consists of a set of scalars used to describe the server such
+/// that the application can carry out DNS update exchanges with it. Servers
+/// are represented by the class, DnsServerInfo.
+///
+/// The configuration specification for use with BIND10 is detailed in the file
+/// dhcp-ddns.spec.
+///
+/// The parsing class hierarchy reflects this same scheme. Working top down:
+///
+/// A DdnsDomainListMgrParser parses a managed domain list entry. It handles
+/// any scalars which belong to the manager as well as creating and invoking a
+/// DdnsDomainListParser to parse its list of domain entries.
+///
+/// A DdnsDomainLiatParser creates and invokes DdnsDomainListParser for each
+/// domain entry in its list.
+///
+/// A DdnsDomainParser handles the scalars which belong to the domain as well as
+/// creating and invoking a DnsSeverInfoListParser to parse its list of server
+/// entries.
+///
+/// A DnsServerInfoListParser creates and invokes a DnsServerInfoParser for
+/// each server entry in its list.
+///
+/// A DdnsServerInfoParser handles the scalars which belong to the server.
+/// The following is sample configuration in JSON form with extra spacing
+/// for clarity:
+///
+/// @code
+/// {
+/// "interface" : "eth1" ,
+/// "ip_address" : "192.168.1.33" ,
+/// "port" : 88 ,
+/// "tsig_keys":
+//// [
+/// {
+/// "name": "d2_key.tmark.org" ,
+/// "algorithm": "md5" ,
+/// "secret": "0123456989"
+/// }
+/// ],
+/// "forward_ddns" :
+/// {
+/// "ddns_domains":
+/// [
+/// {
+/// "name": "tmark.org." ,
+/// "key_name": "d2_key.tmark.org" ,
+/// "dns_servers" :
+/// [
+/// { "hostname": "fserver.tmark.org" },
+/// { "hostname": "f2server.tmark.org" }
+/// ]
+/// },
+/// {
+/// "name": "pub.tmark.org." ,
+/// "key_name": "d2_key.tmark.org" ,
+/// "dns_servers" :
+/// [
+/// { "hostname": "f3server.tmark.org" }
+/// ]
+/// }
+/// ]
+/// },
+/// "reverse_ddns" :
+/// {
+/// "ddns_domains":
+/// [
+/// {
+/// "name": " 0.168.192.in.addr.arpa." ,
+/// "key_name": "d2_key.tmark.org" ,
+/// "dns_servers" :
+/// [
+/// { "ip_address": "127.0.0.101" , "port": 100 }
+/// ]
+/// }
+/// ]
+/// }
+/// }
+/// @endcode
+
+/// @brief Exception thrown when the error during configuration handling
+/// occurs.
+class D2CfgError : public isc::Exception {
+public:
+ D2CfgError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Represents a TSIG Key.
+///
+/// Currently, this is simple storage class containing the basic attributes of
+/// a TSIG Key. It is intended primarily as a reference for working with
+/// actual keys and may eventually be replaced by isc::dns::TSIGKey. TSIG Key
+/// functionality at this stage is strictly limited to configuration parsing.
+/// @todo full functionality for using TSIG during DNS updates will be added
+/// in a future release.
+class TSIGKeyInfo {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param name the unique label used to identify this key
+ /// @param algorithm the name of the encryption alogirthm this key uses.
+ /// (@todo This will be a fixed list of choices)
+ ///
+ /// @param secret the secret component of this key
+ TSIGKeyInfo(const std::string& name, const std::string& algorithm,
+ const std::string& secret);
+
+ /// @brief Destructor
+ virtual ~TSIGKeyInfo();
+
+ /// @brief Getter which returns the key's name.
+ ///
+ /// @return returns the name as as std::string.
+ const std::string getName() const {
+ return (name_);
+ }
+
+ /// @brief Getter which returns the key's algorithm.
+ ///
+ /// @return returns the algorithm as as std::string.
+ const std::string getAlgorithm() const {
+ return (algorithm_);
+ }
+
+ /// @brief Getter which returns the key's secret.
+ ///
+ /// @return returns the secret as as std::string.
+ const std::string getSecret() const {
+ return (secret_);
+ }
+
+private:
+ /// @brief The name of the key.
+ ///
+ /// This value is the unique identifeir thay domains use to
+ /// to specify which TSIG key they need.
+ std::string name_;
+
+ /// @brief The algorithm that should be used for this key.
+ std::string algorithm_;
+
+ /// @brief The secret value component of this key.
+ std::string secret_;
+};
+
+/// @brief Defines a pointer for TSIGKeyInfo instances.
+typedef boost::shared_ptr<TSIGKeyInfo> TSIGKeyInfoPtr;
+
+/// @brief Defines a map of TSIGKeyInfos, keyed by the name.
+typedef std::map<std::string, TSIGKeyInfoPtr> TSIGKeyInfoMap;
+
+/// @brief Defines a iterator pairing of name and TSIGKeyInfo
+typedef std::pair<std::string, TSIGKeyInfoPtr> TSIGKeyInfoMapPair;
+
+/// @brief Defines a pointer to map of TSIGkeyInfos
+typedef boost::shared_ptr<TSIGKeyInfoMap> TSIGKeyInfoMapPtr;
+
+
+/// @brief Represents a specific DNS Server.
+/// It provides information about the server's network identity and typically
+/// belongs to a list of servers supporting DNS for a given domain. It will
+/// be used to establish communications with the server to carry out DNS
+/// updates.
+class DnsServerInfo {
+public:
+
+ /// @brief defines DNS standard port value
+ static const uint32_t STANDARD_DNS_PORT = 53;
+
+ /// @brief defines an "empty" string version of an ip address.
+ static const char* EMPTY_IP_STR;
+
+
+ /// @brief Constructor
+ ///
+ /// @param hostname is the resolvable name of the server. If not blank,
+ /// then the server address should be resolved at runtime.
+ /// @param ip_address is the static IP address of the server. If hostname
+ /// is blank, then this address should be used to connect to the server.
+ /// @param port is the port number on which the server listens.
+ /// primarily meant for testing purposes. Normally, DNS traffic is on
+ /// is port 53. (NOTE the constructing code is responsible for setting
+ /// the default.)
+ /// @param enabled is a flag that indicates whether this server is
+ /// enabled for use. It defaults to true.
+ DnsServerInfo(const std::string& hostname,
+ isc::asiolink::IOAddress ip_address,
+ uint32_t port = STANDARD_DNS_PORT,
+ bool enabled=true);
+
+ /// @brief Destructor
+ virtual ~DnsServerInfo();
+
+ /// @brief Getter which returns the server's hostname.
+ ///
+ /// @return returns the hostname as as std::string.
+ const std::string getHostname() const {
+ return (hostname_);
+ }
+
+ /// @brief Getter which returns the server's port number.
+ ///
+ /// @return returns the port number as a unsigned integer.
+ uint32_t getPort() const {
+ return (port_);
+ }
+
+ /// @brief Getter which returns the server's ip_address.
+ ///
+ /// @return returns the address as an IOAddress reference.
+ const isc::asiolink::IOAddress& getIpAddress() const {
+ return (ip_address_);
+ }
+
+ /// @brief Convenience method which returns whether or not the
+ /// server is enabled.
+ ///
+ /// @return returns true if the server is enabled, false otherwise.
+ bool isEnabled() const {
+ return (enabled_);
+ }
+
+ /// @brief Sets the server's enabled flag to true.
+ void enable() {
+ enabled_ = true;
+ }
+
+ /// @brief Sets the server's enabled flag to false.
+ void disable() {
+ enabled_ = false;
+ }
+
+ /// @brief Returns a text representation for the server.
+ std::string toText() const;
+
+
+private:
+ /// @brief The resolvable name of the server. If not blank, then the
+ /// server's IP address should be dynamically resolved at runtime.
+ std::string hostname_;
+
+ /// @brief The static IP address of the server. When hostname is blank,
+ /// then this address should be used to connect to the server.
+ isc::asiolink::IOAddress ip_address_;
+
+ /// @brief The port number on which the server listens for DNS traffic.
+ uint32_t port_;
+
+ /// @param enabled is a flag that indicates whether this server is
+ /// enabled for use. It defaults to true.
+ bool enabled_;
+};
+
+std::ostream&
+operator<<(std::ostream& os, const DnsServerInfo& server);
+
+/// @brief Defines a pointer for DnsServerInfo instances.
+typedef boost::shared_ptr<DnsServerInfo> DnsServerInfoPtr;
+
+/// @brief Defines a storage container for DnsServerInfo pointers.
+typedef std::vector<DnsServerInfoPtr> DnsServerInfoStorage;
+
+/// @brief Defines a pointer to DnsServerInfo storage containers.
+typedef boost::shared_ptr<DnsServerInfoStorage> DnsServerInfoStoragePtr;
+
+
+/// @brief Represents a DNS domain that is may be updated dynamically.
+/// This class specifies a DNS domain and the list of DNS servers that support
+/// it. It's primary use is to map a domain to the DNS server(s) responsible
+/// for it.
+/// @todo Currently the name entry for a domain is just an std::string. It
+/// may be worthwhile to change this to a dns::Name for purposes of better
+/// validation and matching capabilities.
+class DdnsDomain {
+public:
+ /// @brief Constructor
+ ///
+ /// @param name is the domain name of the domain.
+ /// @param key_name is the TSIG key name for use with this domain.
+ /// @param servers is the list of server(s) supporting this domain.
+ DdnsDomain(const std::string& name, const std::string& key_name,
+ DnsServerInfoStoragePtr servers);
+
+ /// @brief Destructor
+ virtual ~DdnsDomain();
+
+ /// @brief Getter which returns the domain's name.
+ ///
+ /// @return returns the name in an std::string.
+ const std::string getName() const {
+ return (name_);
+ }
+
+ /// @brief Getter which returns the domain's TSIG key name.
+ ///
+ /// @return returns the key name in an std::string.
+ const std::string getKeyName() const {
+ return (key_name_);
+ }
+
+ /// @brief Getter which returns the domain's list of servers.
+ ///
+ /// @return returns the pointer to the server storage.
+ const DnsServerInfoStoragePtr& getServers() {
+ return (servers_);
+ }
+
+private:
+ /// @brief The domain name of the domain.
+ std::string name_;
+
+ /// @brief The name of the TSIG key for use with this domain.
+ std::string key_name_;
+
+ /// @brief The list of server(s) supporting this domain.
+ DnsServerInfoStoragePtr servers_;
+};
+
+/// @brief Defines a pointer for DdnsDomain instances.
+typedef boost::shared_ptr<DdnsDomain> DdnsDomainPtr;
+
+/// @brief Defines a map of DdnsDomains, keyed by the domain name.
+typedef std::map<std::string, DdnsDomainPtr> DdnsDomainMap;
+
+/// @brief Defines a iterator pairing domain name and DdnsDomain
+typedef std::pair<std::string, DdnsDomainPtr> DdnsDomainMapPair;
+
+/// @brief Defines a pointer to DdnsDomain storage containers.
+typedef boost::shared_ptr<DdnsDomainMap> DdnsDomainMapPtr;
+
+/// @brief Provides storage for and management of a list of DNS domains.
+/// In addition to housing the domain list storage, it provides domain matching
+/// services. These services are used to match a FQDN to a domain. Currently
+/// it supports a single matching service, which will return the matching
+/// domain or a wild card domain if one is specified. The wild card domain is
+/// specified as a domain whose name is "*". The wild card domain will match
+/// any entry and is provided for flexibility in FQDNs If for instance, all
+/// forward requests are handled by the same servers, the configuration could
+/// specify the wild card domain as the only forward domain. All forward DNS
+/// updates would be sent to that one list of servers, regardless of the FQDN.
+/// As matching capabilities evolve this class is expected to expand.
+class DdnsDomainListMgr {
+public:
+ /// @brief defines the domain name for denoting the wildcard domain.
+ static const char* wildcard_domain_name_;
+
+ /// @brief Constructor
+ ///
+ /// @param name is an arbitrary label assigned to this manager.
+ DdnsDomainListMgr(const std::string& name);
+
+ /// @brief Destructor
+ virtual ~DdnsDomainListMgr ();
+
+ /// @brief Matches a given name to a domain based on a longest match
+ /// scheme.
+ ///
+ /// Given a FQDN, search the list of domains, successively removing a
+ /// sub-domain from the FQDN until a match is found. If no match is found
+ /// and the wild card domain is present in the list, then return it as the
+ /// match. If the wild card domain is the only domain in the list, then
+ /// it will be returned immediately for any FQDN.
+ ///
+ /// @param fqdn is the name for which to look.
+ /// @param domain receives the matching domain. If no match is found its
+ /// contents will be unchanged.
+ ///
+ /// @return returns true if a match is found, false otherwise.
+ /// @todo This is a very basic match method, which expects valid FQDNs
+ /// both as input and for the DdnsDomain::getName(). Currently both are
+ /// simple strings and there is no normalization (i.e. added trailing dots
+ /// if missing).
+ virtual bool matchDomain(const std::string& fqdn, DdnsDomainPtr& domain);
+
+ /// @brief Fetches the manager's name.
+ ///
+ /// @return returns a std::string containing the name of the manager.
+ const std::string getName() const {
+ return (name_);
+ }
+
+ /// @brief Returns the number of domains in the domain list.
+ ///
+ /// @brief returns an unsigned int containing the domain count.
+ uint32_t size() const {
+ return (domains_->size());
+ }
+
+ /// @brief Fetches the wild card domain.
+ ///
+ /// @return returns a pointer reference to the domain. The pointer will
+ /// empty if the wild card domain is not present.
+ const DdnsDomainPtr& getWildcardDomain() {
+ return (wildcard_domain_);
+ }
+
+ /// @brief Fetches the domain list.
+ ///
+ /// @return returns a pointer reference to the list of domains.
+ const DdnsDomainMapPtr &getDomains() {
+ return (domains_);
+ }
+
+ /// @brief Sets the manger's domain list to the given list of domains.
+ /// This method will scan the inbound list for the wild card domain and
+ /// set the internal wild card domain pointer accordingly.
+ void setDomains(DdnsDomainMapPtr domains);
+
+private:
+ /// @brief An arbitrary label assigned to this manager.
+ std::string name_;
+
+ /// @brief Map of the domains, keyed by name.
+ DdnsDomainMapPtr domains_;
+
+ /// @brief Pointer to the wild card domain.
+ DdnsDomainPtr wildcard_domain_;
+};
+
+/// @brief Defines a pointer for DdnsDomain instances.
+typedef boost::shared_ptr<DdnsDomainListMgr> DdnsDomainListMgrPtr;
+
+/// @brief Storage container for scalar configuration parameters.
+///
+/// This class is useful for implementing parsers for more complex configuration
+/// elements (e.g. those of item type "map"). It provides a convenient way to
+/// add storage to the parser for an arbitrary number and variety of scalar
+/// configuration items (e.g. ints, bools, strings...) without explicitly adding
+/// storage for each individual type needed by the parser.
+///
+/// This class implements a concrete version of the base class by supplying a
+/// "clone" method.
+class DScalarContext : public DCfgContextBase {
+public:
+
+ /// @brief Constructor
+ DScalarContext() {
+ };
+
+ /// @brief Destructor
+ virtual ~DScalarContext() {
+ }
+
+ /// @brief Creates a clone of a DStubContext.
+ ///
+ /// @return returns a pointer to the new clone.
+ virtual DCfgContextBasePtr clone() {
+ return (DCfgContextBasePtr(new DScalarContext(*this)));
+ }
+
+protected:
+ /// @brief Copy constructor
+ DScalarContext(const DScalarContext& rhs) : DCfgContextBase(rhs) {
+ }
+
+private:
+ /// @brief Private assignment operator, not implemented.
+ DScalarContext& operator=(const DScalarContext& rhs);
+};
+
+/// @brief Defines a pointer for DScalarContext instances.
+typedef boost::shared_ptr<DScalarContext> DScalarContextPtr;
+
+/// @brief Parser for TSIGKeyInfo
+///
+/// This class parses the configuration element "tsig_key" defined in
+/// src/bin/d2/dhcp-ddns.spec and creates an instance of a TSIGKeyInfo.
+class TSIGKeyInfoParser : public isc::dhcp::DhcpConfigParser {
+public:
+ /// @brief Constructor
+ ///
+ /// @param entry_name is an arbitrary label assigned to this configuration
+ /// definition. Since servers are specified in a list this value is likely
+ /// be something akin to "key:0", set during parsing.
+ /// @param keys is a pointer to the storage area to which the parser
+ /// should commit the newly created TSIGKeyInfo instance.
+ TSIGKeyInfoParser(const std::string& entry_name, TSIGKeyInfoMapPtr keys);
+
+ /// @brief Destructor
+ virtual ~TSIGKeyInfoParser();
+
+ /// @brief Performs the actual parsing of the given "tsig_key" element.
+ ///
+ /// The results of the parsing are retained internally for use during
+ /// commit.
+ ///
+ /// @param key_config is the "tsig_key" configuration to parse
+ virtual void build(isc::data::ConstElementPtr key_config);
+
+ /// @brief Creates a parser for the given "tsig_key" member element id.
+ ///
+ /// The key elements currently supported are(see dhcp-ddns.spec):
+ /// 1. name
+ /// 2. algorithm
+ /// 3. secret
+ ///
+ /// @param config_id is the "item_name" for a specific member element of
+ /// the "tsig_key" specification.
+ ///
+ /// @return returns a pointer to newly created parser.
+ virtual isc::dhcp::ParserPtr createConfigParser(const std::string&
+ config_id);
+
+ /// @brief Instantiates a DnsServerInfo from internal data values
+ /// saves it to the storage area pointed to by servers_.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ /// Since servers are specified in a list this value is likely be something
+ /// akin to "key:0", set during parsing. Primarily here for diagnostics.
+ std::string entry_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the newly created TSIGKeyInfo instance. This is given to us as a
+ /// constructor argument by an upper level.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Local storage area for scalar parameter values. Use to hold
+ /// data until time to commit.
+ DScalarContext local_scalars_;
+};
+
+/// @brief Parser for a list of TSIGKeyInfos
+///
+/// This class parses a list of "tsig_key" configuration elements.
+/// (see src/bin/d2/dhcp-ddns.spec). The TSIGKeyInfo instances are added
+/// to the given storage upon commit.
+class TSIGKeyInfoListParser : public isc::dhcp::DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param list_name is an arbitrary label assigned to this parser instance.
+ /// @param keys is a pointer to the storage area to which the parser
+ /// should commit the newly created TSIGKeyInfo instance.
+ TSIGKeyInfoListParser(const std::string& list_name, TSIGKeyInfoMapPtr keys);
+
+ /// @brief Destructor
+ virtual ~TSIGKeyInfoListParser();
+
+ /// @brief Performs the parsing of the given list "tsig_key" elements.
+ ///
+ /// It iterates over each key entry in the list:
+ /// 1. Instantiate a TSIGKeyInfoParser for the entry
+ /// 2. Pass the element configuration to the parser's build method
+ /// 3. Add the parser instance to local storage
+ ///
+ /// The net effect is to parse all of the key entries in the list
+ /// prepping them for commit.
+ ///
+ /// @param key_list_config is the list of "tsig_key" elements to parse.
+ virtual void build(isc::data::ConstElementPtr key_list_config);
+
+ /// @brief Iterates over the internal list of TSIGKeyInfoParsers,
+ /// invoking commit on each. This causes each parser to instantiate a
+ /// TSIGKeyInfo from its internal data values and add that key
+ /// instance to the local key storage area, local_keys_. If all of the
+ /// key parsers commit cleanly, then update the context key map (keys_)
+ /// with the contents of local_keys_. This is done to allow for duplicate
+ /// key detection while parsing the keys, but not get stumped by it
+ /// updating the context with a valid list.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ std::string list_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the list of newly created TSIGKeyInfo instances. This is given to us
+ /// as a constructor argument by an upper level.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Local storage area to which individual key parsers commit.
+ TSIGKeyInfoMapPtr local_keys_;
+
+ /// @brief Local storage of TSIGKeyInfoParser instances
+ isc::dhcp::ParserCollection parsers_;
+};
+
+/// @brief Parser for DnsServerInfo
+///
+/// This class parses the configuration element "dns_server" defined in
+/// src/bin/d2/dhcp-ddns.spec and creates an instance of a DnsServerInfo.
+class DnsServerInfoParser : public isc::dhcp::DhcpConfigParser {
+public:
+ /// @brief Constructor
+ ///
+ /// @param entry_name is an arbitrary label assigned to this configuration
+ /// definition. Since servers are specified in a list this value is likely
+ /// be something akin to "server:0", set during parsing.
+ /// @param servers is a pointer to the storage area to which the parser
+ /// should commit the newly created DnsServerInfo instance.
+ DnsServerInfoParser(const std::string& entry_name,
+ DnsServerInfoStoragePtr servers);
+
+ /// @brief Destructor
+ virtual ~DnsServerInfoParser();
+
+ /// @brief Performs the actual parsing of the given "dns_server" element.
+ /// The results of the parsing are retained internally for use during
+ /// commit.
+ ///
+ /// @param server_config is the "dns_server" configuration to parse
+ virtual void build(isc::data::ConstElementPtr server_config);
+
+ /// @brief Creates a parser for the given "dns_server" member element id.
+ ///
+ /// The server elements currently supported are(see dhcp-ddns.spec):
+ /// 1. hostname
+ /// 2. ip_address
+ /// 3. port
+ ///
+ /// @param config_id is the "item_name" for a specific member element of
+ /// the "dns_server" specification.
+ ///
+ /// @return returns a pointer to newly created parser.
+ virtual isc::dhcp::ParserPtr createConfigParser(const std::string&
+ config_id);
+
+ /// @brief Instantiates a DnsServerInfo from internal data values
+ /// saves it to the storage area pointed to by servers_.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ /// Since servers are specified in a list this value is likely be something
+ /// akin to "server:0", set during parsing. Primarily here for diagnostics.
+ std::string entry_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the newly created DnsServerInfo instance. This is given to us as a
+ /// constructor argument by an upper level.
+ DnsServerInfoStoragePtr servers_;
+
+ /// @brief Local storage area for scalar parameter values. Use to hold
+ /// data until time to commit.
+ DScalarContext local_scalars_;
+};
+
+/// @brief Parser for a list of DnsServerInfos
+///
+/// This class parses a list of "dns_server" configuration elements.
+/// (see src/bin/d2/dhcp-ddns.spec). The DnsServerInfo instances are added
+/// to the given storage upon commit.
+class DnsServerInfoListParser : public isc::dhcp::DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param list_name is an arbitrary label assigned to this parser instance.
+ /// @param servers is a pointer to the storage area to which the parser
+ /// should commit the newly created DnsServerInfo instance.
+ DnsServerInfoListParser(const std::string& list_name,
+ DnsServerInfoStoragePtr servers);
+
+ /// @brief Destructor
+ virtual ~DnsServerInfoListParser();
+
+ /// @brief Performs the actual parsing of the given list "dns_server"
+ /// elements.
+ /// It iterates over each server entry in the list:
+ /// 1. Instantiate a DnsServerInfoParser for the entry
+ /// 2. Pass the element configuration to the parser's build method
+ /// 3. Add the parser instance to local storage
+ ///
+ /// The net effect is to parse all of the server entries in the list
+ /// prepping them for commit.
+ ///
+ /// @param server_list_config is the list of "dns_server" elements to parse.
+ virtual void build(isc::data::ConstElementPtr server_list_config);
+
+ /// @brief Iterates over the internal list of DnsServerInfoParsers,
+ /// invoking commit on each. This causes each parser to instantiate a
+ /// DnsServerInfo from its internal data values and add that that server
+ /// instance to the storage area, servers_.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ std::string list_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the list of newly created DnsServerInfo instances. This is given to us
+ /// as a constructor argument by an upper level.
+ DnsServerInfoStoragePtr servers_;
+
+ /// @brief Local storage of DnsServerInfoParser instances
+ isc::dhcp::ParserCollection parsers_;
+};
+
+/// @brief Parser for DdnsDomain
+///
+/// This class parses the configuration element "ddns_domain" defined in
+/// src/bin/d2/dhcp-ddns.spec and creates an instance of a DdnsDomain.
+class DdnsDomainParser : public isc::dhcp::DhcpConfigParser {
+public:
+ /// @brief Constructor
+ ///
+ /// @param entry_name is an arbitrary label assigned to this configuration
+ /// definition. Since domains are specified in a list this value is likely
+ /// be something akin to "forward_ddns:0", set during parsing.
+ /// @param domains is a pointer to the storage area to which the parser
+ /// @param keys is a pointer to a map of the defined TSIG keys.
+ /// should commit the newly created DdnsDomain instance.
+ DdnsDomainParser(const std::string& entry_name, DdnsDomainMapPtr domains,
+ TSIGKeyInfoMapPtr keys);
+
+ /// @brief Destructor
+ virtual ~DdnsDomainParser();
+
+ /// @brief Performs the actual parsing of the given "ddns_domain" element.
+ /// The results of the parsing are retained internally for use during
+ /// commit.
+ ///
+ /// @param domain_config is the "ddns_domain" configuration to parse
+ virtual void build(isc::data::ConstElementPtr domain_config);
+
+ /// @brief Creates a parser for the given "ddns_domain" member element id.
+ ///
+ /// The domain elements currently supported are(see dhcp-ddns.spec):
+ /// 1. name
+ /// 2. key_name
+ /// 3. dns_servers
+ ///
+ /// @param config_id is the "item_name" for a specific member element of
+ /// the "ddns_domain" specification.
+ ///
+ /// @return returns a pointer to newly created parser.
+ virtual isc::dhcp::ParserPtr createConfigParser(const std::string&
+ config_id);
+
+ /// @brief Instantiates a DdnsDomain from internal data values
+ /// saves it to the storage area pointed to by domains_.
+ virtual void commit();
+
+private:
+
+ /// @brief Arbitrary label assigned to this parser instance.
+ std::string entry_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the newly created DdnsDomain instance. This is given to us as a
+ /// constructor argument by an upper level.
+ DdnsDomainMapPtr domains_;
+
+ /// @brief Pointer to the map of defined TSIG keys.
+ /// This map is passed into us and contains all of the TSIG keys defined
+ /// for this configuration. It is used to validate the key name entry of
+ /// DdnsDomains that specify one.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Local storage for DnsServerInfo instances. This is passed into
+ /// DnsServerInfoListParser(s), which in turn passes it into each
+ /// DnsServerInfoParser. When the DnsServerInfoParsers "commit" they add
+ /// their server instance to this storage.
+ DnsServerInfoStoragePtr local_servers_;
+
+ /// @brief Local storage area for scalar parameter values. Use to hold
+ /// data until time to commit.
+ DScalarContext local_scalars_;
+};
+
+/// @brief Parser for a list of DdnsDomains
+///
+/// This class parses a list of "ddns_domain" configuration elements.
+/// (see src/bin/d2/dhcp-ddns.spec). The DdnsDomain instances are added
+/// to the given storage upon commit.
+class DdnsDomainListParser : public isc::dhcp::DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param list_name is an arbitrary label assigned to this parser instance.
+ /// @param domains is a pointer to the storage area to which the parser
+ /// @param keys is a pointer to a map of the defined TSIG keys.
+ /// should commit the newly created DdnsDomain instance.
+ DdnsDomainListParser(const std::string& list_name,
+ DdnsDomainMapPtr domains, TSIGKeyInfoMapPtr keys);
+
+ /// @brief Destructor
+ virtual ~DdnsDomainListParser();
+
+ /// @brief Performs the actual parsing of the given list "ddns_domain"
+ /// elements.
+ /// It iterates over each server entry in the list:
+ /// 1. Instantiate a DdnsDomainParser for the entry
+ /// 2. Pass the element configuration to the parser's build method
+ /// 3. Add the parser instance to local storage
+ ///
+ /// The net effect is to parse all of the domain entries in the list
+ /// prepping them for commit.
+ ///
+ /// @param domain_list_config is the list of "ddns_domain" elements to
+ /// parse.
+ virtual void build(isc::data::ConstElementPtr domain_list_config);
+
+ /// @brief Iterates over the internal list of DdnsDomainParsers, invoking
+ /// commit on each. This causes each parser to instantiate a DdnsDomain
+ /// from its internal data values and add that domain instance to the
+ /// storage area, domains_.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ std::string list_name_;
+
+ /// @brief Pointer to the storage area to which the parser should commit
+ /// the list of newly created DdnsDomain instances. This is given to us
+ /// as a constructor argument by an upper level.
+ DdnsDomainMapPtr domains_;
+
+ /// @brief Pointer to the map of defined TSIG keys.
+ /// This map is passed into us and contains all of the TSIG keys defined
+ /// for this configuration. It is used to validate the key name entry of
+ /// DdnsDomains that specify one.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Local storage of DdnsDomainParser instances
+ isc::dhcp::ParserCollection parsers_;
+};
+
+/// @brief Parser for DdnsDomainListMgr
+///
+/// This class parses the configuration elements "forward_ddns" and
+/// "reverse_ddns" as defined in src/bin/d2/dhcp-ddns.spec. It populates the
+/// given DdnsDomainListMgr with parsed information upon commit. Note that
+/// unlike other parsers, this parser does NOT instantiate the final object
+/// during the commit phase, it populates it. It must pre-exist.
+class DdnsDomainListMgrParser : public isc::dhcp::DhcpConfigParser {
+public:
+ /// @brief Constructor
+ ///
+ /// @param entry_name is an arbitrary label assigned to this configuration
+ /// definition.
+ /// @param mgr is a pointer to the DdnsDomainListMgr to populate.
+ /// @param keys is a pointer to a map of the defined TSIG keys.
+ /// @throw throws D2CfgError if mgr pointer is empty.
+ DdnsDomainListMgrParser(const std::string& entry_name,
+ DdnsDomainListMgrPtr mgr, TSIGKeyInfoMapPtr keys);
+
+ /// @brief Destructor
+ virtual ~DdnsDomainListMgrParser();
+
+ /// @brief Performs the actual parsing of the given manager element.
+ /// The results of the parsing are retained internally for use during
+ /// commit.
+ ///
+ /// @param mgr_config is the manager configuration to parse
+ virtual void build(isc::data::ConstElementPtr mgr_config);
+
+ /// @brief Creates a parser for the given manager member element id.
+ ///
+ /// The manager elements currently supported are (see dhcp-ddns.spec):
+ /// 1. ddns_domains
+ ///
+ /// @param config_id is the "item_name" for a specific member element of
+ /// the manager specification.
+ ///
+ /// @return returns a pointer to newly created parser.
+ virtual isc::dhcp::ParserPtr createConfigParser(const std::string&
+ config_id);
+
+ /// @brief Populates the DdnsDomainListMgr from internal data values
+ /// set during parsing.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ std::string entry_name_;
+
+ /// @brief Pointer to manager instance to which the parser should commit
+ /// the parsed data. This is given to us as a constructor argument by an
+ /// upper level.
+ DdnsDomainListMgrPtr mgr_;
+
+ /// @brief Pointer to the map of defined TSIG keys.
+ /// This map is passed into us and contains all of the TSIG keys defined
+ /// for this configuration. It is used to validate the key name entry of
+ /// DdnsDomains that specify one.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Local storage for DdnsDomain instances. This is passed into a
+ /// DdnsDomainListParser(s), which in turn passes it into each
+ /// DdnsDomainParser. When the DdnsDomainParsers "commit" they add their
+ /// domain instance to this storage.
+ DdnsDomainMapPtr local_domains_;
+
+ /// @brief Local storage area for scalar parameter values. Use to hold
+ /// data until time to commit.
+ /// @todo Currently, the manager has no scalars but this is likely to
+ /// change as matching capabilities expand.
+ DScalarContext local_scalars_;
+};
+
+
+}; // end of isc::d2 namespace
+}; // end of isc namespace
+
+#endif // D2_CONFIG_H
diff --git a/src/bin/d2/d2_controller.cc b/src/bin/d2/d2_controller.cc
new file mode 100644
index 0000000..2206207
--- /dev/null
+++ b/src/bin/d2/d2_controller.cc
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_controller.h>
+#include <d2/d2_process.h>
+#include <d2/spec_config.h>
+
+#include <stdlib.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Defines the application name, this is passed into base class
+/// and appears in log statements.
+const char* D2Controller::d2_app_name_ = "DHCP-DDNS";
+
+/// @brief Defines the executable name. This is passed into the base class
+/// by convention this should match the BIND10 module name.
+const char* D2Controller::d2_bin_name_ = "b10-dhcp-ddns";
+
+DControllerBasePtr&
+D2Controller::instance() {
+ // If the instance hasn't been created yet, create it. Note this method
+ // must use the base class singleton instance methods. The base class
+ // must have access to the singleton in order to use it within BIND10
+ // static function callbacks.
+ if (!getController()) {
+ DControllerBasePtr controller_ptr(new D2Controller());
+ setController(controller_ptr);
+ }
+
+ return (getController());
+}
+
+DProcessBase* D2Controller::createProcess() {
+ // Instantiate and return an instance of the D2 application process. Note
+ // that the process is passed the controller's io_service.
+ return (new D2Process(getAppName().c_str(), getIOService()));
+}
+
+D2Controller::D2Controller()
+ : DControllerBase(d2_app_name_, d2_bin_name_) {
+ // set the BIND10 spec file either from the environment or
+ // use the production value.
+ if (getenv("B10_FROM_BUILD")) {
+ setSpecFileName(std::string(getenv("B10_FROM_BUILD")) +
+ "/src/bin/d2/dhcp-ddns.spec");
+ } else {
+ setSpecFileName(D2_SPECFILE_LOCATION);
+ }
+}
+
+D2Controller::~D2Controller() {
+}
+
+}; // end namespace isc::d2
+}; // end namespace isc
diff --git a/src/bin/d2/d2_controller.h b/src/bin/d2/d2_controller.h
new file mode 100644
index 0000000..0290f87
--- /dev/null
+++ b/src/bin/d2/d2_controller.h
@@ -0,0 +1,72 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_CONTROLLER_H
+#define D2_CONTROLLER_H
+
+#include <d2/d_controller.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Process Controller for D2 Process
+/// This class is the DHCP-DDNS specific derivation of DControllerBase. It
+/// creates and manages an instance of the DHCP-DDNS application process,
+/// D2Process.
+/// @todo Currently, this class provides only the minimum required specialized
+/// behavior to run the DHCP-DDNS service. It may very well expand as the
+/// service implementation evolves. Some thought was given to making
+/// DControllerBase a templated class but the labor savings versus the
+/// potential number of virtual methods which may be overridden didn't seem
+/// worth the clutter at this point.
+class D2Controller : public DControllerBase {
+public:
+ /// @brief Static singleton instance method. This method returns the
+ /// base class singleton instance member. It instantiates the singleton
+ /// and sets the base class instance member upon first invocation.
+ ///
+ /// @return returns the pointer reference to the singleton instance.
+ static DControllerBasePtr& instance();
+
+ /// @brief Destructor.
+ virtual ~D2Controller();
+
+ /// @brief Defines the application name, this is passed into base class
+ /// and appears in log statements.
+ static const char* d2_app_name_;
+
+ /// @brief Defines the executable name. This is passed into the base class
+ /// by convention this should match the BIND10 module name.
+ static const char* d2_bin_name_;
+
+private:
+ /// @brief Creates an instance of the DHCP-DDNS specific application
+ /// process. This method is invoked during the process initialization
+ /// step of the controller launch.
+ ///
+ /// @return returns a DProcessBase* to the application process created.
+ /// Note the caller is responsible for destructing the process. This
+ /// is handled by the base class, which wraps this pointer with a smart
+ /// pointer.
+ virtual DProcessBase* createProcess();
+
+ /// @brief Constructor is declared private to maintain the integrity of
+ /// the singleton instance.
+ D2Controller();
+};
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/d2_log.cc b/src/bin/d2/d2_log.cc
new file mode 100644
index 0000000..c938c2c
--- /dev/null
+++ b/src/bin/d2/d2_log.cc
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// Defines the logger used by the top-level component of b10-d2.
+
+#include <d2/d2_log.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Defines the logger used within D2.
+isc::log::Logger dctl_logger("dhcpddns");
+
+} // namespace d2
+} // namespace isc
+
diff --git a/src/bin/d2/d2_log.h b/src/bin/d2/d2_log.h
new file mode 100644
index 0000000..b91fc15
--- /dev/null
+++ b/src/bin/d2/d2_log.h
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_LOG_H
+#define D2_LOG_H
+
+#include <log/logger_support.h>
+#include <log/macros.h>
+#include <d2/d2_messages.h>
+
+namespace isc {
+namespace d2 {
+
+/// Define the logger for the "d2" logging.
+extern isc::log::Logger dctl_logger;
+
+
+} // namespace d2
+} // namespace isc
+
+#endif // D2_LOG_H
diff --git a/src/bin/d2/d2_messages.mes b/src/bin/d2/d2_messages.mes
new file mode 100644
index 0000000..dc5103d
--- /dev/null
+++ b/src/bin/d2/d2_messages.mes
@@ -0,0 +1,449 @@
+# Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+$NAMESPACE isc::d2
+
+% DCTL_CCSESSION_ENDING %1 ending control channel session
+This debug message is issued just before the controller attempts
+to disconnect from its session with the BIND10 control channel.
+
+% DCTL_CCSESSION_STARTING %1 starting control channel session, specfile: %2
+This debug message is issued just before the controller attempts
+to establish a session with the BIND10 control channel.
+
+% DCTL_COMMAND_RECEIVED %1 received command: %2, arguments: %3
+A debug message listing the command (and possible arguments) received
+from the BIND10 control system by the controller.
+
+% DCTL_CONFIG_COMPLETE server has completed configuration: %1
+This is an informational message announcing the successful processing of a
+new configuration. It is output during server startup, and when an updated
+configuration is committed by the administrator. Additional information
+may be provided.
+
+% DCTL_CONFIG_LOAD_FAIL %1 configuration failed to load: %2
+This critical error message indicates that the initial application
+configuration has failed. The service will start, but will not
+process requests until the configuration has been corrected.
+
+% DCTL_CONFIG_START parsing new configuration: %1
+A debug message indicating that the application process has received an
+updated configuration and has passed it to its configuration manager
+for parsing.
+
+% DCTL_CONFIG_STUB %1 configuration stub handler called
+This debug message is issued when the dummy handler for configuration
+events is called. This only happens during initial startup.
+
+% DCTL_CONFIG_UPDATE %1 updated configuration received: %2
+A debug message indicating that the controller has received an
+updated configuration from the BIND10 configuration system.
+
+% DCTL_DISCONNECT_FAIL %1 controller failed to end session with BIND10: %2
+This message indicates that while shutting down, the Dhcp-Ddns controller
+encountered an error terminating communication with the BIND10. The service
+will still exit. While theoretically possible, this situation is rather
+unlikely.
+
+% DCTL_INIT_PROCESS %1 initializing the application
+This debug message is issued just before the controller attempts
+to create and initialize its application instance.
+
+% DCTL_INIT_PROCESS_FAIL %1 application initialization failed: %2
+This error message is issued if the controller could not initialize the
+application and will exit.
+
+% DCTL_NOT_RUNNING %1 application instance is not running
+A warning message is issued when an attempt is made to shut down the
+application when it is not running.
+
+% DCTL_ORDER_ERROR configuration contains more elements than the parsing order
+An error message which indicates that configuration being parsed includes
+element ids not specified the configuration manager's parse order list. This
+is a programmatic error.
+
+% DCTL_ORDER_NO_ELEMENT element: %1 is in the parsing order but is missing from the configuration
+An error message output during a configuration update. The program is
+expecting an item but has not found it in the new configuration. This may
+mean that the BIND 10 configuration database is corrupt.
+
+% DCTL_PARSER_FAIL configuration parsing failed for configuration element: %1, reason: %2
+On receipt of message containing details to a change of its configuration,
+the server failed to create a parser to decode the contents of the named
+configuration element, or the creation succeeded but the parsing actions
+and committal of changes failed. The reason for the failure is given in
+the message.
+
+% DCTL_PROCESS_FAILED %1 application execution failed: %2
+The controller has encountered a fatal error while running the
+application and is terminating. The reason for the failure is
+included in the message.
+
+% DCTL_RUN_PROCESS %1 starting application event loop
+This debug message is issued just before the controller invokes
+the application run method.
+
+% DCTL_SESSION_FAIL %1 controller failed to establish BIND10 session: %1
+The controller has failed to establish communication with the rest of BIND
+10 and will exit.
+
+% DCTL_STANDALONE %1 skipping message queue, running standalone
+This is a debug message indicating that the controller is running in the
+application in standalone mode. This means it will not connected to the BIND10
+message queue. Standalone mode is only useful during program development,
+and should not be used in a production environment.
+
+% DCTL_STARTING %1 controller starting, pid: %2
+This is an informational message issued when controller for the
+service first starts.
+
+% DCTL_STOPPING %1 controller is exiting
+This is an informational message issued when the controller is exiting
+following a shut down (normal or otherwise) of the service.
+
+% DHCP_DDNS_AT_MAX_TRANSACTIONS application has %1 queued requests but has reached maximum number of %2 concurrent transactions
+This is a debug message that indicates that the application has DHCP_DDNS
+requests in the queue but is working as many concurrent requests as allowed.
+
+% DHCP_DDNS_CLEARED_FOR_SHUTDOWN application has met shutdown criteria for shutdown type: %1
+This is an informational message issued when the application has been instructed
+to shutdown and has met the required criteria to exit.
+
+% DHCP_DDNS_COMMAND command directive received, command: %1 - args: %2
+This is a debug message issued when the Dhcp-Ddns application command method
+has been invoked.
+
+% DHCP_DDNS_CONFIGURE configuration update received: %1
+This is a debug message issued when the Dhcp-Ddns application configure method
+has been invoked.
+
+% DHCP_DDNS_FAILED application experienced a fatal error: %1
+This is a debug message issued when the Dhcp-Ddns application encounters an
+unrecoverable error from within the event loop.
+
+% DHCP_DDNS_INVALID_RESPONSE received response to DNS Update message is malformed: %1
+This is a debug message issued when the DHCP-DDNS application encountered an
+error while decoding a response to DNS Update message. Typically, this error
+will be encountered when a response message is malformed.
+
+% DHCP_DDNS_NO_ELIGIBLE_JOBS although there are queued requests, there are pending transactions for each Queue count: %1 Transaction count: %2
+This is a debug message issued when all of the queued requests represent clients
+for which there is a an update already in progress. This may occur under
+normal operations but should be temporary situation.
+
+% DHCP_DDNS_NO_FWD_MATCH_ERROR the configured list of forward DDNS domains does not contain a match for FQDN %1 The request has been discarded.
+This is an error message that indicates that DHCP_DDNS received a request to
+update a the forward DNS information for the given FQDN but for which there are
+no configured DDNS domains in the DHCP_DDNS configuration. Either the DHCP_DDNS
+configuration needs to be updated or the source of the FQDN itself should be
+investigated.
+
+% DHCP_DDNS_NO_MATCH No DNS servers match FQDN %1
+This is warning message issued when there are no domains in the configuration
+which match the cited fully qualified domain name (FQDN). The DNS Update
+request for the FQDN cannot be processed.
+
+% DHCP_DDNS_NO_REV_MATCH_ERROR the configured list of reverse DDNS domains does not contain a match for FQDN %1 The request has been discarded.
+This is an error message that indicates that DHCP_DDNS received a request to
+update a the reverse DNS information for the given FQDN but for which there are
+no configured DDNS domains in the DHCP_DDNS configuration. Either the DHCP_DDNS
+configuration needs to be updated or the source of the FQDN itself should be
+investigated.
+
+% DHCP_DDNS_PROCESS_INIT application init invoked
+This is a debug message issued when the Dhcp-Ddns application enters
+its init method.
+
+% DHCP_DDNS_QUEUE_MGR_QUEUE_FULL application request queue has reached maximum number of entries %1
+This an error message indicating that DHCP-DDNS is receiving DNS update
+requests faster than they can be processed. This may mean the maximum queue
+needs to be increased, the DHCP-DDNS clients are simply generating too many
+requests too quickly, or perhaps upstream DNS servers are experiencing
+load issues.
+
+% DHCP_DDNS_QUEUE_MGR_RECONFIGURING application is reconfiguring the queue manager
+This is an informational message indicating that DHCP_DDNS is reconfiguring the
+queue manager as part of normal startup or in response to a new configuration.
+
+% DHCP_DDNS_QUEUE_MGR_RECOVERING application is attempting to recover from a
+queue manager IO error
+This is an informational message indicating that DHCP_DDNS is attempting to
+restart the queue manager after it suffered an IO error while receiving
+requests.
+
+% DHCP_DDNS_QUEUE_MGR_RECV_ERROR application's queue manager was notified of a request receive error by its listener.
+This is an error message indicating that the NameChangeRequest listener used by
+DHCP-DDNS to receive requests encountered a IO error. There should be
+corresponding log messages from the listener layer with more details. This may
+indicate a network connectivity or system resource issue.
+
+% DHCP_DDNS_QUEUE_MGR_RESUME_ERROR application could not restart the queue manager, reason: %1
+This is an error message indicating that DHCP_DDNS's Queue Manager could not
+be restarted after stopping due to a full receive queue. This means that
+the application cannot receive requests. This is most likely due to DHCP_DDNS
+configuration parameters referring to resources such as an IP address or port,
+that is no longer unavailable. DHCP_DDNS will attempt to restart the queue
+manager if given a new configuration.
+
+% DHCP_DDNS_QUEUE_MGR_RESUMING application is resuming listening for requests now that the request queue size has reached %1 of a maximum %2 allowed
+This is an informational message indicating that DHCP_DDNS, which had stopped
+accepting new requests, has processed enough entries from the receive queue to
+resume accepting requests.
+
+% DHCP_DDNS_QUEUE_MGR_STARTED application's queue manager has begun listening for requests.
+This is a debug message indicating that DHCP_DDNS's Queue Manager has
+successfully started and is now listening for NameChangeRequests.
+
+% DHCP_DDNS_QUEUE_MGR_START_ERROR application could not start the queue manager, reason: %1
+This is an error message indicating that DHCP_DDNS's Queue Manager could not
+be started. This means that the application cannot receive requests. This is
+most likely due to DHCP_DDNS configuration parameters referring to resources
+such as an IP address or port, that are unavailable. DHCP_DDNS will attempt to
+restart the queue manager if given a new configuration.
+
+% DHCP_DDNS_QUEUE_MGR_STOPPED application's queue manager has stopped listening for requests.
+This is an informational message indicating that DHCP_DDNS's Queue Manager has
+stopped listening for NameChangeRequests. This may be because of normal event
+such as reconfiguration or as a result of an error. There should be log
+messages preceding this one to indicate why it has stopped.
+
+% DHCP_DDNS_QUEUE_MGR_STOPPING application is stopping the queue manager for %1
+This is an informational message indicating that DHCP_DDNS is stopping the
+queue manager either to reconfigure it or as part of application shutdown.
+
+% DHCP_DDNS_QUEUE_MGR_STOP_ERROR application encountered an error stopping the queue manager: %1
+This is an error message indicating that DHCP_DDNS encountered an error while
+trying to stop the queue manager. This error is unlikely to occur or to
+impair the application's ability to function but it should be reported for
+analysis.
+
+% DHCP_DDNS_QUEUE_MGR_UNEXPECTED_HANDLER_ERROR application's queue manager request receive handler experienced an unexpected exception %1:
+This is an error message indicating that an unexpected error occurred within the
+DHCP_DDNS's Queue Manager request receive completion handler. This is most
+likely a programmatic issue that should be reported. The application may
+recover on its own.
+
+% DHCP_DDNS_QUEUE_MGR_UNEXPECTED_STOP application's queue manager receive was
+aborted unexpectedly while queue manager state is: %1
+This is an error message indicating that DHCP_DDNS's Queue Manager request
+receive was unexpected interrupted. Normally, the read is receive is only
+interrupted as a normal part of stopping the queue manager. This is most
+likely a programmatic issue that should be reported.
+
+% DHCP_DDNS_RUN_ENTER application has entered the event loop
+This is a debug message issued when the Dhcp-Ddns application enters
+its run method.
+
+% DHCP_DDNS_RUN_EXIT application is exiting the event loop
+This is a debug message issued when the Dhcp-Ddns exits the
+in event loop.
+
+% DHCP_DDNS_SHUTDOWN application received shutdown command with args: %1
+This is informational message issued when the application has been instructed
+to shut down by the controller.
+
+% DHCP_DDNS_STATE_MODEL_UNEXPECTED_ERROR application encountered an unexpected error while carrying out a NameChangeRequest: %1
+This is error message issued when the application fails to process a
+NameChangeRequest correctly. Some or all of the DNS updates requested as part
+of this update did not succeed. This is a programmatic error and should be
+reported.
+
+% DHCP_DDNS_FORWARD_ADD_REJECTED DNS Server, %1, rejected a DNS update request to add the address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_ADD_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping add for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address update. The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_ADD_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while adding forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to add a forward address mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_ADD_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while adding a forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was adding a forward address mapping. The request will be
+aborted. This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping replace for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address update. The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to replace a forward address mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REPLACE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while replacing forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was replacing a forward address mapping. The request will be
+aborted. This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_REVERSE_REPLACE_REJECTED DNS Server, %1, rejected a DNS update request to replace the reverse mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_REVERSE_REPLACE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping replacement for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a reverse address update. The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_REVERSE_REPLACE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while replacing reverse address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to replace a reverse address, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_REVERSE_REPLACE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while replacing reverse address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was replacing a reverse address mapping. The request will be
+aborted. This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_TRANS_SEND_ERROR application encountered an unexpected error while attempting to send a DNS update: %1
+This is error message issued when the application is able to construct an update
+message but the attempt to send it suffered a unexpected error. This is most
+likely a programmatic error, rather than a communications issue. Some or all
+of the DNS updates requested as part of this request did not succeed.
+
+% DHCP_DDNS_FORWARD_ADD_BUILD_FAILURE DNS udpate message to add a forward DNS entry could not be constructed for this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address addition. This is due
+to invalid data contained in the NameChangeRequest. The request will be aborted.
+This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REPLACE_BUILD_FAILURE DNS update message to replace a forward DNS entry could not be constructed from this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address replacement. This is
+due to invalid data contained in the NameChangeRequest. The request will be
+aborted. This is most likely a configuration issue.
+
+% DHCP_DDNS_REVERSE_REPLACE_BUILD_FAILURE DNS update message to replace a reverse DNS entry could not be constructed from this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a reverse PTR replacement. This is
+due to invalid data contained in the NameChangeRequest. The request will be
+aborted. This is most likely a configuration issue.
+
+% DHCP_DDNS_ADD_SUCCEEDED DHCP_DDNS successfully added the DNS mapping addition for this request: %1
+This is a debug message issued after DHCP_DDNS has submitted DNS mapping
+additions which were received and accepted by an appropriate DNS server.
+
+% DHCP_DDNS_ADD_FAILED DHCP_DDNS failed attempting to make DNS mapping additions for this request: %1, event: %2
+This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
+entry additions have failed. The precise reason for the failure should be
+documented in preceding log entries.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BUILD_FAILURE DNS udpate message to remove a forward DNS Address entry could not be constructed for this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a forward address (A or AAAA) removal. This
+is due to invalid data contained in the NameChangeRequest. The request will be
+aborted. This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_REJECTED DNS Server, %1, rejected a DNS update request to remove the forward address mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward mapping address removal for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward address remove. The application will retry
+against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to remove a forward address mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_ADDRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing a forward address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was removing a forward address mapping. The request will be
+aborted. This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_BUILD_FAILURE DNS udpate message to remove forward DNS RR entries could not be constructed for this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting forward RR (DHCID RR) removal. This is due
+to invalid data contained in the NameChangeRequest. The request will be aborted.This is most likely a configuration issue.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_REJECTED DNS Server, %1, rejected a DNS update request to remove forward RR entries for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_IO_ERROR DHCP_DDNS encountered an IO error sending a forward RR removal for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a forward RR remove. The application will retry
+against the same server.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing forward RRs for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to remove forward RRs mapping, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_FORWARD_REMOVE_RRS_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing forward RRs for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was removing forward RRs. The request will be aborted. This is
+most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_REVERSE_REMOVE_BUILD_FAILURE DNS update message to remove a reverse DNS entry could not be constructed from this request: %1, reason: %2
+This is an error message issued when an error occurs attempting to construct
+the server bound packet requesting a reverse PTR removal. This is
+due to invalid data contained in the NameChangeRequest. The request will be
+aborted. This is most likely a configuration issue.
+
+% DHCP_DDNS_REVERSE_REMOVE_REJECTED DNS Server, %1, rejected a DNS update request to remove the reverse mapping for FQDN, %2, with an RCODE: %3
+This is an error message issued when an update was rejected by the DNS server
+it was sent to for the reason given by the RCODE. The rcode values are defined
+in RFC 2136.
+
+% DHCP_DDNS_REVERSE_REMOVE_IO_ERROR DHCP_DDNS encountered an IO error sending a reverse mapping remove for FQDN %1 to DNS server %2
+This is an error message issued when a communication error occurs while
+DHCP_DDNS is carrying out a reverse address update. The application will
+retry against the same server or others as appropriate.
+
+% DHCP_DDNS_REVERSE_REMOVE_RESP_CORRUPT DHCP_DDNS received a corrupt response from the DNS server, %1, while removing reverse address mapping for FQDN, %2
+This is an error message issued when the response received by DHCP_DDNS, to a
+update request to remove a reverse address, is mangled or malformed.
+The application will retry against the same server or others as appropriate.
+
+% DHCP_DDNS_REVERSE_REMOVE_BAD_DNSCLIENT_STATUS DHCP_DDNS received an unknown DNSClient status: %1, while removing reverse address mapping for FQDN %2 to DNS server %3
+This is an error message issued when DNSClient returns an unrecognized status
+while DHCP_DDNS was removing a reverse address mapping. The request will be
+aborted. This is most likely a programmatic issue and should be reported.
+
+% DHCP_DDNS_REMOVE_SUCCEEDED DHCP_DDNS successfully removed the DNS mapping addition for this request: %1
+This is a debug message issued after DHCP_DDNS has submitted DNS mapping
+removals which were received and accepted by an appropriate DNS server.
+
+% DHCP_DDNS_REMOVE_FAILED DHCP_DDNS failed attempting to make DNS mapping removals for this request: %1, event: %2
+This is an error message issued after DHCP_DDNS attempts to submit DNS mapping
+entry removals have failed. The precise reason for the failure should be
+documented in preceding log entries.
+
+% DHCP_DDNS_STARTING_TRANSACTION Transaction Key: %1
+
+% DHCP_DDNS_UPDATE_REQUEST_SENT for transaction key: %1 to server: %2
+This is a debug message issued when DHCP_DDNS sends a DNS request to a DNS
+server.
+
+% DHCP_DDNS_UPDATE_RESPONSE_RECEIVED for transaction key: %1 to server: %2 status: %3
+This is a debug message issued when DHCP_DDNS receives sends a DNS update
+response from a DNS server.
diff --git a/src/bin/d2/d2_process.cc b/src/bin/d2/d2_process.cc
new file mode 100644
index 0000000..187bfe1
--- /dev/null
+++ b/src/bin/d2/d2_process.cc
@@ -0,0 +1,394 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/ccsession.h>
+#include <d2/d2_log.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/d2_process.h>
+
+#include <asio.hpp>
+
+namespace isc {
+namespace d2 {
+
+// Setting to 80% for now. This is an arbitrary choice and should probably
+// be configurable.
+const unsigned int D2Process::QUEUE_RESTART_PERCENT = 80;
+
+D2Process::D2Process(const char* name, IOServicePtr io_service)
+ : DProcessBase(name, io_service, DCfgMgrBasePtr(new D2CfgMgr())),
+ reconf_queue_flag_(false), shutdown_type_(SD_NORMAL) {
+
+ // Instantiate queue manager. Note that queue manager does not start
+ // listening at this point. That can only occur after configuration has
+ // been received. This means that until we receive the configuration,
+ // D2 will neither receive nor process NameChangeRequests.
+ // Pass in IOService for NCR IO event processing.
+ queue_mgr_.reset(new D2QueueMgr(getIoService()));
+
+ // Instantiate update manager.
+ // Pass in both queue manager and configuration manager.
+ // Pass in IOService for DNS update transaction IO event processing.
+ D2CfgMgrPtr tmp = getD2CfgMgr();
+ update_mgr_.reset(new D2UpdateMgr(queue_mgr_, tmp, getIoService()));
+};
+
+void
+D2Process::init() {
+};
+
+void
+D2Process::run() {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DHCP_DDNS_RUN_ENTER);
+ // Loop forever until we are allowed to shutdown.
+ while (!canShutdown()) {
+ try {
+ // Check on the state of the request queue. Take any
+ // actions necessary regarding it.
+ checkQueueStatus();
+
+ // Give update manager a time slice to queue new jobs and
+ // process finished ones.
+ update_mgr_->sweep();
+
+ // Wait on IO event(s) - block until one or more of the following
+ // has occurred:
+ // a. NCR message has been received
+ // b. Transaction IO has completed
+ // c. Interval timer expired
+ // d. Something stopped IO service (runIO returns 0)
+ if (runIO() == 0) {
+ // Pretty sure this amounts to an unexpected stop and we
+ // should bail out now. Normal shutdowns do not utilize
+ // stopping the IOService.
+ isc_throw(DProcessBaseError,
+ "Primary IO service stopped unexpectedly");
+ }
+ } catch (const std::exception& ex) {
+ LOG_FATAL(dctl_logger, DHCP_DDNS_FAILED).arg(ex.what());
+ isc_throw (DProcessBaseError,
+ "Process run method failed: " << ex.what());
+ }
+ }
+
+ // @todo - if queue isn't empty, we may need to persist its contents
+ // this might be the place to do it, once there is a persistence mgr.
+ // This may also be better in checkQueueStatus.
+
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DHCP_DDNS_RUN_EXIT);
+
+};
+
+size_t
+D2Process::runIO() {
+ // We want to block until at least one handler is called. We'll use
+ // asio::io_service directly for two reasons. First off
+ // asiolink::IOService::run_one is a void and asio::io_service::stopped
+ // is not present in older versions of boost. We need to know if any
+ // handlers ran or if the io_service was stopped. That latter represents
+ // some form of error and the application cannot proceed with a stopped
+ // service. Secondly, asiolink::IOService does not provide the poll
+ // method. This is a handy method which runs all ready handlers without
+ // blocking.
+ IOServicePtr& io = getIoService();
+ asio::io_service& asio_io_service = io->get_io_service();
+
+ // Poll runs all that are ready. If none are ready it returns immediately
+ // with a count of zero.
+ size_t cnt = asio_io_service.poll();
+ if (!cnt) {
+ // Poll ran no handlers either none are ready or the service has been
+ // stopped. Either way, call run_one to wait for a IO event. If the
+ // service is stopped it will return immediately with a cnt of zero.
+ cnt = asio_io_service.run_one();
+ }
+
+ return (cnt);
+}
+
+bool
+D2Process::canShutdown() const {
+ bool all_clear = false;
+
+ // If we have been told to shutdown, find out if we are ready to do so.
+ if (shouldShutdown()) {
+ switch (shutdown_type_) {
+ case SD_NORMAL:
+ // For a normal shutdown we need to stop the queue manager but
+ // wait until we have finished all the transactions in progress.
+ all_clear = (((queue_mgr_->getMgrState() != D2QueueMgr::RUNNING) &&
+ (queue_mgr_->getMgrState() != D2QueueMgr::STOPPING))
+ && (update_mgr_->getTransactionCount() == 0));
+ break;
+
+ case SD_DRAIN_FIRST:
+ // For a drain first shutdown we need to stop the queue manager but
+ // process all of the requests in the receive queue first.
+ all_clear = (((queue_mgr_->getMgrState() != D2QueueMgr::RUNNING) &&
+ (queue_mgr_->getMgrState() != D2QueueMgr::STOPPING))
+ && (queue_mgr_->getQueueSize() == 0)
+ && (update_mgr_->getTransactionCount() == 0));
+ break;
+
+ case SD_NOW:
+ // Get out right now, no niceties.
+ all_clear = true;
+ break;
+
+ default:
+ // shutdown_type_ is an enum and should only be one of the above.
+ // if its getting through to this, something is whacked.
+ break;
+ }
+
+ if (all_clear) {
+ LOG_INFO(dctl_logger,DHCP_DDNS_CLEARED_FOR_SHUTDOWN)
+ .arg(getShutdownTypeStr(shutdown_type_));
+ }
+ }
+
+ return (all_clear);
+}
+
+isc::data::ConstElementPtr
+D2Process::shutdown(isc::data::ConstElementPtr args) {
+ LOG_INFO(dctl_logger, DHCP_DDNS_SHUTDOWN).arg(args ? args->str()
+ : "(no args)");
+
+ // Default shutdown type is normal.
+ std::string type_str(getShutdownTypeStr(SD_NORMAL));
+ shutdown_type_ = SD_NORMAL;
+
+ if (args) {
+ if ((args->getType() == isc::data::Element::map) &&
+ args->contains("type")) {
+ type_str = args->get("type")->stringValue();
+
+ if (type_str == getShutdownTypeStr(SD_NORMAL)) {
+ shutdown_type_ = SD_NORMAL;
+ } else if (type_str == getShutdownTypeStr(SD_DRAIN_FIRST)) {
+ shutdown_type_ = SD_DRAIN_FIRST;
+ } else if (type_str == getShutdownTypeStr(SD_NOW)) {
+ shutdown_type_ = SD_NOW;
+ } else {
+ setShutdownFlag(false);
+ return (isc::config::createAnswer(1, "Invalid Shutdown type: "
+ + type_str));
+ }
+ }
+ }
+
+ // Set the base class's shutdown flag.
+ setShutdownFlag(true);
+ return (isc::config::createAnswer(0, "Shutdown initiated, type is: "
+ + type_str));
+}
+
+isc::data::ConstElementPtr
+D2Process::configure(isc::data::ConstElementPtr config_set) {
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_BASIC,
+ DHCP_DDNS_CONFIGURE).arg(config_set->str());
+
+ int rcode = 0;
+ isc::data::ConstElementPtr comment;
+ isc::data::ConstElementPtr answer = getCfgMgr()->parseConfig(config_set);;
+ comment = isc::config::parseAnswer(rcode, answer);
+
+ if (rcode) {
+ // Non-zero means we got an invalid configuration, take no further
+ // action. In integrated mode, this will send a failed response back
+ // to BIND10.
+ reconf_queue_flag_ = false;
+ return (answer);
+ }
+
+ // Set the reconf_queue_flag to indicate that we need to reconfigure
+ // the queue manager. Reconfiguring the queue manager may be asynchronous
+ // and require one or more events to occur, therefore we set a flag
+ // indicating it needs to be done but we cannot do it here. It must
+ // be done over time, while events are being processed. Remember that
+ // the method we are in now is invoked as part of the configuration event
+ // callback. This means you can't wait for events here, you are already
+ // in one.
+ // (@todo NOTE This could be turned into a bitmask of flags if we find other
+ // things that need reconfiguration. It might also be useful if we
+ // did some analysis to decide what if anything we need to do.)
+ reconf_queue_flag_ = true;
+
+ // If we are here, configuration was valid, at least it parsed correctly
+ // and therefore contained no invalid values.
+ // Return the success answer from above.
+ return (answer);
+}
+
+void
+D2Process::checkQueueStatus() {
+ switch (queue_mgr_->getMgrState()){
+ case D2QueueMgr::RUNNING:
+ if (reconf_queue_flag_ || shouldShutdown()) {
+ // If we need to reconfigure the queue manager or we have been
+ // told to shutdown, then stop listening first. Stopping entails
+ // canceling active listening which may generate an IO event, so
+ // instigate the stop and get out.
+ try {
+ LOG_INFO(dctl_logger, DHCP_DDNS_QUEUE_MGR_STOPPING)
+ .arg(reconf_queue_flag_ ? "reconfiguration"
+ : "shutdown");
+ queue_mgr_->stopListening();
+ } catch (const isc::Exception& ex) {
+ // It is very unlikey that we would experience an error
+ // here, but theoretically possible.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_STOP_ERROR)
+ .arg(ex.what());
+ }
+ }
+ break;
+
+ case D2QueueMgr::STOPPED_QUEUE_FULL: {
+ // Resume receiving once the queue has decreased by twenty
+ // percent. This is an arbitrary choice. @todo this value should
+ // probably be configurable.
+ size_t threshold = (((queue_mgr_->getMaxQueueSize()
+ * QUEUE_RESTART_PERCENT)) / 100);
+ if (queue_mgr_->getQueueSize() <= threshold) {
+ LOG_INFO (dctl_logger, DHCP_DDNS_QUEUE_MGR_RESUMING)
+ .arg(threshold).arg(queue_mgr_->getMaxQueueSize());
+ try {
+ queue_mgr_->startListening();
+ } catch (const isc::Exception& ex) {
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_RESUME_ERROR)
+ .arg(ex.what());
+ }
+ }
+
+ break;
+ }
+
+ case D2QueueMgr::STOPPED_RECV_ERROR:
+ // If the receive error is not due to some fallout from shutting
+ // down then we will attempt to recover by reconfiguring the listener.
+ // This will close and destruct the current listener and make a new
+ // one with new resources.
+ // @todo This may need a safety valve such as retry count or a timer
+ // to keep from endlessly retrying over and over, with little time
+ // in between.
+ if (!shouldShutdown()) {
+ LOG_INFO (dctl_logger, DHCP_DDNS_QUEUE_MGR_RECOVERING);
+ reconfigureQueueMgr();
+ }
+ break;
+
+ case D2QueueMgr::STOPPING:
+ // We are waiting for IO to cancel, so this is a NOP.
+ // @todo Possible timer for self-defense? We could conceivably
+ // get into a condition where we never get the event, which would
+ // leave us stuck in stopping. This is hugely unlikely but possible?
+ break;
+
+ default:
+ // If the reconfigure flag is set, then we are in a state now where
+ // we can do the reconfigure. In other words, we aren't RUNNING or
+ // STOPPING.
+ if (reconf_queue_flag_) {
+ LOG_INFO (dctl_logger, DHCP_DDNS_QUEUE_MGR_RECONFIGURING);
+ reconfigureQueueMgr();
+ }
+ break;
+ }
+}
+
+void
+D2Process::reconfigureQueueMgr() {
+ // Set reconfigure flag to false. We are only here because we have
+ // a valid configuration to work with so if we fail below, it will be
+ // an operational issue, such as a busy IP address. That will leave
+ // queue manager in INITTED state, which is fine.
+ // What we dont' want is to continually attempt to reconfigure so set
+ // the flag false now.
+ // @todo This method assumes only 1 type of listener. This will change
+ // to support at least a TCP version, possibly some form of RDBMS listener
+ // as well.
+ reconf_queue_flag_ = false;
+ try {
+ // Wipe out the current listener.
+ queue_mgr_->removeListener();
+
+ // Get the configuration parameters that affect Queue Manager.
+ // @todo Need to add parameters for listener TYPE, FORMAT, address reuse
+ std::string ip_address;
+ uint32_t port;
+ getCfgMgr()->getContext()->getParam("ip_address", ip_address);
+ getCfgMgr()->getContext()->getParam("port", port);
+ isc::asiolink::IOAddress addr(ip_address);
+
+ // Instantiate the listener.
+ queue_mgr_->initUDPListener(addr, port, dhcp_ddns::FMT_JSON, true);
+
+ // Now start it. This assumes that starting is a synchronous,
+ // blocking call that executes quickly. @todo Should that change then
+ // we will have to expand the state model to accommodate this.
+ queue_mgr_->startListening();
+ } catch (const isc::Exception& ex) {
+ // Queue manager failed to initialize and therefore not listening.
+ // This is most likely due to an unavailable IP address or port,
+ // which is a configuration issue.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_START_ERROR).arg(ex.what());
+ }
+}
+
+isc::data::ConstElementPtr
+D2Process::command(const std::string& command,
+ isc::data::ConstElementPtr args) {
+ // @todo This is the initial implementation. If and when D2 is extended
+ // to support its own commands, this implementation must change. Otherwise
+ // it should reject all commands as it does now.
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_BASIC, DHCP_DDNS_COMMAND)
+ .arg(command).arg(args ? args->str() : "(no args)");
+
+ return (isc::config::createAnswer(COMMAND_INVALID, "Unrecognized command: "
+ + command));
+}
+
+D2Process::~D2Process() {
+};
+
+D2CfgMgrPtr
+D2Process::getD2CfgMgr() {
+ // The base class gives a base class pointer to our configuration manager.
+ // Since we are D2, and we need D2 specific extensions, we need a pointer
+ // to D2CfgMgr for some things.
+ return (boost::dynamic_pointer_cast<D2CfgMgr>(getCfgMgr()));
+}
+
+const char* D2Process::getShutdownTypeStr(const ShutdownType& type) {
+ const char* str = "invalid";
+ switch (type) {
+ case SD_NORMAL:
+ str = "normal";
+ break;
+ case SD_DRAIN_FIRST:
+ str = "drain_first";
+ break;
+ case SD_NOW:
+ str = "now";
+ break;
+ default:
+ break;
+ }
+
+ return (str);
+}
+
+}; // namespace isc::d2
+}; // namespace isc
diff --git a/src/bin/d2/d2_process.h b/src/bin/d2/d2_process.h
new file mode 100644
index 0000000..5c76af5
--- /dev/null
+++ b/src/bin/d2/d2_process.h
@@ -0,0 +1,333 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_PROCESS_H
+#define D2_PROCESS_H
+
+#include <d2/d_process.h>
+#include <d2/d2_queue_mgr.h>
+#include <d2/d2_update_mgr.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief DHCP-DDNS Application Process
+///
+/// D2Process provides the top level application logic for DHCP-driven DDNS
+/// update processing. It provides the asynchronous event processing required
+/// to receive DNS mapping change requests and carry them out.
+/// It implements the DProcessBase interface, which structures it such that it
+/// is a managed "application", controlled by a management layer.
+class D2Process : public DProcessBase {
+public:
+
+ /// @brief Defines the shutdown types supported by D2Process
+ ///
+ /// * SD_NORMAL - Stops the queue manager and finishes all current
+ /// transactions before exiting. This is the default.
+ ///
+ /// * SD_DRAIN_FIRST - Stops the queue manager but continues processing
+ /// requests from the queue until it is empty.
+ ///
+ /// * SD_NOW - Exits immediately.
+ enum ShutdownType {
+ SD_NORMAL,
+ SD_DRAIN_FIRST,
+ SD_NOW
+ };
+
+ /// @brief Defines the point at which to resume receiving requests.
+ /// If the receive queue has become full, D2Process will "pause" the
+ /// reception of requests by putting the queue manager in the stopped
+ /// state. Once the number of entries has decreased to this percentage
+ /// of the maximum allowed, D2Process will "resume" receiving requests
+ /// by restarting the queue manager.
+ static const unsigned int QUEUE_RESTART_PERCENT;
+
+ /// @brief Constructor
+ ///
+ /// Construction creates the configuration manager, the queue
+ /// manager, and the update manager.
+ ///
+ /// @param name name is a text label for the process. Generally used
+ /// in log statements, but otherwise arbitrary.
+ /// @param io_service is the io_service used by the caller for
+ /// asynchronous event handling.
+ ///
+ /// @throw DProcessBaseError is io_service is NULL.
+ D2Process(const char* name, IOServicePtr io_service);
+
+ /// @brief Called after instantiation to perform initialization unique to
+ /// D2.
+ ///
+ /// This is invoked by the controller after command line arguments but
+ /// PRIOR to configuration reception. The base class provides this method
+ /// as a place to perform any derivation-specific initialization steps
+ /// that are inapppropriate for the constructor but necessary prior to
+ /// launch. So far, no such steps have been identified for D2, so its
+ /// implementantion is empty but required.
+ ///
+ /// @throw DProcessBaseError if the initialization fails.
+ virtual void init();
+
+ /// @brief Implements the process's event loop.
+ ///
+ /// Once entered, the main control thread remains inside this method
+ /// until shutdown. The event loop logic is as follows:
+ /// @code
+ /// while should not down {
+ /// process queue manager state change
+ /// process completed jobs
+ /// dequeue new jobs
+ /// wait for IO event(s)
+ ///
+ /// ON an exception, exit with fatal error
+ /// }
+ /// @endcode
+ ///
+ /// To summarize, each pass through the event loop first checks the state
+ /// of the received queue and takes any steps required to ensure it is
+ /// operating in the manner necessary. Next the update manager is given
+ /// a chance to clean up any completed transactions and start new
+ /// transactions by dequeuing jobs from the request queue. Lastly, it
+ /// allows IOService to process until one or more event handlers are
+ /// called. Note that this last step will block until at least one
+ /// ready handler is invoked. In other words, if no IO events have occurred
+ /// since it was last called, the event loop will block at this step until
+ /// an IO event occurs. At that time we return to the top of the loop.
+ ///
+ /// @throw DProcessBaseError if an error is encountered. Note that
+ /// exceptions thrown at this point are assumed to be FATAL exceptions.
+ /// This includes exceptions generated but not caught by IO callbacks.
+ /// Services which rely on callbacks are expected to be well behaved and
+ /// any errors they encounter handled internally.
+ virtual void run();
+
+ /// @brief Initiates the D2Process shutdown process.
+ ///
+ /// This is last step in the shutdown event callback chain. It is invoked
+ /// to notify D2Process that it needs to begin its shutdown procedure.
+ /// Note that shutting down may be neither instantaneous nor synchronous,
+ /// This method records the request for and the type of shutdown desired.
+ /// Generally it will require one or more subsequent events to complete,
+ /// dependent on the type of shutdown requested. The type of shutdown is
+ /// specified as an optional argument of the shutdown command. The types
+ /// of shutdown supported are:
+ ///
+ /// * "normal" - Stops the queue manager and finishes all current
+ /// transactions before exiting. This is the default.
+ ///
+ /// * "drain_first" - Stops the queue manager but continues processing
+ /// requests from the queue until it is empty.
+ ///
+ /// * "now" - Exits immediately.
+ ///
+ /// @param args Specifies the shutdown "type" as "normal", "drain_first",
+ /// or "now"
+ ///
+ /// @return an Element that contains the results of argument processing,
+ /// consisting of an integer status value (0 means successful,
+ /// non-zero means failure), and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr
+ shutdown(isc::data::ConstElementPtr args);
+
+ /// @brief Processes the given configuration.
+ ///
+ /// This method may be called multiple times during the process lifetime.
+ /// Certainly once during process startup, and possibly later if the user
+ /// alters configuration. This method must not throw, it should catch any
+ /// processing errors and return a success or failure answer as described
+ /// below.
+ ///
+ /// This method passes the newly received configuration to the configuration
+ /// manager instance for parsing. The configuration manager parses the
+ /// configuration and updates the necessary values within the context,
+ /// assuming it parses correctly. If that's the case this method sets the
+ /// flag to reconfigure the queue manager and returns a successful response
+ /// as described below.
+ ///
+ /// If the new configuration fails to parse, then the current configuration
+ /// is retained and a failure response is returned as described below.
+ ///
+ /// @param config_set a new configuration (JSON) for the process
+ /// @return an Element that contains the results of configuration composed
+ /// of an integer status value (0 means successful, non-zero means failure),
+ /// and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr configure(isc::data::ConstElementPtr
+ config_set);
+
+ /// @brief Processes the given command.
+ ///
+ /// This method is called to execute any custom commands supported by the
+ /// process. This method must not throw, it should catch any processing
+ /// errors and return a success or failure answer as described below.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command. It can be a NULL pointer if no arguments exist for a command.
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value (0 means successful, non-zero means failure),
+ /// and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr command(const std::string& command,
+ isc::data::ConstElementPtr args);
+ /// @brief Destructor
+ virtual ~D2Process();
+
+protected:
+ /// @brief Monitors current queue manager state, takes action accordingly
+ ///
+ /// This method ensures that the queue manager transitions to the state
+ /// most appropriate to the operational state of the D2Process and any
+ /// events that may have occurred since it was last called. It is called
+ /// once for each iteration of the event loop. It is essentially a
+ /// switch statement based on the D2QueueMgr's current state. The logic
+ /// is as follows:
+ ///
+ /// If the state is D2QueueMgr::RUNNING, and the queue manager needs to be
+ /// reconfigured or we have been told to shutdown, then instruct the queue
+ /// manager to stop listening. Exit the method.
+ ///
+ /// If the state is D2QueueMgr::STOPPED_QUEUE_FULL, then check if the
+ /// number of entries in the queue has fallen below the "resume threshold".
+ /// If it has, then instruct the queue manager to start listening. Exit
+ /// the method.
+ ///
+ /// If the state is D2QueueMgr::STOPPED_RECV_ERROR, then attempt to recover
+ /// by calling reconfigureQueueMgr(). Exit the method.
+ ///
+ /// If the state is D2QueueMgr::STOPPING, simply exit the method. This is
+ /// a NOP condition as we are waiting for the IO cancel event
+ ///
+ /// For any other state, (NOT_INITTED,INITTED,STOPPED), if the reconfigure
+ /// queue flag is set, call reconfigureQueueMgr(). Exit the method.
+ ///
+ /// This method is exception safe.
+ virtual void checkQueueStatus();
+
+ /// @brief Initializes then starts the queue manager.
+ ///
+ /// This method is initializes the queue manager with the current
+ /// configuration parameters and instructs it to start listening.
+ /// Note the existing listener instance (if it exists) is destroyed,
+ /// and that a new listener is created during initialization.
+ ///
+ /// This method is exception safe.
+ virtual void reconfigureQueueMgr();
+
+ /// @brief Allows IO processing to run until at least callback is invoked.
+ ///
+ /// This method is called from within the D2Process main event loop and is
+ /// the point at which the D2Process blocks, waiting for IO events to
+ /// cause IO event callbacks to be invoked.
+ ///
+ /// If callbacks are ready to be executed upon entry, the method will
+ /// return as soon as these callbacks have completed. If no callbacks
+ /// are ready, then it will wait (indefinitely) until at least one callback
+ /// is executed.
+ ///
+ /// @note: Should become desirable to periodically force an
+ /// event, an interval timer could be used to do so.
+ ///
+ /// @return The number of callback handlers executed, or 0 if the IO
+ /// service has been stopped.
+ ///
+ /// @throw This method does not throw directly, but the execution of
+ /// callbacks invoked in response to IO events might. If so, these
+ /// will propagate upward out of this method.
+ virtual size_t runIO();
+
+ /// @brief Indicates whether or not the process can perform a shutdown.
+ ///
+ /// Determines if the process has been instructed to shutdown and if
+ /// the criteria for performing the type of shutdown requested has been
+ /// met.
+ ///
+ /// @return Returns true if the criteria has been met, false otherwise.
+ virtual bool canShutdown() const;
+
+ /// @brief Sets queue reconfigure indicator to the given value.
+ ///
+ /// @param value is the new value to assign to the indicator
+ ///
+ /// @note this method is really only intended for testing purposes.
+ void setReconfQueueFlag(const bool value) {
+ reconf_queue_flag_ = value;
+ }
+
+ /// @brief Sets the shutdown type to the given value.
+ ///
+ /// @param value is the new value to assign to shutdown type.
+ ///
+ /// @note this method is really only intended for testing purposes.
+ void setShutdownType(const ShutdownType& value) {
+ shutdown_type_ = value;
+ }
+
+public:
+ /// @brief Returns a pointer to the configuration manager.
+ /// Note, this method cannot return a reference as it uses dynamic
+ /// pointer casting of the base class configuration manager.
+ D2CfgMgrPtr getD2CfgMgr();
+
+ /// @brief Returns a reference to the queue manager.
+ const D2QueueMgrPtr& getD2QueueMgr() const {
+ return (queue_mgr_);
+ }
+
+ /// @brief Returns a reference to the update manager.
+ const D2UpdateMgrPtr& getD2UpdateMgr() const {
+ return (update_mgr_);
+ }
+
+ /// @brief Returns true if the queue manager should be reconfigured.
+ bool getReconfQueueFlag() const {
+ return (reconf_queue_flag_);
+ }
+
+ /// @brief Returns the type of shutdown requested.
+ ///
+ /// Note, this value is meaningless unless shouldShutdown() returns true.
+ ShutdownType getShutdownType() const {
+ return (shutdown_type_);
+ }
+
+ /// @brief Returns a text label for the given shutdown type.
+ ///
+ /// @param type the numerical shutdown type for which the label is desired.
+ ///
+ /// @return A text label corresponding the value or "invalid" if the
+ /// value is not a valid value.
+ static const char* getShutdownTypeStr(const ShutdownType& type);
+
+private:
+ /// @brief Pointer to our queue manager instance.
+ D2QueueMgrPtr queue_mgr_;
+
+ /// @brief Pointer to our update manager instance.
+ D2UpdateMgrPtr update_mgr_;
+
+ /// @brief Indicates if the queue manager should be reconfigured.
+ bool reconf_queue_flag_;
+
+ /// @brief Indicates the type of shutdown requested.
+ ShutdownType shutdown_type_;
+};
+
+/// @brief Defines a shared pointer to D2Process.
+typedef boost::shared_ptr<D2Process> D2ProcessPtr;
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/d2_queue_mgr.cc b/src/bin/d2/d2_queue_mgr.cc
new file mode 100644
index 0000000..041c2e8
--- /dev/null
+++ b/src/bin/d2/d2_queue_mgr.cc
@@ -0,0 +1,263 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/d2_queue_mgr.h>
+#include <dhcp_ddns/ncr_udp.h>
+
+namespace isc {
+namespace d2 {
+
+// Makes constant visible to Google test macros.
+const size_t D2QueueMgr::MAX_QUEUE_DEFAULT;
+
+D2QueueMgr::D2QueueMgr(IOServicePtr& io_service, const size_t max_queue_size)
+ : io_service_(io_service), max_queue_size_(max_queue_size),
+ mgr_state_(NOT_INITTED), target_stop_state_(NOT_INITTED) {
+ if (!io_service_) {
+ isc_throw(D2QueueMgrError, "IOServicePtr cannot be null");
+ }
+
+ // Use setter to do validation.
+ setMaxQueueSize(max_queue_size);
+}
+
+D2QueueMgr::~D2QueueMgr() {
+}
+
+void
+D2QueueMgr::operator()(const dhcp_ddns::NameChangeListener::Result result,
+ dhcp_ddns::NameChangeRequestPtr& ncr) {
+ try {
+ // Note that error conditions must be handled here without throwing
+ // exceptions. Remember this is the application level "link" in the
+ // callback chain. Throwing an exception here will "break" the
+ // io_service "run" we are operating under. With that in mind,
+ // if we hit a problem, we will stop the listener transition to
+ // the appropriate stopped state. Upper layer(s) must monitor our
+ // state as well as our queue size.
+ switch (result) {
+ case dhcp_ddns::NameChangeListener::SUCCESS:
+ // Receive was successful, attempt to queue the request.
+ if (getQueueSize() < getMaxQueueSize()) {
+ // There's room on the queue, add to the end
+ enqueue(ncr);
+ return;
+ }
+
+ // Queue is full, stop the listener.
+ // Note that we can move straight to a STOPPED state as there
+ // is no receive in progress.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_QUEUE_FULL)
+ .arg(max_queue_size_);
+ stopListening(STOPPED_QUEUE_FULL);
+ break;
+
+ case dhcp_ddns::NameChangeListener::STOPPED:
+ if (mgr_state_ == STOPPING) {
+ // This is confirmation that the listener has stopped and its
+ // callback will not be called again, unless its restarted.
+ updateStopState();
+ } else {
+ // We should not get an receive complete status of stopped
+ // unless we canceled the read as part of stopping. Therefore
+ // this is unexpected so we will treat it as a receive error.
+ // This is most likely an unforeseen programmatic issue.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_UNEXPECTED_STOP)
+ .arg(mgr_state_);
+ stopListening(STOPPED_RECV_ERROR);
+ }
+
+ break;
+
+ default:
+ // Receive failed, stop the listener.
+ // Note that we can move straight to a STOPPED state as there
+ // is no receive in progress.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_RECV_ERROR);
+ stopListening(STOPPED_RECV_ERROR);
+ break;
+ }
+ } catch (const std::exception& ex) {
+ // On the outside chance a throw occurs, let's log it and swallow it.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_QUEUE_MGR_UNEXPECTED_HANDLER_ERROR)
+ .arg(ex.what());
+ }
+}
+
+void
+D2QueueMgr::initUDPListener(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port,
+ const dhcp_ddns::NameChangeFormat format,
+ const bool reuse_address) {
+
+ if (listener_) {
+ isc_throw(D2QueueMgrError,
+ "D2QueueMgr listener is already initialized");
+ }
+
+ // Instantiate a UDP listener and set state to INITTED.
+ // Note UDP listener constructor does not throw.
+ listener_.reset(new dhcp_ddns::
+ NameChangeUDPListener(ip_address, port, format, *this,
+ reuse_address));
+ mgr_state_ = INITTED;
+}
+
+void
+D2QueueMgr::startListening() {
+ // We can't listen if we haven't initialized the listener yet.
+ if (!listener_) {
+ isc_throw(D2QueueMgrError, "D2QueueMgr "
+ "listener is not initialized, cannot start listening");
+ }
+
+ // If we are already listening, we do not want to "reopen" the listener
+ // and really we shouldn't be trying.
+ if (mgr_state_ == RUNNING) {
+ isc_throw(D2QueueMgrError, "D2QueueMgr "
+ "cannot call startListening from the RUNNING state");
+ }
+
+ // Instruct the listener to start listening and set state accordingly.
+ try {
+ listener_->startListening(*io_service_);
+ mgr_state_ = RUNNING;
+ } catch (const isc::Exception& ex) {
+ isc_throw(D2QueueMgrError, "D2QueueMgr listener start failed: "
+ << ex.what());
+ }
+
+ LOG_INFO (dctl_logger, DHCP_DDNS_QUEUE_MGR_STARTED);
+}
+
+void
+D2QueueMgr::stopListening(const State target_stop_state) {
+ if (listener_) {
+ // Enforce only valid "stop" states.
+ // This is purely a programmatic error and should never happen.
+ if (target_stop_state != STOPPED &&
+ target_stop_state != STOPPED_QUEUE_FULL &&
+ target_stop_state != STOPPED_RECV_ERROR) {
+ isc_throw(D2QueueMgrError,
+ "D2QueueMgr invalid value for stop state: "
+ << target_stop_state);
+ }
+
+ // Remember the state we want to acheive.
+ target_stop_state_ = target_stop_state;
+
+ // Instruct the listener to stop. If the listener reports that it
+ // has IO pending, then we transition to STOPPING to wait for the
+ // cancellation event. Otherwise, we can move directly to the targeted
+ // state.
+ listener_->stopListening();
+ if (listener_->isIoPending()) {
+ mgr_state_ = STOPPING;
+ } else {
+ updateStopState();
+ }
+ }
+}
+
+void
+D2QueueMgr::updateStopState() {
+ mgr_state_ = target_stop_state_;
+ LOG_INFO (dctl_logger, DHCP_DDNS_QUEUE_MGR_STOPPED);
+}
+
+
+void
+D2QueueMgr::removeListener() {
+ // Force our managing layer(s) to stop us properly first.
+ if (mgr_state_ == RUNNING) {
+ isc_throw(D2QueueMgrError,
+ "D2QueueMgr cannot delete listener while state is RUNNING");
+ }
+
+ listener_.reset();
+ mgr_state_ = NOT_INITTED;
+}
+
+const dhcp_ddns::NameChangeRequestPtr&
+D2QueueMgr::peek() const {
+ if (getQueueSize() == 0) {
+ isc_throw(D2QueueMgrQueueEmpty,
+ "D2QueueMgr peek attempted on an empty queue");
+ }
+
+ return (ncr_queue_.front());
+}
+
+const dhcp_ddns::NameChangeRequestPtr&
+D2QueueMgr::peekAt(const size_t index) const {
+ if (index >= getQueueSize()) {
+ isc_throw(D2QueueMgrInvalidIndex,
+ "D2QueueMgr peek beyond end of queue attempted"
+ << " index: " << index << " queue size: " << getQueueSize());
+ }
+
+ return (ncr_queue_.at(index));
+}
+
+void
+D2QueueMgr::dequeueAt(const size_t index) {
+ if (index >= getQueueSize()) {
+ isc_throw(D2QueueMgrInvalidIndex,
+ "D2QueueMgr dequeue beyond end of queue attempted"
+ << " index: " << index << " queue size: " << getQueueSize());
+ }
+
+ RequestQueue::iterator pos = ncr_queue_.begin() + index;
+ ncr_queue_.erase(pos);
+}
+
+
+void
+D2QueueMgr::dequeue() {
+ if (getQueueSize() == 0) {
+ isc_throw(D2QueueMgrQueueEmpty,
+ "D2QueueMgr dequeue attempted on an empty queue");
+ }
+
+ ncr_queue_.pop_front();
+}
+
+void
+D2QueueMgr::enqueue(dhcp_ddns::NameChangeRequestPtr& ncr) {
+ ncr_queue_.push_back(ncr);
+}
+
+void
+D2QueueMgr::clearQueue() {
+ ncr_queue_.clear();
+}
+
+void
+D2QueueMgr::setMaxQueueSize(const size_t new_queue_max) {
+ if (new_queue_max < 1) {
+ isc_throw(D2QueueMgrError,
+ "D2QueueMgr maximum queue size must be greater than zero");
+ }
+
+ if (new_queue_max < getQueueSize()) {
+ isc_throw(D2QueueMgrError, "D2QueueMgr maximum queue size value cannot"
+ " be less than the current queue size :" << getQueueSize());
+ }
+
+ max_queue_size_ = new_queue_max;
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/d2_queue_mgr.h b/src/bin/d2/d2_queue_mgr.h
new file mode 100644
index 0000000..c9b0298
--- /dev/null
+++ b/src/bin/d2/d2_queue_mgr.h
@@ -0,0 +1,354 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_QUEUE_MGR_H
+#define D2_QUEUE_MGR_H
+
+/// @file d2_queue_mgr.h This file defines the class D2QueueMgr.
+
+#include <exceptions/exceptions.h>
+#include <d2/d2_asio.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcp_ddns/ncr_io.h>
+
+#include <boost/noncopyable.hpp>
+#include <deque>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Defines a queue of requests.
+/// @todo This may be replaced with an actual class in the future.
+typedef std::deque<dhcp_ddns::NameChangeRequestPtr> RequestQueue;
+
+/// @brief Thrown if the queue manager encounters a general error.
+class D2QueueMgrError : public isc::Exception {
+public:
+ D2QueueMgrError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Thrown if the queue manager's receive handler is passed
+/// a failure result.
+class D2QueueMgrReceiveError : public isc::Exception {
+public:
+ D2QueueMgrReceiveError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Thrown if the request queue is full when an enqueue is attempted.
+class D2QueueMgrQueueFull : public isc::Exception {
+public:
+ D2QueueMgrQueueFull(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Thrown if the request queue empty and a read is attempted.
+class D2QueueMgrQueueEmpty : public isc::Exception {
+public:
+ D2QueueMgrQueueEmpty(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Thrown if a queue index is beyond the end of the queue
+class D2QueueMgrInvalidIndex : public isc::Exception {
+public:
+ D2QueueMgrInvalidIndex(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief D2QueueMgr creates and manages a queue of DNS update requests.
+///
+/// D2QueueMgr is class specifically designed as an integral part of DHCP-DDNS.
+/// Its primary responsibility is to listen for NameChangeRequests from
+/// DHCP-DDNS clients (e.g. DHCP servers) and queue them for processing. In
+/// addition it may provide a number services to locate entries in the queue
+/// such as by FQDN or DHCID. These services may eventually be used
+/// for processing optimization. The initial implementation will support
+/// simple FIFO access.
+///
+/// D2QueueMgr uses a NameChangeListener to asynchronously receive requests.
+/// It derives from NameChangeListener::RequestReceiveHandler and supplies an
+/// implementation of the operator()(Result, NameChangeRequestPtr). It is
+/// through this operator() that D2QueueMgr is passed inbound NCRs. D2QueueMgr
+/// will add each newly received request onto the back of the request queue
+///
+/// D2QueueMgr defines a simple state model constructed around the status of
+/// its NameChangeListener, consisting of the following states:
+///
+/// * NOT_INITTED - D2QueueMgr has been constructed, but its listener has
+/// not been initialized.
+///
+/// * INITTED - The listener has been initialized, but it is not open for
+/// listening. To move from NOT_INITTED to INITTED, one of the D2QueueMgr
+/// listener initialization methods must be invoked. Currently there is
+/// only one type of listener, NameChangeUDPListener, hence there is only
+/// one listener initialization method, initUDPListener. As more listener
+/// types are created, listener initialization methods will need to be
+/// added.
+///
+/// * RUNNING - The listener is open and listening for requests.
+/// Once initialized, in order to begin listening for requests, the
+/// startListener() method must be invoked. Upon successful completion of
+/// of this call, D2QueueMgr will begin receiving requests as they arrive
+/// without any further steps. This method may be called from the INITTED
+/// or one of the STOPPED states.
+///
+/// * STOPPING - The listener is in the process of stopping active
+/// listening. This is transitory state between RUNNING and STOPPED, which
+/// is completed by IO cancellation event.
+///
+/// * STOPPED - The listener has been listening but has been stopped
+/// without error. To return to listening, startListener() must be invoked.
+///
+/// * STOPPED_QUEUE_FULL - Request queue is full, the listener has been
+/// stopped. D2QueueMgr will enter this state when the request queue
+/// reaches the maximum queue size. Once this limit is reached, the
+/// listener will be closed and no further requests will be received.
+/// To return to listening, startListener() must be invoked. Note that so
+/// long as the queue is full, any attempt to queue a request will fail.
+///
+/// * STOPPED_RECV_ERROR - The listener has experienced a receive error
+/// and has been stopped. D2QueueMgr will enter this state when it is
+/// passed a failed status into the request completion handler. To return
+/// to listening, startListener() must be invoked.
+///
+/// D2QueueMgr does not attempt to recover from stopped conditions, this is left
+/// to upper layers.
+///
+/// It is important to note that the queue contents are preserved between
+/// state transitions. In other words entries in the queue remain there
+/// until they are removed explicitly via the deque() or implicitly by
+/// via the clearQueue() method.
+///
+class D2QueueMgr : public dhcp_ddns::NameChangeListener::RequestReceiveHandler,
+ boost::noncopyable {
+public:
+ /// @brief Maximum number of entries allowed in the request queue.
+ /// NOTE that 1024 is an arbitrary choice picked for the initial
+ /// implementation.
+ static const size_t MAX_QUEUE_DEFAULT = 1024;
+
+ /// @brief Defines the list of possible states for D2QueueMgr.
+ enum State {
+ NOT_INITTED,
+ INITTED,
+ RUNNING,
+ STOPPING,
+ STOPPED_QUEUE_FULL,
+ STOPPED_RECV_ERROR,
+ STOPPED,
+ };
+
+ /// @brief Constructor
+ ///
+ /// Creates a D2QueueMgr instance. Note that the listener is not created
+ /// in the constructor. The initial state will be NOT_INITTED.
+ ///
+ /// @param io_service IOService instance to be passed into the listener for
+ /// IO management.
+ /// @param max_queue_size the maximum number of entries allowed in the
+ /// queue.
+ /// This value must be greater than zero. It defaults to MAX_QUEUE_DEFAULT.
+ ///
+ /// @throw D2QueueMgrError if max_queue_size is zero.
+ D2QueueMgr(IOServicePtr& io_service,
+ const size_t max_queue_size = MAX_QUEUE_DEFAULT);
+
+ /// @brief Destructor
+ virtual ~D2QueueMgr();
+
+ /// @brief Initializes the listener as a UDP listener.
+ ///
+ /// Instantiates the listener_ member as NameChangeUDPListener passing
+ /// the given parameters. Upon successful completion, the D2QueueMgr state
+ /// will be INITTED.
+ ///
+ /// @param ip_address is the network address on which to listen
+ /// @param port is the IP port on which to listen
+ /// @param format is the wire format of the inbound requests.
+ /// @param reuse_address enables IP address sharing when true
+ /// It defaults to false.
+ void initUDPListener(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port,
+ const dhcp_ddns::NameChangeFormat format,
+ const bool reuse_address = false);
+
+ /// @brief Starts actively listening for requests.
+ ///
+ /// Invokes the listener's startListening method passing in our
+ /// IOService instance.
+ ///
+ /// @throw D2QueueMgrError if the listener has not been initialized,
+ /// state is already RUNNING, or the listener fails to actually start.
+ void startListening();
+
+ /// @brief Function operator implementing the NCR receive callback.
+ ///
+ /// This method is invoked by the listener as part of its receive
+ /// completion callback and is how the inbound NameChangeRequests are
+ /// passed up to the D2QueueMgr for queueing.
+ /// If the given result indicates a successful receive completion and
+ /// there is room left in the queue, the given request is queued.
+ ///
+ /// If the queue is at maximum capacity, stopListening() is invoked and
+ /// the state is set to STOPPED_QUEUE_FULL.
+ ///
+ /// If the result indicates IO stopped, then the state is set to STOPPED.
+ /// Note this is not an error, it results from a deliberate cancellation
+ /// of listener IO as part of a normal stopListener call.
+ ///
+ /// If the result indicates a failed receive, stopListening() is invoked
+ /// and the state is set to STOPPED_RECV_ERROR.
+ ///
+ /// This method specifically avoids throwing on an error as any such throw
+ /// would surface at the io_service::run (or run variant) method invocation
+ /// site. The upper layers are expected to monitor D2QueueMgr's state and
+ /// act accordingly.
+ ///
+ /// @param result contains that receive outcome status.
+ /// @param ncr is a pointer to the newly received NameChangeRequest if
+ /// result is NameChangeListener::SUCCESS. It is indeterminate other
+ /// wise.
+ virtual void operator ()(const dhcp_ddns::NameChangeListener::Result result,
+ dhcp_ddns::NameChangeRequestPtr& ncr);
+
+ /// @brief Stops listening for requests.
+ ///
+ /// Invokes the listener's stopListening method which will cause it to
+ /// cancel any pending IO and close its IO source. It the sets target
+ /// stop state to the given value.
+ ///
+ /// If there is no IO pending, the manager state is immediately set to the
+ /// target stop state, otherwise the manager state is set to STOPPING.
+ ///
+ /// @param target_stop_state is one of the three stopped state values.
+ ///
+ /// @throw D2QueueMgrError if stop_state is a valid stop state.
+ void stopListening(const State target_stop_state = STOPPED);
+
+
+ /// @brief Deletes the current listener
+ ///
+ /// This method will delete the current listener and returns the manager
+ /// to the NOT_INITTED state. This is provided to support reconfiguring
+ /// a new listener without losing queued requests.
+ ///
+ /// @throw D2QueMgrError if called when the manager state is RUNNING.
+ void removeListener();
+
+ /// @brief Returns the number of entries in the queue.
+ size_t getQueueSize() const {
+ return (ncr_queue_.size());
+ };
+
+ /// @brief Returns the maximum number of entries allowed in the queue.
+ size_t getMaxQueueSize() const {
+ return (max_queue_size_);
+ }
+
+ /// @brief Sets the maximum number of entries allowed in the queue.
+ ///
+ /// @param max_queue_size is the new maximum size of the queue.
+ ///
+ /// @throw D2QueueMgrError if the new value is less than one or if
+ /// the new value is less than the number of entries currently in the
+ /// queue.
+ void setMaxQueueSize(const size_t max_queue_size);
+
+ /// @brief Returns the current state.
+ State getMgrState() const {
+ return (mgr_state_);
+ }
+
+ /// @brief Returns the entry at the front of the queue.
+ ///
+ /// The entry returned is next in line to be processed, assuming a FIFO
+ /// approach to task selection. Note, the entry is not removed from the
+ /// queue.
+ ///
+ /// @return Pointer reference to the queue entry.
+ ///
+ /// @throw D2QueueMgrQueEmpty if there are no entries in the queue.
+ const dhcp_ddns::NameChangeRequestPtr& peek() const;
+
+ /// @brief Returns the entry at a given position in the queue.
+ ///
+ /// Note that the entry is not removed from the queue.
+ /// @param index the index of the entry in the queue to fetch.
+ /// Valid values are 0 (front of the queue) to (queue size - 1).
+ ///
+ /// @return Pointer reference to the queue entry.
+ ///
+ /// @throw D2QueueMgrInvalidIndex if the given index is beyond the
+ /// end of the queue.
+ const dhcp_ddns::NameChangeRequestPtr& peekAt(const size_t index) const;
+
+ /// @brief Removes the entry at a given position in the queue.
+ ///
+ /// @param index the index of the entry in the queue to remove.
+ /// Valid values are 0 (front of the queue) to (queue size - 1).
+ ///
+ /// @throw D2QueueMgrInvalidIndex if the given index is beyond the
+ /// end of the queue.
+ void dequeueAt(const size_t index);
+
+ /// @brief Removes the entry at the front of the queue.
+ ///
+ /// @throw D2QueueMgrQueEmpty if there are no entries in the queue.
+ void dequeue();
+
+ /// @brief Adds a request to the end of the queue.
+ ///
+ /// @param ncr pointer to the NameChangeRequest to add to the queue.
+ void enqueue(dhcp_ddns::NameChangeRequestPtr& ncr);
+
+ /// @brief Removes all entries from the queue.
+ void clearQueue();
+
+ private:
+ /// @brief Sets the manager state to the target stop state.
+ ///
+ /// Convenience method which sets the manager state to the target stop
+ /// state and logs that the manager is stopped.
+ void updateStopState();
+
+ /// @brief IOService that our listener should use for IO management.
+ IOServicePtr io_service_;
+
+ /// @brief Dictates the maximum number of entries allowed in the queue.
+ size_t max_queue_size_;
+
+ /// @brief Queue of received NameChangeRequests.
+ RequestQueue ncr_queue_;
+
+ /// @brief Listener instance from which requests are received.
+ boost::shared_ptr<dhcp_ddns::NameChangeListener> listener_;
+
+ /// @brief Current state of the manager.
+ State mgr_state_;
+
+ /// @brief Tracks the state the manager should be in once stopped.
+ State target_stop_state_;
+};
+
+/// @brief Defines a pointer for manager instances.
+typedef boost::shared_ptr<D2QueueMgr> D2QueueMgrPtr;
+
+} // namespace isc::d2
+} // namespace isc
+
+#endif
diff --git a/src/bin/d2/d2_update_message.cc b/src/bin/d2/d2_update_message.cc
new file mode 100644
index 0000000..71ea5b2
--- /dev/null
+++ b/src/bin/d2/d2_update_message.cc
@@ -0,0 +1,221 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_update_message.h>
+#include <dns/messagerenderer.h>
+#include <dns/name.h>
+#include <dns/opcode.h>
+#include <dns/question.h>
+
+namespace isc {
+namespace d2 {
+
+using namespace isc::dns;
+
+D2UpdateMessage::D2UpdateMessage(const Direction direction)
+ : message_(direction == INBOUND ?
+ dns::Message::PARSE : dns::Message::RENDER) {
+ // If this object is to create an outgoing message, we have to
+ // set the proper Opcode field and QR flag here.
+ if (direction == OUTBOUND) {
+ message_.setOpcode(Opcode(Opcode::UPDATE_CODE));
+ message_.setHeaderFlag(dns::Message::HEADERFLAG_QR, false);
+ message_.setRcode(Rcode::NOERROR());
+ }
+}
+
+D2UpdateMessage::QRFlag
+D2UpdateMessage::getQRFlag() const {
+ return (message_.getHeaderFlag(dns::Message::HEADERFLAG_QR) ?
+ RESPONSE : REQUEST);
+}
+
+uint16_t
+D2UpdateMessage::getId() const {
+ return (message_.getQid());
+}
+
+void
+D2UpdateMessage::setId(const uint16_t id) {
+ message_.setQid(id);
+}
+
+
+const dns::Rcode&
+D2UpdateMessage::getRcode() const {
+ return (message_.getRcode());
+}
+
+void
+D2UpdateMessage::setRcode(const dns::Rcode& rcode) {
+ message_.setRcode(rcode);
+}
+
+unsigned int
+D2UpdateMessage::getRRCount(const UpdateMsgSection section) const {
+ return (message_.getRRCount(ddnsToDnsSection(section)));
+}
+
+const dns::RRsetIterator
+D2UpdateMessage::beginSection(const UpdateMsgSection section) const {
+ return (message_.beginSection(ddnsToDnsSection(section)));
+}
+
+const dns::RRsetIterator
+D2UpdateMessage::endSection(const UpdateMsgSection section) const {
+ return (message_.endSection(ddnsToDnsSection(section)));
+}
+
+void
+D2UpdateMessage::setZone(const Name& zone, const RRClass& rrclass) {
+ // The Zone data is kept in the underlying Question class. If there
+ // is a record stored there already, we need to remove it, because
+ // we may have at most one Zone record in the DNS Update message.
+ if (message_.getRRCount(dns::Message::SECTION_QUESTION) > 0) {
+ message_.clearSection(dns::Message::SECTION_QUESTION);
+ }
+ // Add the new record...
+ Question question(zone, rrclass, RRType::SOA());
+ message_.addQuestion(question);
+ // ... and update the local class member holding the D2Zone object.
+ zone_.reset(new D2Zone(question.getName(), question.getClass()));
+}
+
+D2ZonePtr
+D2UpdateMessage::getZone() const {
+ return (zone_);
+}
+
+void
+D2UpdateMessage::addRRset(const UpdateMsgSection section,
+ const dns::RRsetPtr& rrset) {
+ if (section == SECTION_ZONE) {
+ isc_throw(isc::BadValue, "unable to add RRset to the Zone section"
+ " of the DNS Update message, use setZone instead");
+ }
+ message_.addRRset(ddnsToDnsSection(section), rrset);
+}
+
+void
+D2UpdateMessage::toWire(AbstractMessageRenderer& renderer) {
+ // We are preparing the wire format of the message, meaning
+ // that this message will be sent as a request to the DNS.
+ // Therefore, we expect that this message is a REQUEST.
+ if (getQRFlag() != REQUEST) {
+ isc_throw(InvalidQRFlag, "QR flag must be cleared for the outgoing"
+ " DNS Update message");
+ }
+ // According to RFC2136, the ZONE section may contain exactly one
+ // record.
+ if (getRRCount(SECTION_ZONE) != 1) {
+ isc_throw(InvalidZoneSection, "Zone section of the DNS Update message"
+ " must comprise exactly one record (RFC2136, section 2.3)");
+ }
+ message_.toWire(renderer);
+}
+
+void
+D2UpdateMessage::fromWire(isc::util::InputBuffer& buffer) {
+ // First, use the underlying dns::Message implementation to get the
+ // contents of the DNS response message. Note that it may or may
+ // not be the message that we are interested in, but needs to be
+ // parsed so as we can check its ID, Opcode etc.
+ message_.fromWire(buffer);
+ // This class exposes the getZone() function. This function will return
+ // pointer to the D2Zone object if non-empty Zone section exists in the
+ // received message. It will return NULL pointer if it doesn't exist.
+ // The pointer is held in the D2UpdateMessage class member. We need to
+ // update this pointer every time we parse the message.
+ if (getRRCount(D2UpdateMessage::SECTION_ZONE) > 0) {
+ // There is a Zone section in the received message. Replace
+ // Zone pointer with the new value.
+ QuestionPtr question = *message_.beginQuestion();
+ // If the Zone counter is greater than 0 (which we have checked)
+ // there must be a valid Question pointer stored in the message_
+ // object. If there isn't, it is a programming error.
+ assert(question);
+ zone_.reset(new D2Zone(question->getName(), question->getClass()));
+
+ } else {
+ // Zone section doesn't hold any pointers, so set the pointer to NULL.
+ zone_.reset();
+
+ }
+ // Check that the content of the received message is sane.
+ // One of the basic checks to do is to verify that we have
+ // received the DNS update message. If not, it can be dropped
+ // or an error message can be printed. Other than that, we
+ // will check that there is at most one Zone record and QR flag
+ // is set.
+ validateResponse();
+}
+
+dns::Message::Section
+D2UpdateMessage::ddnsToDnsSection(const UpdateMsgSection section) {
+ /// The following switch maps the enumerator values from the
+ /// DNS Update message to the corresponding enumerator values
+ /// representing fields of the DNS message.
+ switch(section) {
+ case SECTION_ZONE :
+ return (dns::Message::SECTION_QUESTION);
+
+ case SECTION_PREREQUISITE:
+ return (dns::Message::SECTION_ANSWER);
+
+ case SECTION_UPDATE:
+ return (dns::Message::SECTION_AUTHORITY);
+
+ case SECTION_ADDITIONAL:
+ return (dns::Message::SECTION_ADDITIONAL);
+
+ default:
+ ;
+ }
+ isc_throw(dns::InvalidMessageSection,
+ "unknown message section " << section);
+}
+
+void
+D2UpdateMessage::validateResponse() const {
+ // Verify that we are dealing with the DNS Update message. According to
+ // RFC 2136, section 3.8 server will copy the Opcode from the query.
+ // If we are dealing with a different type of message, we may simply
+ // stop further processing, because it is likely that the message was
+ // directed to someone else.
+ if (message_.getOpcode() != Opcode::UPDATE()) {
+ isc_throw(NotUpdateMessage, "received message is not a DDNS update,"
+ << " received message code is "
+ << message_.getOpcode().getCode());
+ }
+ // Received message should have QR flag set, which indicates that it is
+ // a RESPONSE.
+ if (getQRFlag() == REQUEST) {
+ isc_throw(InvalidQRFlag, "received message should have QR flag set,"
+ " to indicate that it is a RESPONSE message; the QR"
+ << " flag in received message is unset");
+ }
+ // DNS server may copy a Zone record from the query message. Since query
+ // must comprise exactly one Zone record (RFC 2136, section 2.3), the
+ // response message may contain 1 record at most. It may also contain no
+ // records if a server chooses not to copy Zone section.
+ if (getRRCount(SECTION_ZONE) > 1) {
+ isc_throw(InvalidZoneSection, "received message contains "
+ << getRRCount(SECTION_ZONE) << " Zone records,"
+ << " it should contain at most 1 record");
+ }
+}
+
+} // namespace d2
+} // namespace isc
+
diff --git a/src/bin/d2/d2_update_message.h b/src/bin/d2/d2_update_message.h
new file mode 100644
index 0000000..955e5c0
--- /dev/null
+++ b/src/bin/d2/d2_update_message.h
@@ -0,0 +1,341 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_UPDATE_MESSAGE_H
+#define D2_UPDATE_MESSAGE_H
+
+#include <d2/d2_zone.h>
+#include <dns/message.h>
+#include <dns/name.h>
+#include <dns/rcode.h>
+#include <dns/rrclass.h>
+#include <dns/rrset.h>
+
+#include <map>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Exception indicating that Zone section contains invalid content.
+///
+/// This exception is thrown when ZONE section of the DNS Update message
+/// is invalid. According to RFC2136, section 2.3, the zone section is
+/// allowed to contain exactly one record. When Request message contains
+/// more records or is empty, this exception is thrown.
+class InvalidZoneSection : public Exception {
+public:
+ InvalidZoneSection(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Exception indicating that QR flag has invalid value.
+///
+/// This exception is thrown when QR flag has invalid value for
+/// the operation performed on the particular message. For instance,
+/// the QR flag must be set to indicate that the given message is
+/// a RESPONSE when @c D2UpdateMessage::fromWire is performed.
+/// The QR flag must be cleared when @c D2UpdateMessage::toWire
+/// is executed.
+class InvalidQRFlag : public Exception {
+public:
+ InvalidQRFlag(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Exception indicating that the parsed message is not DNS Update.
+///
+/// This exception is thrown when decoding the DNS message which is not
+/// a DNS Update.
+class NotUpdateMessage : public Exception {
+public:
+ NotUpdateMessage(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+class D2UpdateMessage;
+
+/// @brief Pointer to the DNS Update Message.
+typedef boost::shared_ptr<D2UpdateMessage> D2UpdateMessagePtr;
+
+/// @brief The @c D2UpdateMessage encapsulates a DNS Update message.
+///
+/// This class represents the DNS Update message. Functions exposed by this
+/// class allow to specify the data sections carried by the message and create
+/// an on-wire format of this message. This class is also used to decode
+/// messages received from the DNS server in the on-wire format.
+///
+/// <b>Design choice:</b> A dedicated class has been created to encapsulate
+/// DNS Update message because existing @c isc::dns::Message is designed to
+/// support regular DNS messages (described in RFC 1035) only. Although DNS
+/// Update has the same format, particular sections serve different purposes.
+/// In order to avoid rewrite of significant portions of @c isc::dns::Message
+/// class, this class is implemented in-terms-of @c isc::dns::Message class
+/// to reuse its functionality where possible.
+class D2UpdateMessage {
+public:
+
+ /// @brief Indicates if the @c D2UpdateMessage object encapsulates Inbound
+ /// or Outbound message.
+ enum Direction {
+ INBOUND,
+ OUTBOUND
+ };
+
+ /// @brief Indicates whether DNS Update message is a REQUEST or RESPONSE.
+ enum QRFlag {
+ REQUEST,
+ RESPONSE
+ };
+
+ /// @brief Identifies sections in the DNS Update Message.
+ ///
+ /// Each message comprises message Header and may contain the following
+ /// sections:
+ /// - ZONE
+ /// - PREREQUISITE
+ /// - UPDATE
+ /// - ADDITIONAL
+ ///
+ /// The enum elements are used by functions such as @c getRRCount (to get
+ /// the number of records in a corresponding section) and @c beginSection
+ /// and @c endSection (to access data in the corresponding section).
+ enum UpdateMsgSection {
+ SECTION_ZONE,
+ SECTION_PREREQUISITE,
+ SECTION_UPDATE,
+ SECTION_ADDITIONAL
+ };
+
+public:
+ /// @brief Constructor used to create an instance of the DNS Update Message
+ /// (either outgoing or incoming).
+ ///
+ /// This constructor is used to create an instance of either incoming or
+ /// outgoing DNS Update message. The boolean argument indicates wheteher it
+ /// is incoming (true) or outgoing (false) message. For incoming messages
+ /// the @c D2UpdateMessage::fromWire function is used to parse on-wire data.
+ /// For outgoing messages, modifier functions should be used to set the
+ /// message contents and @c D2UpdateMessage::toWire function to create
+ /// on-wire data.
+ ///
+ /// @param direction indicates if this is an inbound or outbound message.
+ D2UpdateMessage(const Direction direction = OUTBOUND);
+
+ ///
+ /// @name Copy constructor and assignment operator
+ ///
+ /// Copy constructor and assignment operator are private because we assume
+ /// there will be no need to copy messages on the client side.
+ //@{
+private:
+ D2UpdateMessage(const D2UpdateMessage& source);
+ D2UpdateMessage& operator=(const D2UpdateMessage& source);
+ //@}
+
+public:
+
+ /// @brief Returns enum value indicating if the message is a
+ /// REQUEST or RESPONSE
+ ///
+ /// The returned value is REQUEST if the message is created as an outgoing
+ /// message. In such case the QR flag bit in the message header is cleared.
+ /// The returned value is RESPONSE if the message is created as an incoming
+ /// message and the QR flag bit was set in the received message header.
+ ///
+ /// @return An enum value indicating whether the message is a
+ /// REQUEST or RESPONSE.
+ QRFlag getQRFlag() const;
+
+ /// @brief Returns message ID.
+ ///
+ /// @return message ID.
+ uint16_t getId() const;
+
+ /// @brief Sets message ID.
+ ///
+ /// @param id 16-bit value of the message id.
+ void setId(const uint16_t id);
+
+ /// @brief Returns an object representing message RCode.
+ ///
+ /// @return An object representing message RCode.
+ const dns::Rcode& getRcode() const;
+
+ /// @brief Sets message RCode.
+ ///
+ /// @param rcode An object representing message RCode.
+ void setRcode(const dns::Rcode& rcode);
+
+ /// @brief Returns number of RRsets in the specified message section.
+ ///
+ /// @param section An @c UpdateMsgSection enum specifying a message section
+ /// for which the number of RRsets is to be returned.
+ ///
+ /// @return A number of RRsets in the specified message section.
+ unsigned int getRRCount(const UpdateMsgSection section) const;
+
+ /// @name Functions returning iterators to RRsets in message sections.
+ ///
+ //@{
+ /// @brief Return iterators pointing to the beginning of the list of RRsets,
+ /// which belong to the specified section.
+ ///
+ /// @param section An @c UpdateMsgSection enum specifying a message section
+ /// for which the iterator should be returned.
+ ///
+ /// @return An iterator pointing to the beginning of the list of the
+ /// RRsets, which belong to the specified section.
+ const dns::RRsetIterator beginSection(const UpdateMsgSection section) const;
+
+ /// @brief Return iterators pointing to the end of the list of RRsets,
+ /// which belong to the specified section.
+ ///
+ /// @param section An @c UpdateMsgSection enum specifying a message section
+ /// for which the iterator should be returned.
+ ///
+ /// @return An iterator pointing to the end of the list of the
+ /// RRsets, which belong to the specified section.
+ const dns::RRsetIterator endSection(const UpdateMsgSection section) const;
+ //@}
+
+ /// @brief Sets the Zone record.
+ ///
+ /// This function creates the @c D2Zone object, representing a Zone record
+ /// for the outgoing message. If the Zone record is already set, it is
+ /// replaced by the new record being set by this function. The RRType for
+ /// the record is always SOA.
+ ///
+ /// @param zone A name of the zone being updated.
+ /// @param rrclass A class of the zone record.
+ void setZone(const dns::Name& zone, const dns::RRClass& rrclass);
+
+ /// @brief Returns a pointer to the object representing Zone record.
+ ///
+ /// @return A pointer to the object representing Zone record.
+ D2ZonePtr getZone() const;
+
+ /// @brief Adds an RRset to the specified section.
+ ///
+ /// This function may throw exception if the specified section is
+ /// out of bounds or Zone section update is attempted. For Zone
+ /// section @c D2UpdateMessage::setZone function should be used instead.
+ /// Also, this function expects that @c rrset argument is non-NULL.
+ ///
+ /// @param section A message section where the RRset should be added.
+ /// @param rrset A reference to a RRset which should be added.
+ void addRRset(const UpdateMsgSection section, const dns::RRsetPtr& rrset);
+
+ /// @name Functions to handle message encoding and decoding.
+ ///
+ //@{
+ /// @brief Encode outgoing message into wire format.
+ ///
+ /// This function encodes the DNS Update into the wire format. The format of
+ /// such a message is described in the RFC2136, section 2. Some of the
+ /// sections which belong to encoded message may be empty. If a particular
+ /// message section is empty (does not comprise any RRs), the corresponding
+ /// counter in the message header is set to 0. These counters are: PRCOUNT,
+ /// UPCOUNT, ADCOUNT for the Prerequisites, Update RRs and Additional Data
+ /// RRs respectively. The ZOCOUNT must be equal to 1 because RFC2136
+ /// requires that the message comprises exactly one Zone record.
+ ///
+ /// This function does not guarantee exception safety. However, exceptions
+ /// should be rare because @c D2UpdateMessage class API prevents invalid
+ /// use of the class. The typical case, when this function may throw an
+ /// exception is when this it is called on the object representing
+ /// incoming (instead of outgoing) message. In such case, the QR field
+ /// will be set to RESPONSE, which is invalid setting when calling this
+ /// function.
+ ///
+ /// @param renderer A renderer object used to generate the message wire
+ /// format.
+ void toWire(dns::AbstractMessageRenderer& renderer);
+
+ /// @brief Decode incoming message from the wire format.
+ ///
+ /// This function decodes the DNS Update message stored in the buffer
+ /// specified by the function argument. In the first turn, this function
+ /// parses message header and extracts the section counters: ZOCOUNT,
+ /// PRCOUNT, UPCOUNT and ADCOUNT. Using these counters, function identifies
+ /// message sections, which follow message header. These sections can be
+ /// later accessed using: @c D2UpdateMessage::getZone,
+ /// @c D2UpdateMessage::beginSection and @c D2UpdateMessage::endSection
+ /// functions.
+ ///
+ /// This function is NOT exception safe. It signals message decoding errors
+ /// through exceptions. Message decoding error may occur if the received
+ /// message does not conform to the general DNS Message format, specified in
+ /// RFC 1035. Errors which are specific to DNS Update messages include:
+ /// - Invalid Opcode - not an UPDATE.
+ /// - Invalid QR flag - the QR bit should be set to indicate that the
+ /// message is the server response.
+ /// - The number of records in the Zone section is greater than 1.
+ ///
+ /// @param buffer input buffer, holding DNS Update message to be parsed.
+ void fromWire(isc::util::InputBuffer& buffer);
+ //@}
+
+private:
+ /// Maps the values of the @c UpdateMessageSection field to the
+ /// corresponding values in the @c isc::dns::Message class. This
+ /// mapping is required here because this class uses @c isc::dns::Message
+ /// class to do the actual processing of the DNS Update message.
+ ///
+ /// @param section An enum indicating the section for which the
+ /// corresponding enum value from @c isc::dns::Message will be returned.
+ ///
+ /// @return The enum value indicating the section in the DNS message
+ /// represented by the @c isc::dns::Message class.
+ static
+ dns::Message::Section ddnsToDnsSection(const UpdateMsgSection section);
+
+ /// @brief Checks received response message for correctness.
+ ///
+ /// This function verifies that the received response from a server is
+ /// correct. Currently this function checks the following:
+ /// - Opcode is 'DNS Update',
+ /// - QR flag is RESPONSE (flag bit is set),
+ /// - Zone section comprises at most one record.
+ ///
+ /// The function will throw exception if any of the conditions above are
+ /// not met.
+ ///
+ /// @throw isc::d2::NotUpdateMessage if invalid Opcode.
+ /// @throw isc::d2::InvalidQRFlag if QR flag is not set to RESPONSE
+ /// @throw isc::d2::InvalidZone section, if Zone section comprises more
+ /// than one record.
+ void validateResponse() const;
+
+ /// @brief An object representing DNS Message which is used by the
+ /// implementation of @c D2UpdateMessage to perform low level.
+ ///
+ /// Declaration of this object pollutes the header with the details
+ /// of @c D2UpdateMessage implementation. It might be cleaner to use
+ /// Pimpl idiom to hide this object in an D2UpdateMessageImpl. However,
+ /// it would bring additional complications to the implementation
+ /// while the benefit would low - this header is not a part of any
+ /// common library. Therefore, if implementation is changed, modification of
+ /// private members of this class in the header has low impact.
+ dns::Message message_;
+
+ /// @brief Holds a pointer to the object, representing Zone in the DNS
+ /// Update.
+ D2ZonePtr zone_;
+
+};
+
+} // namespace d2
+} // namespace isc
+
+#endif // D2_UPDATE_MESSAGE_H
diff --git a/src/bin/d2/d2_update_mgr.cc b/src/bin/d2/d2_update_mgr.cc
new file mode 100644
index 0000000..abd1aa3
--- /dev/null
+++ b/src/bin/d2/d2_update_mgr.cc
@@ -0,0 +1,249 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_update_mgr.h>
+#include <d2/nc_add.h>
+#include <d2/nc_remove.h>
+
+#include <sstream>
+#include <iostream>
+#include <vector>
+
+namespace isc {
+namespace d2 {
+
+const size_t D2UpdateMgr::MAX_TRANSACTIONS_DEFAULT;
+
+D2UpdateMgr::D2UpdateMgr(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
+ IOServicePtr& io_service,
+ const size_t max_transactions)
+ :queue_mgr_(queue_mgr), cfg_mgr_(cfg_mgr), io_service_(io_service) {
+ if (!queue_mgr_) {
+ isc_throw(D2UpdateMgrError, "D2UpdateMgr queue manager cannot be null");
+ }
+
+ if (!cfg_mgr_) {
+ isc_throw(D2UpdateMgrError,
+ "D2UpdateMgr configuration manager cannot be null");
+ }
+
+ if (!io_service_) {
+ isc_throw(D2UpdateMgrError, "IOServicePtr cannot be null");
+ }
+
+ // Use setter to do validation.
+ setMaxTransactions(max_transactions);
+}
+
+D2UpdateMgr::~D2UpdateMgr() {
+ transaction_list_.clear();
+}
+
+void D2UpdateMgr::sweep() {
+ // cleanup finished transactions;
+ checkFinishedTransactions();
+
+ // if the queue isn't empty, find the next suitable job and
+ // start a transaction for it.
+ // @todo - Do we want to queue max transactions? The logic here will only
+ // start one new transaction per invocation. On the other hand a busy
+ // system will generate many IO events and this method will be called
+ // frequently. It will likely achieve max transactions quickly on its own.
+ if (getQueueCount() > 0) {
+ if (getTransactionCount() >= max_transactions_) {
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL_DATA,
+ DHCP_DDNS_AT_MAX_TRANSACTIONS).arg(getQueueCount())
+ .arg(getMaxTransactions());
+
+ return;
+ }
+
+ // We are not at maximum transactions, so pick and start the next job.
+ pickNextJob();
+ }
+}
+
+void
+D2UpdateMgr::checkFinishedTransactions() {
+ // Cycle through transaction list and do whatever needs to be done
+ // for finished transactions.
+ // At the moment all we do is remove them from the list. This is likely
+ // to expand as DHCP_DDNS matures.
+ // NOTE: One must use postfix increments of the iterator on the calls
+ // to erase. This replaces the old iterator which becomes invalid by the
+ // erase with a the next valid iterator. Prefix incrementing will not
+ // work.
+ TransactionList::iterator it = transaction_list_.begin();
+ while (it != transaction_list_.end()) {
+ NameChangeTransactionPtr trans = (*it).second;
+ if (trans->isModelDone()) {
+ // @todo Addtional actions based on NCR status could be
+ // performed here.
+ transaction_list_.erase(it++);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void D2UpdateMgr::pickNextJob() {
+ // Start at the front of the queue, looking for the first entry for
+ // which no transaction is in progress. If we find an eligible entry
+ // remove it from the queue and make a transaction for it.
+ // Requests and transactions are associated by DHCID. If a request has
+ // the same DHCID as a transaction, they are presumed to be for the same
+ // "end user".
+ size_t queue_count = getQueueCount();
+ for (size_t index = 0; index < queue_count; ++index) {
+ dhcp_ddns::NameChangeRequestPtr found_ncr = queue_mgr_->peekAt(index);
+ if (!hasTransaction(found_ncr->getDhcid())) {
+ queue_mgr_->dequeueAt(index);
+ makeTransaction(found_ncr);
+ return;
+ }
+ }
+
+ // There were no eligible jobs. All of the current DHCIDs already have
+ // transactions pending.
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL_DATA, DHCP_DDNS_NO_ELIGIBLE_JOBS)
+ .arg(getQueueCount()).arg(getTransactionCount());
+}
+
+void
+D2UpdateMgr::makeTransaction(dhcp_ddns::NameChangeRequestPtr& next_ncr) {
+ // First lets ensure there is not a transaction in progress for this
+ // DHCID. (pickNextJob should ensure this, as it is the only real caller
+ // but for safety's sake we'll check).
+ const TransactionKey& key = next_ncr->getDhcid();
+ if (findTransaction(key) != transactionListEnd()) {
+ // This is programmatic error. Caller(s) should be checking this.
+ isc_throw(D2UpdateMgrError, "Transaction already in progress for: "
+ << key.toStr());
+ }
+
+ // If forward change is enabled, match to forward servers.
+ DdnsDomainPtr forward_domain;
+ if (next_ncr->isForwardChange()) {
+ bool matched = cfg_mgr_->matchForward(next_ncr->getFqdn(),
+ forward_domain);
+ // Could not find a match for forward DNS server. Log it and get out.
+ // This has the net affect of dropping the request on the floor.
+ if (!matched) {
+ LOG_ERROR(dctl_logger, DHCP_DDNS_NO_FWD_MATCH_ERROR)
+ .arg(next_ncr->getFqdn());
+ return;
+ }
+ }
+
+ // If reverse change is enabled, match to reverse servers.
+ DdnsDomainPtr reverse_domain;
+ if (next_ncr->isReverseChange()) {
+ bool matched = cfg_mgr_->matchReverse(next_ncr->getIpAddress(),
+ reverse_domain);
+ // Could not find a match for reverse DNS server. Log it and get out.
+ // This has the net affect of dropping the request on the floor.
+ if (!matched) {
+ LOG_ERROR(dctl_logger, DHCP_DDNS_NO_REV_MATCH_ERROR)
+ .arg(next_ncr->getIpAddress());
+ return;
+ }
+ }
+
+ // We matched to the required servers, so construct the transaction.
+ // @todo If multi-threading is implemented, one would pass in an
+ // empty IOServicePtr, rather than our instance value. This would cause
+ // the transaction to instantiate its own, separate IOService to handle
+ // the transaction's IO.
+ NameChangeTransactionPtr trans;
+ if (next_ncr->getChangeType() == dhcp_ddns::CHG_ADD) {
+ trans.reset(new NameAddTransaction(io_service_, next_ncr,
+ forward_domain, reverse_domain));
+ } else {
+ trans.reset(new NameRemoveTransaction(io_service_, next_ncr,
+ forward_domain, reverse_domain));
+ }
+
+ // Add the new transaction to the list.
+ transaction_list_[key] = trans;
+
+ // Start it.
+ trans->startTransaction();
+}
+
+TransactionList::iterator
+D2UpdateMgr::findTransaction(const TransactionKey& key) {
+ return (transaction_list_.find(key));
+}
+
+bool
+D2UpdateMgr::hasTransaction(const TransactionKey& key) {
+ return (findTransaction(key) != transactionListEnd());
+}
+
+void
+D2UpdateMgr::removeTransaction(const TransactionKey& key) {
+ TransactionList::iterator pos = findTransaction(key);
+ if (pos != transactionListEnd()) {
+ transaction_list_.erase(pos);
+ }
+}
+
+TransactionList::iterator
+D2UpdateMgr::transactionListBegin() {
+ return (transaction_list_.begin());
+}
+
+TransactionList::iterator
+D2UpdateMgr::transactionListEnd() {
+ return (transaction_list_.end());
+}
+
+void
+D2UpdateMgr::clearTransactionList() {
+ // @todo for now this just wipes them out. We might need something
+ // more elegant, that allows a cancel first.
+ transaction_list_.clear();
+}
+
+void
+D2UpdateMgr::setMaxTransactions(const size_t new_trans_max) {
+ // Obviously we need at room for at least one transaction.
+ if (new_trans_max < 1) {
+ isc_throw(D2UpdateMgrError, "D2UpdateMgr"
+ " maximum transactions limit must be greater than zero");
+ }
+
+ // Do not allow the list maximum to be set to less then current list size.
+ if (new_trans_max < getTransactionCount()) {
+ isc_throw(D2UpdateMgrError, "D2UpdateMgr maximum transaction limit "
+ "cannot be less than the current transaction count :"
+ << getTransactionCount());
+ }
+
+ max_transactions_ = new_trans_max;
+}
+
+size_t
+D2UpdateMgr::getQueueCount() const {
+ return (queue_mgr_->getQueueSize());
+}
+
+size_t
+D2UpdateMgr::getTransactionCount() const {
+ return (transaction_list_.size());
+}
+
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/d2_update_mgr.h b/src/bin/d2/d2_update_mgr.h
new file mode 100644
index 0000000..c250ef6
--- /dev/null
+++ b/src/bin/d2/d2_update_mgr.h
@@ -0,0 +1,256 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_UPDATE_MGR_H
+#define D2_UPDATE_MGR_H
+
+/// @file d2_update_mgr.h This file defines the class D2UpdateMgr.
+
+#include <exceptions/exceptions.h>
+#include <d2/d2_asio.h>
+#include <d2/d2_log.h>
+#include <d2/d2_queue_mgr.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/nc_trans.h>
+
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <map>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the update manager encounters a general error.
+class D2UpdateMgrError : public isc::Exception {
+public:
+ D2UpdateMgrError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines a list of transactions.
+typedef std::map<TransactionKey, NameChangeTransactionPtr> TransactionList;
+
+/// @brief D2UpdateMgr creates and manages update transactions.
+///
+/// D2UpdateMgr is the DHCP_DDNS task master, instantiating and then supervising
+/// transactions that execute the DNS updates needed to fulfill the requests
+/// (NameChangeRequests) received from DHCP_DDNS clients (e.g. DHCP servers).
+///
+/// D2UpdateMgr uses the services of D2QueueMgr to monitor the queue of
+/// NameChangeRequests and select and dequeue requests for processing.
+/// When request is dequeued for processing it is removed from the queue and
+/// wrapped in NameChangeTransaction and added to the D2UpdateMgr's list of
+/// transactions.
+///
+/// As part of the process of forming transactions, D2UpdateMgr matches each
+/// request with the appropriate list of DNS servers. This matching is based
+/// upon request attributes, primarily the FQDN and update direction (forward
+/// or reverse). D2UpdateMgr uses the services of D2CfgMgr to match requests
+/// to DNS server lists.
+///
+/// Once created, each transaction is responsible for carrying out the steps
+/// required to fulfill its specific request. These steps typically consist of
+/// one or more DNS packet exchanges with the appropriate DNS server. As
+/// transactions complete, D2UpdateMgr removes them from the transaction list,
+/// replacing them with new transactions.
+///
+/// D2UpdateMgr carries out each of the above steps, from with a method called
+/// sweep(). This method is intended to be called as IO events complete.
+/// The upper layer(s) are responsible for calling sweep in a timely and cyclic
+/// manner.
+///
+class D2UpdateMgr : public boost::noncopyable {
+public:
+ /// @brief Maximum number of concurrent transactions
+ /// NOTE that 32 is an arbitrary choice picked for the initial
+ /// implementation.
+ static const size_t MAX_TRANSACTIONS_DEFAULT = 32;
+
+ // @todo This structure is not yet used. It is here in anticipation of
+ // enabled statistics capture.
+ struct Stats {
+ uint64_t start_time_;
+ uint64_t stop_time_;
+ uint64_t update_count_;
+ uint64_t min_update_time_;
+ uint64_t max_update_time_;
+ uint64_t server_rejects_;
+ uint64_t server_timeouts_;
+ };
+
+ /// @brief Constructor
+ ///
+ /// @param queue_mgr reference to the queue manager receiving requests
+ /// @param cfg_mgr reference to the configuration manager
+ /// @param io_service IO service used by the upper layer(s) to manage
+ /// IO events
+ /// @param max_transactions the maximum number of concurrent transactions
+ ///
+ /// @throw D2UpdateMgrError if either the queue manager or configuration
+ /// managers are NULL, or max transactions is less than one.
+ D2UpdateMgr(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
+ IOServicePtr& io_service,
+ const size_t max_transactions = MAX_TRANSACTIONS_DEFAULT);
+
+ /// @brief Destructor
+ virtual ~D2UpdateMgr();
+
+ /// @brief Check current transactions; start transactions for new requests.
+ ///
+ /// This method is the primary public interface used by the upper layer. It
+ /// should be called as IO events complete. During each invocation it does
+ /// the following:
+ ///
+ /// - Removes all completed transactions from the transaction list.
+ ///
+ /// - If the request queue is not empty and the number of transactions
+ /// in the transaction list has not reached maximum allowed, then select
+ /// a request from the queue.
+ ///
+ /// - If a request was selected, start a new transaction for it and
+ /// add the transaction to the list of transactions.
+ void sweep();
+
+protected:
+ /// @brief Performs post-completion cleanup on completed transactions.
+ ///
+ /// Iterates through the list of transactions and removes any that have
+ /// reached completion. This method may expand in complexity or even
+ /// disappear altogether as the implementation matures.
+ void checkFinishedTransactions();
+
+ /// @brief Starts a transaction for the next eligible request in the queue.
+ ///
+ /// This method will scan the request queue for the next request to
+ /// dequeue. The current implementation starts at the front of the queue
+ /// and looks for the first request for whose DHCID there is no current
+ /// transaction in progress.
+ ///
+ /// If a request is selected, it is removed from the queue and transaction
+ /// is constructed for it.
+ ///
+ /// It is possible that no such request exists, though this is likely to be
+ /// rather rare unless a system is frequently seeing requests for the same
+ /// clients in quick succession.
+ void pickNextJob();
+
+ /// @brief Create a new transaction for the given request.
+ ///
+ /// This method will attempt to match the request to a list of configured
+ /// DNS servers. If a list of servers is found, it will instantiate a
+ /// transaction for it and add the transaction to the transaction list.
+ ///
+ /// If no servers are found that match the request, this constitutes a
+ /// configuration error. The error will be logged and the request will
+ /// be discarded.
+ ///
+ /// @param ncr the NameChangeRequest for which to create a transaction.
+ ///
+ /// @throw D2UpdateMgrError if a transaction for this DHCID already
+ /// exists. Note this would be programmatic error.
+ void makeTransaction(isc::dhcp_ddns::NameChangeRequestPtr& ncr);
+
+public:
+ /// @brief Gets the UpdateMgr's IOService.
+ ///
+ /// @return returns a reference to the IOService
+ const IOServicePtr& getIOService() {
+ return (io_service_);
+ }
+
+ /// @brief Returns the maximum number of concurrent transactions.
+ size_t getMaxTransactions() const {
+ return (max_transactions_);
+ }
+
+ /// @brief Sets the maximum number of entries allowed in the queue.
+ ///
+ /// @param max_transactions is the new maximum number of transactions
+ ///
+ /// @throw Throws D2QueueMgrError if the new value is less than one or if
+ /// the new value is less than the number of entries currently in the
+ /// queue.
+ void setMaxTransactions(const size_t max_transactions);
+
+ /// @brief Search the transaction list for the given key.
+ ///
+ /// @param key the transaction key value for which to search.
+ ///
+ /// @return Iterator pointing to the entry found. If no entry is
+ /// it will point to the list end position.
+ TransactionList::iterator findTransaction(const TransactionKey& key);
+
+ /// @brief Returns the transaction list end position.
+ TransactionList::iterator transactionListEnd();
+
+ /// @brief Returns the transaction list beg position.
+ TransactionList::iterator transactionListBegin();
+
+ /// @brief Convenience method that checks transaction list for the given key
+ ///
+ /// @param key the transaction key value for which to search.
+ ///
+ /// @return Returns true if the key is found within the list, false
+ /// otherwise.
+ bool hasTransaction(const TransactionKey& key);
+
+ /// @brief Removes the entry pointed to by key from the transaction list.
+ ///
+ /// Removes the entry referred to by key if it exists. It has no effect
+ /// if the entry is not found.
+ ///
+ /// @param key of the transaction to remove
+ void removeTransaction(const TransactionKey& key);
+
+ /// @brief Immediately discards all entries in the transaction list.
+ ///
+ /// @todo For now this just wipes them out. We might need something
+ /// more elegant, that allows a cancel first.
+ void clearTransactionList();
+
+ /// @brief Convenience method that returns the number of requests queued.
+ size_t getQueueCount() const;
+
+ /// @brief Returns the current number of transactions.
+ size_t getTransactionCount() const;
+
+private:
+ /// @brief Pointer to the queue manager.
+ D2QueueMgrPtr queue_mgr_;
+
+ /// @brief Pointer to the configuration manager.
+ D2CfgMgrPtr cfg_mgr_;
+
+ /// @brief Primary IOService instance.
+ /// This is the IOService that the upper layer(s) use for IO events, such
+ /// as shutdown and configuration commands. It is the IOService that is
+ /// passed into transactions to manager their IO events.
+ /// (For future reference, multi-threaded transactions would each use their
+ /// own IOService instance.)
+ IOServicePtr io_service_;
+
+ /// @brief Maximum number of concurrent transactions.
+ size_t max_transactions_;
+
+ /// @brief List of transactions.
+ TransactionList transaction_list_;
+};
+
+/// @brief Defines a pointer to a D2UpdateMgr instance.
+typedef boost::shared_ptr<D2UpdateMgr> D2UpdateMgrPtr;
+
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/d2_zone.cc b/src/bin/d2/d2_zone.cc
new file mode 100644
index 0000000..96aa2bb
--- /dev/null
+++ b/src/bin/d2/d2_zone.cc
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_zone.h>
+
+namespace isc {
+namespace d2 {
+
+D2Zone::D2Zone(const dns::Name& name, const dns::RRClass& rrclass)
+ : name_(name), rrclass_(rrclass) {
+}
+
+std::string D2Zone::toText() const {
+ return (name_.toText() + " " + rrclass_.toText() + " SOA\n");
+}
+
+std::ostream&
+operator<<(std::ostream& os, const D2Zone& zone) {
+ os << zone.toText();
+ return (os);
+}
+
+} // namespace d2
+} // namespace isc
+
diff --git a/src/bin/d2/d2_zone.h b/src/bin/d2/d2_zone.h
new file mode 100644
index 0000000..60d43c8
--- /dev/null
+++ b/src/bin/d2/d2_zone.h
@@ -0,0 +1,117 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_ZONE_H
+#define D2_ZONE_H
+
+#include <dns/name.h>
+#include <dns/rrclass.h>
+
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace d2 {
+
+/// @brief The @c D2Zone encapsulates the Zone section in DNS Update message.
+///
+/// This class is used by the @c D2UpdateMessage to encapsulate the Zone section
+/// of the DNS Update message. Class members hold corresponding values of
+/// section's fields: NAME, CLASS. This class does not hold the RTYPE field
+/// value because RTYPE is always equal to SOA for DNS Update message (see
+/// RFC 2136, section 2.3).
+///
+/// Note, that this @c D2Zone class neither exposes functions to decode messages
+/// from wire format nor to encode to wire format. This is not needed, because
+/// @c isc::d2::D2UpdateMessage class uses @c D2Zone only to return the parsed
+/// Zone information to the caller. Internally, D2UpdateMessage parses and
+/// stores Zone section using @c isc::dns::Question class, and the @c toWire
+/// and @c fromWire functions of the @c isc::dns::Question class are used.
+class D2Zone {
+public:
+ /// @brief Constructor from Name and RRClass.
+ ///
+ /// @param name The name of the Zone.
+ /// @param rrclass The RR class of the Zone.
+ D2Zone(const dns::Name& name, const dns::RRClass& rrclass);
+
+ ///
+ /// @name Getters
+ ///
+ //@{
+ /// @brief Returns the Zone name.
+ ///
+ /// @return A reference to the Zone name.
+ const dns::Name& getName() const { return (name_); }
+
+ /// @brief Returns the Zone class.
+ ///
+ /// @return A reference to the Zone class.
+ const dns::RRClass& getClass() const { return (rrclass_); }
+ //@}
+
+ /// @brief Returns text representation of the Zone.
+ ///
+ /// This function concatenates the name of the Zone, Class and Type.
+ /// The type is always SOA.
+ ///
+ /// @return A text representation of the Zone.
+ std::string toText() const;
+
+ ///
+ /// @name Comparison Operators
+ ///
+ //@{
+ /// @brief Equality operator to compare @c D2Zone objects in query and
+ /// response messages.
+ ///
+ /// @param rhs Zone to compare against.
+ ///
+ /// @return true if name and class are equal, false otherwise.
+ bool operator==(const D2Zone& rhs) const {
+ return ((rrclass_ == rhs.rrclass_) && (name_ == rhs.name_));
+ }
+
+ /// @brief Inequality operator to compare @c D2Zone objects in query and
+ /// response messages.
+ ///
+ /// @param rhs Zone to compare against.
+ ///
+ /// @return true if any of name or class are unequal, false otherwise.
+ bool operator!=(const D2Zone& rhs) const {
+ return (!operator==(rhs));
+ }
+ //@}
+
+private:
+ dns::Name name_; ///< Holds the Zone name.
+ dns::RRClass rrclass_; ///< Holds the Zone class.
+};
+
+typedef boost::shared_ptr<D2Zone> D2ZonePtr;
+
+/// @brief Insert the @c D2Zone as a string into stream.
+///
+/// @param os A @c std::ostream object on which the insertion operation is
+/// performed.
+/// @param zone A reference to the @c D2Zone object output by the
+/// operation.
+///
+/// @return A reference to the same @c std::ostream object referenced by
+/// parameter @c os after the insertion operation.
+std::ostream& operator<<(std::ostream& os, const D2Zone& zone);
+
+} // namespace d2
+} // namespace isc
+
+#endif // D2_ZONE_H
diff --git a/src/bin/d2/d_cfg_mgr.cc b/src/bin/d2/d_cfg_mgr.cc
new file mode 100644
index 0000000..1e6bb57
--- /dev/null
+++ b/src/bin/d2/d_cfg_mgr.cc
@@ -0,0 +1,240 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/ccsession.h>
+#include <d2/d2_log.h>
+#include <dhcp/libdhcp++.h>
+#include <d2/d_cfg_mgr.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <util/encode/hex.h>
+#include <util/strutil.h>
+
+#include <boost/foreach.hpp>
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string.hpp>
+
+#include <limits>
+#include <iostream>
+#include <vector>
+#include <map>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::data;
+using namespace isc::asiolink;
+
+namespace isc {
+namespace d2 {
+
+// *********************** DCfgContextBase *************************
+
+DCfgContextBase::DCfgContextBase():
+ boolean_values_(new BooleanStorage()),
+ uint32_values_(new Uint32Storage()),
+ string_values_(new StringStorage()) {
+ }
+
+DCfgContextBase::DCfgContextBase(const DCfgContextBase& rhs):
+ boolean_values_(new BooleanStorage(*(rhs.boolean_values_))),
+ uint32_values_(new Uint32Storage(*(rhs.uint32_values_))),
+ string_values_(new StringStorage(*(rhs.string_values_))) {
+}
+
+void
+DCfgContextBase::getParam(const std::string& name, bool& value, bool optional) {
+ try {
+ value = boolean_values_->getParam(name);
+ } catch (DhcpConfigError& ex) {
+ // If the parameter is not optional, re-throw the exception.
+ if (!optional) {
+ throw;
+ }
+ }
+}
+
+
+void
+DCfgContextBase::getParam(const std::string& name, uint32_t& value,
+ bool optional) {
+ try {
+ value = uint32_values_->getParam(name);
+ } catch (DhcpConfigError& ex) {
+ // If the parameter is not optional, re-throw the exception.
+ if (!optional) {
+ throw;
+ }
+ }
+}
+
+void
+DCfgContextBase::getParam(const std::string& name, std::string& value,
+ bool optional) {
+ try {
+ value = string_values_->getParam(name);
+ } catch (DhcpConfigError& ex) {
+ // If the parameter is not optional, re-throw the exception.
+ if (!optional) {
+ throw;
+ }
+ }
+}
+
+DCfgContextBase::~DCfgContextBase() {
+}
+
+// *********************** DCfgMgrBase *************************
+
+DCfgMgrBase::DCfgMgrBase(DCfgContextBasePtr context)
+ : parse_order_(), context_(context) {
+ if (!context_) {
+ isc_throw(DCfgMgrBaseError, "DCfgMgrBase ctor: context cannot be NULL");
+ }
+}
+
+DCfgMgrBase::~DCfgMgrBase() {
+}
+
+isc::data::ConstElementPtr
+DCfgMgrBase::parseConfig(isc::data::ConstElementPtr config_set) {
+ LOG_DEBUG(dctl_logger, DBGLVL_COMMAND,
+ DCTL_CONFIG_START).arg(config_set->str());
+
+ if (!config_set) {
+ return (isc::config::createAnswer(1,
+ std::string("Can't parse NULL config")));
+ }
+
+ // The parsers implement data inheritance by directly accessing
+ // configuration context. For this reason the data parsers must store
+ // the parsed data into context immediately. This may cause data
+ // inconsistency if the parsing operation fails after the context has been
+ // modified. We need to preserve the original context here
+ // so as we can rollback changes when an error occurs.
+ DCfgContextBasePtr original_context = context_->clone();
+
+ // Answer will hold the result returned to the caller.
+ ConstElementPtr answer;
+
+ // Holds the name of the element being parsed.
+ std::string element_id;
+
+ try {
+ // Grab a map of element_ids and their data values from the new
+ // configuration set.
+ const std::map<std::string, ConstElementPtr>& values_map =
+ config_set->mapValue();
+
+ // Use a pre-ordered list of element ids to parse the elements in a
+ // specific order if the list (parser_order_) is not empty; otherwise
+ // elements are parsed in the order the value_map presents them.
+
+ if (!parse_order_.empty()) {
+ // For each element_id in the parse order list, look for it in the
+ // value map. If the element exists in the map, pass it and it's
+ // associated data in for parsing.
+ // If there is no matching entry in the value map an error is
+ // thrown. Note, that elements tagged as "optional" from the user
+ // perspective must still have default or empty entries in the
+ // configuration set to be parsed.
+ int parsed_count = 0;
+ std::map<std::string, ConstElementPtr>::const_iterator it;
+ BOOST_FOREACH(element_id, parse_order_) {
+ it = values_map.find(element_id);
+ if (it != values_map.end()) {
+ ++parsed_count;
+ buildAndCommit(element_id, it->second);
+ }
+ else {
+ LOG_ERROR(dctl_logger, DCTL_ORDER_NO_ELEMENT)
+ .arg(element_id);
+ isc_throw(DCfgMgrBaseError, "Element:" << element_id <<
+ " is listed in the parse order but is not "
+ " present in the configuration");
+ }
+ }
+
+ // NOTE: When using ordered parsing, the parse order list MUST
+ // include every possible element id that the value_map may contain.
+ // Entries in the map that are not in the parse order, would not be
+ // parsed. For now we will flag this as a programmatic error. One
+ // could attempt to adjust for this, by identifying such entries
+ // and parsing them either first or last but which would be correct?
+ // Better to hold the engineer accountable. So, if we parsed none
+ // or we parsed fewer than are in the map; then either the parse i
+ // order is incomplete OR the map has unsupported values.
+ if (!parsed_count ||
+ (parsed_count && ((parsed_count + 1) < values_map.size()))) {
+ LOG_ERROR(dctl_logger, DCTL_ORDER_ERROR);
+ isc_throw(DCfgMgrBaseError,
+ "Configuration contains elements not in parse order");
+ }
+ } else {
+ // Order doesn't matter so iterate over the value map directly.
+ // Pass each element and it's associated data in to be parsed.
+ ConfigPair config_pair;
+ BOOST_FOREACH(config_pair, values_map) {
+ element_id = config_pair.first;
+ buildAndCommit(element_id, config_pair.second);
+ }
+ }
+
+ // Everything was fine. Configuration set processed successfully.
+ LOG_INFO(dctl_logger, DCTL_CONFIG_COMPLETE).arg("");
+ answer = isc::config::createAnswer(0, "Configuration committed.");
+
+ } catch (const isc::Exception& ex) {
+ LOG_ERROR(dctl_logger, DCTL_PARSER_FAIL).arg(element_id).arg(ex.what());
+ answer = isc::config::createAnswer(1,
+ string("Configuration parsing failed: ") + ex.what());
+
+ // An error occurred, so make sure that we restore original context.
+ context_ = original_context;
+ return (answer);
+ }
+
+ return (answer);
+}
+
+void DCfgMgrBase::buildAndCommit(std::string& element_id,
+ isc::data::ConstElementPtr value) {
+ // Call derivation's implementation to create the appropriate parser
+ // based on the element id.
+ ParserPtr parser = createConfigParser(element_id);
+ if (!parser) {
+ isc_throw(DCfgMgrBaseError, "Could not create parser");
+ }
+
+ try {
+ // Invoke the parser's build method passing in the value. This will
+ // "convert" the Element form of value into the actual data item(s)
+ // and store them in parser's local storage.
+ parser->build(value);
+
+ // Invoke the parser's commit method. This "writes" the the data
+ // item(s) stored locally by the parser into the context. (Note that
+ // parsers are free to do more than update the context, but that is an
+ // nothing something we are concerned with here.)
+ parser->commit();
+ } catch (const isc::Exception& ex) {
+ isc_throw(DCfgMgrBaseError,
+ "Could not build and commit: " << ex.what());
+ } catch (...) {
+ isc_throw(DCfgMgrBaseError, "Non-ISC exception occurred");
+ }
+}
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
diff --git a/src/bin/d2/d_cfg_mgr.h b/src/bin/d2/d_cfg_mgr.h
new file mode 100644
index 0000000..a0bd9bb
--- /dev/null
+++ b/src/bin/d2/d_cfg_mgr.h
@@ -0,0 +1,331 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D_CFG_MGR_H
+#define D_CFG_MGR_H
+
+#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <dhcpsrv/dhcp_parsers.h>
+
+#include <stdint.h>
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Exception thrown if the configuration manager encounters an error.
+class DCfgMgrBaseError : public isc::Exception {
+public:
+ DCfgMgrBaseError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+class DCfgContextBase;
+/// @brief Pointer to a configuration context.
+typedef boost::shared_ptr<DCfgContextBase> DCfgContextBasePtr;
+
+/// @brief Abstract class that implements a container for configuration context.
+/// It provides a single enclosure for the storage of configuration parameters
+/// and any other context specific information that needs to be accessible
+/// during configuration parsing as well as to the application as a whole.
+/// The base class supports storage for a small set of simple data types.
+/// Derivations simply add additional storage as needed. Note that this class
+/// declares the pure virtual clone() method, its copy constructor is protected,
+/// and its copy operator is inaccessible. Derivations must supply an
+/// implementation of clone that calls the base class copy constructor.
+/// This allows the management class to perform context backup and restoration
+/// without derivation specific knowledge using logic like
+/// the following:
+///
+/// // Make a backup copy
+/// DCfgContextBasePtr backup_copy(context_->clone());
+/// :
+/// // Restore from backup
+/// context_ = backup_copy;
+///
+class DCfgContextBase {
+public:
+ /// @brief Indicator that a configuration parameter is optional.
+ static const bool OPTIONAL = true;
+ static const bool REQUIRED = false;
+
+ /// @brief Constructor
+ DCfgContextBase();
+
+ /// @brief Destructor
+ virtual ~DCfgContextBase();
+
+ /// @brief Fetches the value for a given boolean configuration parameter
+ /// from the context.
+ ///
+ /// @param name is the name of the parameter to retrieve.
+ /// @param value is an output parameter in which to return the retrieved
+ /// value.
+ /// @param optional if true, the parameter is optional and the method
+ /// will not throw if the parameter is not found in the context. The
+ /// contents of the output parameter, value, will not be altered.
+ /// It defaults to false if not specified.
+ /// @throw throws DhcpConfigError if the context does not contain the
+ /// parameter and optional is false.
+ void getParam(const std::string& name, bool& value, bool optional=false);
+
+ /// @brief Fetches the value for a given uint32_t configuration parameter
+ /// from the context.
+ ///
+ /// @param name is the name of the parameter to retrieve.
+ /// @param value is an output parameter in which to return the retrieved
+ /// value.
+ /// @param optional if true, the parameter is optional and the method
+ /// will not throw if the parameter is not found in the context. The
+ /// contents of the output parameter, value, will not be altered.
+ /// @throw throws DhcpConfigError if the context does not contain the
+ /// parameter and optional is false.
+ void getParam(const std::string& name, uint32_t& value,
+ bool optional=false);
+
+ /// @brief Fetches the value for a given string configuration parameter
+ /// from the context.
+ ///
+ /// @param name is the name of the parameter to retrieve.
+ /// @param value is an output parameter in which to return the retrieved
+ /// value.
+ /// @param optional if true, the parameter is optional and the method
+ /// will not throw if the parameter is not found in the context. The
+ /// contents of the output parameter, value, will not be altered.
+ /// @throw throws DhcpConfigError if the context does not contain the
+ /// parameter and optional is false.
+ void getParam(const std::string& name, std::string& value,
+ bool optional=false);
+
+ /// @brief Fetches the Boolean Storage. Typically used for passing
+ /// into parsers.
+ ///
+ /// @return returns a pointer to the Boolean Storage.
+ isc::dhcp::BooleanStoragePtr getBooleanStorage() {
+ return (boolean_values_);
+ }
+
+ /// @brief Fetches the uint32 Storage. Typically used for passing
+ /// into parsers.
+ ///
+ /// @return returns a pointer to the uint32 Storage.
+ isc::dhcp::Uint32StoragePtr getUint32Storage() {
+ return (uint32_values_);
+ }
+
+ /// @brief Fetches the string Storage. Typically used for passing
+ /// into parsers.
+ ///
+ /// @return returns a pointer to the string Storage.
+ isc::dhcp::StringStoragePtr getStringStorage() {
+ return (string_values_);
+ }
+
+ /// @brief Creates a clone of this context object.
+ ///
+ /// As mentioned in the the class brief, derivation must supply an
+ /// implementation that initializes the base class storage as well as its
+ /// own. Typically the derivation's clone method would return the result
+ /// of passing "*this" into its own copy constructor:
+ ///
+ /// @code
+ /// class DStubContext : public DCfgContextBase {
+ /// public:
+ /// :
+ /// // Clone calls its own copy constructor
+ /// virtual DCfgContextBasePtr clone() {
+ /// return (DCfgContextBasePtr(new DStubContext(*this)));
+ /// }
+ ///
+ /// // Note that the copy constructor calls the base class copy ctor
+ /// // then initializes its additional storage.
+ /// DStubContext(const DStubContext& rhs) : DCfgContextBase(rhs),
+ /// extra_values_(new Uint32Storage(*(rhs.extra_values_))) {
+ /// }
+ /// :
+ /// // Here's the derivation's additional storage.
+ /// isc::dhcp::Uint32StoragePtr extra_values_;
+ /// :
+ /// @endcode
+ ///
+ /// @return returns a pointer to the new clone.
+ virtual DCfgContextBasePtr clone() = 0;
+
+protected:
+ /// @brief Copy constructor for use by derivations in clone().
+ DCfgContextBase(const DCfgContextBase& rhs);
+
+private:
+ /// @brief Private assignment operator to avoid potential for slicing.
+ DCfgContextBase& operator=(const DCfgContextBase& rhs);
+
+ /// @brief Storage for boolean parameters.
+ isc::dhcp::BooleanStoragePtr boolean_values_;
+
+ /// @brief Storage for uint32 parameters.
+ isc::dhcp::Uint32StoragePtr uint32_values_;
+
+ /// @brief Storage for string parameters.
+ isc::dhcp::StringStoragePtr string_values_;
+};
+
+/// @brief Defines an unsorted, list of string Element IDs.
+typedef std::vector<std::string> ElementIdList;
+
+/// @brief Configuration Manager
+///
+/// DCfgMgrBase is an abstract class that provides the mechanisms for managing
+/// an application's configuration. This includes services for parsing sets of
+/// configuration values, storing the parsed information in its converted form,
+/// and retrieving the information on demand. It is intended to be the worker
+/// class which is handed a set of configuration values to process by upper
+/// application management layers.
+///
+/// The class presents a public method for receiving new configurations,
+/// parseConfig. This method coordinates the parsing effort as follows:
+///
+/// @code
+/// make backup copy of configuration context
+/// for each top level element in new configuration
+/// get derivation-specific parser for element
+/// run parser
+/// update context with parsed results
+/// break on error
+///
+/// if an error occurred
+/// restore configuration context from backup
+/// @endcode
+///
+/// After making a backup of the current context, it iterates over the top-level
+/// elements in the new configuration. The order in which the elements are
+/// processed is either:
+///
+/// 1. Natural order presented by the configuration set
+/// 2. Specific order determined by a list of element ids
+///
+/// This allows a derivation to specify the order in which its elements are
+/// parsed if there are dependencies between elements.
+///
+/// To parse a given element, its id is passed into createConfigParser,
+/// which returns an instance of the appropriate parser. This method is
+/// abstract so the derivation's implementation determines the type of parser
+/// created. This isolates the knowledge of specific element ids and which
+/// application specific parsers to derivation.
+///
+/// Once the parser has been created, it is used to parse the data value
+/// associated with the element id and update the context with the parsed
+/// results.
+///
+/// In the event that an error occurs, parsing is halted and the
+/// configuration context is restored from backup.
+class DCfgMgrBase {
+public:
+ /// @brief Constructor
+ ///
+ /// @param context is a pointer to the configuration context the manager
+ /// will use for storing parsed results.
+ ///
+ /// @throw throws DCfgMgrBaseError if context is null
+ DCfgMgrBase(DCfgContextBasePtr context);
+
+ /// @brief Destructor
+ virtual ~DCfgMgrBase();
+
+ /// @brief Acts as the receiver of new configurations and coordinates
+ /// the parsing as described in the class brief.
+ ///
+ /// @param config_set is a set of configuration elements to parsed.
+ ///
+ /// @return an Element that contains the results of configuration composed
+ /// of an integer status value (0 means successful, non-zero means failure),
+ /// and a string explanation of the outcome.
+ isc::data::ConstElementPtr parseConfig(isc::data::ConstElementPtr
+ config_set);
+
+ /// @brief Adds a given element id to the end of the parse order list.
+ ///
+ /// The order in which elements are retrieved from this is the order in
+ /// which they are added to the list. Derivations should use this method
+ /// to populate the parse order as part of their constructor.
+ ///
+ /// @param element_id is the string name of the element as it will appear
+ /// in the configuration set.
+ void addToParseOrder(const std::string& element_id){
+ parse_order_.push_back(element_id);
+ }
+
+ /// @brief Fetches the parse order list.
+ ///
+ /// @return returns a const reference to the list.
+ const ElementIdList& getParseOrder() const {
+ return (parse_order_);
+ }
+
+ /// @brief Fetches the configuration context.
+ ///
+ /// @return returns a pointer reference to the configuration context.
+ DCfgContextBasePtr& getContext() {
+ return (context_);
+ }
+
+protected:
+ /// @brief Create a parser instance based on an element id.
+ ///
+ /// Given an element_id returns an instance of the appropriate parser.
+ /// This method is abstract, isolating any direct knowledge of element_ids
+ /// and parsers to within the application-specific derivation.
+ ///
+ /// @param element_id is the string name of the element as it will appear
+ /// in the configuration set.
+ ///
+ /// @return returns a ParserPtr to the parser instance.
+ /// @throw throws DCfgMgrBaseError if an error occurs.
+ virtual isc::dhcp::ParserPtr
+ createConfigParser(const std::string& element_id) = 0;
+
+private:
+
+ /// @brief Parse a configuration element.
+ ///
+ /// Given an element_id and data value, instantiate the appropriate
+ /// parser, parse the data value, and commit the results.
+ ///
+ /// @param element_id is the string name of the element as it will appear
+ /// in the configuration set.
+ /// @param value is the data value to be parsed and associated with
+ /// element_id.
+ ///
+ /// @throw throws DCfgMgrBaseError if an error occurs.
+ void buildAndCommit(std::string& element_id,
+ isc::data::ConstElementPtr value);
+
+ /// @brief A list of element ids which specifies the element parsing order.
+ ///
+ /// If the list is empty, the natural order in the configuration set
+ /// it used.
+ ElementIdList parse_order_;
+
+ /// @brief Pointer to the configuration context instance.
+ DCfgContextBasePtr context_;
+};
+
+/// @brief Defines a shared pointer to DCfgMgrBase.
+typedef boost::shared_ptr<DCfgMgrBase> DCfgMgrBasePtr;
+
+
+}; // end of isc::d2 namespace
+}; // end of isc namespace
+
+#endif // D_CFG_MGR_H
diff --git a/src/bin/d2/d_controller.cc b/src/bin/d2/d_controller.cc
new file mode 100644
index 0000000..fe39dd0
--- /dev/null
+++ b/src/bin/d2/d_controller.cc
@@ -0,0 +1,423 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+
+#include <d2/d2_log.h>
+#include <d2/d_controller.h>
+#include <exceptions/exceptions.h>
+#include <log/logger_support.h>
+
+#include <sstream>
+
+namespace isc {
+namespace d2 {
+
+DControllerBasePtr DControllerBase::controller_;
+
+// Note that the constructor instantiates the controller's primary IOService.
+DControllerBase::DControllerBase(const char* app_name, const char* bin_name)
+ : app_name_(app_name), bin_name_(bin_name), stand_alone_(false),
+ verbose_(false), spec_file_name_(""),
+ io_service_(new isc::asiolink::IOService()){
+}
+
+
+void
+DControllerBase::setController(const DControllerBasePtr& controller) {
+ if (controller_) {
+ // This shouldn't happen, but let's make sure it can't be done.
+ // It represents a programmatic error.
+ isc_throw (DControllerBaseError,
+ "Multiple controller instances attempted.");
+ }
+
+ controller_ = controller;
+}
+
+void
+DControllerBase::launch(int argc, char* argv[], const bool test_mode) {
+ // Step 1 is to parse the command line arguments.
+ try {
+ parseArgs(argc, argv);
+ } catch (const InvalidUsage& ex) {
+ usage(ex.what());
+ throw; // rethrow it
+ }
+
+ // Do not initialize logger here if we are running unit tests. It would
+ // replace an instance of unit test specific logger.
+ if (!test_mode) {
+ // Now that we know what the mode flags are, we can init logging.
+ // If standalone is enabled, do not buffer initial log messages
+ isc::log::initLogger(bin_name_,
+ ((verbose_ && stand_alone_)
+ ? isc::log::DEBUG : isc::log::INFO),
+ isc::log::MAX_DEBUG_LEVEL, NULL, !stand_alone_);
+ }
+
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_STARTING)
+ .arg(app_name_).arg(getpid());
+ try {
+ // Step 2 is to create and initialize the application process object.
+ initProcess();
+ } catch (const std::exception& ex) {
+ LOG_FATAL(dctl_logger, DCTL_INIT_PROCESS_FAIL)
+ .arg(app_name_).arg(ex.what());
+ isc_throw (ProcessInitError,
+ "Application Process initialization failed: " << ex.what());
+ }
+
+ // Next we connect if we are running integrated.
+ if (stand_alone_) {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_STANDALONE)
+ .arg(app_name_);
+ } else {
+ try {
+ establishSession();
+ } catch (const std::exception& ex) {
+ LOG_FATAL(dctl_logger, DCTL_SESSION_FAIL).arg(ex.what());
+ isc_throw (SessionStartError,
+ "Session start up failed: " << ex.what());
+ }
+ }
+
+ // Everything is clear for launch, so start the application's
+ // event loop.
+ try {
+ runProcess();
+ } catch (const std::exception& ex) {
+ LOG_FATAL(dctl_logger, DCTL_PROCESS_FAILED)
+ .arg(app_name_).arg(ex.what());
+ isc_throw (ProcessRunError,
+ "Application process event loop failed: " << ex.what());
+ }
+
+ // If running integrated, disconnect.
+ if (!stand_alone_) {
+ try {
+ disconnectSession();
+ } catch (const std::exception& ex) {
+ LOG_ERROR(dctl_logger, DCTL_DISCONNECT_FAIL)
+ .arg(app_name_).arg(ex.what());
+ isc_throw (SessionEndError, "Session end failed: " << ex.what());
+ }
+ }
+
+ // All done, so bail out.
+ LOG_INFO(dctl_logger, DCTL_STOPPING).arg(app_name_);
+}
+
+
+void
+DControllerBase::parseArgs(int argc, char* argv[])
+{
+ // Iterate over the given command line options. If its a stock option
+ // ("s" or "v") handle it here. If its a valid custom option, then
+ // invoke customOption.
+ int ch;
+ opterr = 0;
+ optind = 1;
+ std::string opts(":vs" + getCustomOpts());
+ while ((ch = getopt(argc, argv, opts.c_str())) != -1) {
+ switch (ch) {
+ case 'v':
+ // Enables verbose logging.
+ verbose_ = true;
+ break;
+
+ case 's':
+ // Enables stand alone or "BINDLESS" operation.
+ stand_alone_ = true;
+ break;
+
+ case '?': {
+ // We hit an invalid option.
+ isc_throw(InvalidUsage, "unsupported option: ["
+ << static_cast<char>(optopt) << "] "
+ << (!optarg ? "" : optarg));
+
+ break;
+ }
+
+ default:
+ // We hit a valid custom option
+ if (!customOption(ch, optarg)) {
+ // This would be a programmatic error.
+ isc_throw(InvalidUsage, " Option listed but implemented?: ["
+ << static_cast<char>(ch) << "] "
+ << (!optarg ? "" : optarg));
+ }
+ break;
+ }
+ }
+
+ // There was too much information on the command line.
+ if (argc > optind) {
+ isc_throw(InvalidUsage, "extraneous command line information");
+ }
+}
+
+bool
+DControllerBase::customOption(int /* option */, char* /*optarg*/)
+{
+ // Default implementation returns false.
+ return (false);
+}
+
+void
+DControllerBase::initProcess() {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_INIT_PROCESS).arg(app_name_);
+
+ // Invoke virtual method to instantiate the application process.
+ try {
+ process_.reset(createProcess());
+ } catch (const std::exception& ex) {
+ isc_throw(DControllerBaseError, std::string("createProcess failed: ")
+ + ex.what());
+ }
+
+ // This is pretty unlikely, but will test for it just to be safe..
+ if (!process_) {
+ isc_throw(DControllerBaseError, "createProcess returned NULL");
+ }
+
+ // Invoke application's init method (Note this call should throw
+ // DProcessBaseError if it fails).
+ process_->init();
+}
+
+void
+DControllerBase::establishSession() {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_CCSESSION_STARTING)
+ .arg(app_name_).arg(spec_file_name_);
+
+ // Create the BIND10 command control session with the our IOService.
+ cc_session_ = SessionPtr(new isc::cc::Session(
+ io_service_->get_io_service()));
+
+ // Create the BIND10 config session with the stub configuration handler.
+ // This handler is internally invoked by the constructor and on success
+ // the constructor updates the current session with the configuration that
+ // had been committed in the previous session. If we do not install
+ // the dummy handler, the previous configuration would be lost.
+ config_session_ = ModuleCCSessionPtr(new isc::config::ModuleCCSession(
+ spec_file_name_, *cc_session_,
+ dummyConfigHandler, commandHandler,
+ false));
+ // Enable configuration even processing.
+ config_session_->start();
+
+ // We initially create ModuleCCSession() with a dummy configHandler, as
+ // the session module is too eager to send partial configuration.
+ // Replace the dummy config handler with the real handler.
+ config_session_->setConfigHandler(configHandler);
+
+ // Call the real configHandler with the full configuration retrieved
+ // from the config session.
+ isc::data::ConstElementPtr answer = configHandler(
+ config_session_->getFullConfig());
+
+ // Parse the answer returned from the configHandler. Log the error but
+ // keep running. This provides an opportunity for the user to correct
+ // the configuration dynamically.
+ int ret = 0;
+ isc::data::ConstElementPtr comment = isc::config::parseAnswer(ret, answer);
+ if (ret) {
+ LOG_ERROR(dctl_logger, DCTL_CONFIG_LOAD_FAIL)
+ .arg(app_name_).arg(comment->str());
+ }
+
+ // Lastly, call onConnect. This allows deriving class to execute custom
+ // logic predicated by session connect.
+ onSessionConnect();
+}
+
+void
+DControllerBase::runProcess() {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_RUN_PROCESS).arg(app_name_);
+ if (!process_) {
+ // This should not be possible.
+ isc_throw(DControllerBaseError, "Process not initialized");
+ }
+
+ // Invoke the application process's run method. This may throw
+ // DProcessBaseError
+ process_->run();
+}
+
+void DControllerBase::disconnectSession() {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_CCSESSION_ENDING)
+ .arg(app_name_);
+
+ // Call virtual onDisconnect. Allows deriving class to execute custom
+ // logic prior to session loss.
+ onSessionDisconnect();
+
+ // Destroy the BIND10 config session.
+ if (config_session_) {
+ config_session_.reset();
+ }
+
+ // Destroy the BIND10 command and control session.
+ if (cc_session_) {
+ cc_session_->disconnect();
+ cc_session_.reset();
+ }
+}
+
+isc::data::ConstElementPtr
+DControllerBase::dummyConfigHandler(isc::data::ConstElementPtr) {
+ LOG_DEBUG(dctl_logger, DBGLVL_START_SHUT, DCTL_CONFIG_STUB)
+ .arg(controller_->getAppName());
+ return (isc::config::createAnswer(0, "Configuration accepted."));
+}
+
+isc::data::ConstElementPtr
+DControllerBase::configHandler(isc::data::ConstElementPtr new_config) {
+
+ LOG_DEBUG(dctl_logger, DBGLVL_COMMAND, DCTL_CONFIG_UPDATE)
+ .arg(controller_->getAppName()).arg(new_config->str());
+
+ // Invoke the instance method on the controller singleton.
+ return (controller_->updateConfig(new_config));
+}
+
+// Static callback which invokes non-static handler on singleton
+isc::data::ConstElementPtr
+DControllerBase::commandHandler(const std::string& command,
+ isc::data::ConstElementPtr args) {
+
+ LOG_DEBUG(dctl_logger, DBGLVL_COMMAND, DCTL_COMMAND_RECEIVED)
+ .arg(controller_->getAppName()).arg(command)
+ .arg(args ? args->str() : "(no args)");
+
+ // Invoke the instance method on the controller singleton.
+ return (controller_->executeCommand(command, args));
+}
+
+isc::data::ConstElementPtr
+DControllerBase::updateConfig(isc::data::ConstElementPtr new_config) {
+ isc::data::ConstElementPtr full_config;
+ if (stand_alone_) {
+ // @todo Until there is a configuration manager to provide retrieval
+ // we'll just assume the incoming config is the full configuration set.
+ // It may also make more sense to isolate the controller from the
+ // configuration manager entirely. We could do something like
+ // process_->getFullConfig() here for stand-alone mode?
+ full_config = new_config;
+ } else {
+ if (!config_session_) {
+ // That should never happen as we install config_handler
+ // after we instantiate the server.
+ isc::data::ConstElementPtr answer =
+ isc::config::createAnswer(1, "Configuration rejected,"
+ " Session has not started.");
+ return (answer);
+ }
+
+ // Let's get the existing configuration.
+ full_config = config_session_->getFullConfig();
+ }
+
+ // The configuration passed to this handler function is partial.
+ // In other words, it just includes the values being modified.
+ // In the same time, there may be dependencies between various
+ // configuration parsers. For example: the option value can
+ // be set if the definition of this option is set. If someone removes
+ // an existing option definition then the partial configuration that
+ // removes that definition is triggered while a relevant option value
+ // may remain configured. This eventually results in the
+ // configuration being in the inconsistent state.
+ // In order to work around this problem we need to merge the new
+ // configuration with the existing (full) configuration.
+
+ // Let's create a new object that will hold the merged configuration.
+ boost::shared_ptr<isc::data::MapElement>
+ merged_config(new isc::data::MapElement());
+
+ // Merge an existing and new configuration.
+ merged_config->setValue(full_config->mapValue());
+ isc::data::merge(merged_config, new_config);
+
+ // Send the merged configuration to the application.
+ return (process_->configure(merged_config));
+}
+
+
+isc::data::ConstElementPtr
+DControllerBase::executeCommand(const std::string& command,
+ isc::data::ConstElementPtr args) {
+ // Shutdown is universal. If its not that, then try it as
+ // an custom command supported by the derivation. If that
+ // doesn't pan out either, than send to it the application
+ // as it may be supported there.
+ isc::data::ConstElementPtr answer;
+ if (command.compare(SHUT_DOWN_COMMAND) == 0) {
+ answer = shutdown(args);
+ } else {
+ // It wasn't shutdown, so may be a custom controller command.
+ int rcode = 0;
+ answer = customControllerCommand(command, args);
+ isc::config::parseAnswer(rcode, answer);
+ if (rcode == COMMAND_INVALID)
+ {
+ // It wasn't controller command, so may be an application command.
+ answer = process_->command(command, args);
+ }
+ }
+
+ return (answer);
+}
+
+isc::data::ConstElementPtr
+DControllerBase::customControllerCommand(const std::string& command,
+ isc::data::ConstElementPtr /* args */) {
+
+ // Default implementation always returns invalid command.
+ return (isc::config::createAnswer(COMMAND_INVALID,
+ "Unrecognized command: " + command));
+}
+
+isc::data::ConstElementPtr
+DControllerBase::shutdown(isc::data::ConstElementPtr args) {
+ if (process_) {
+ return (process_->shutdown(args));
+ }
+
+ // Not really a failure, but this condition is worth noting. In reality
+ // it should be pretty hard to cause this.
+ LOG_WARN(dctl_logger, DCTL_NOT_RUNNING).arg(app_name_);
+ return (isc::config::createAnswer(0, "Process has not been initialzed."));
+}
+
+void
+DControllerBase::usage(const std::string & text)
+{
+ if (text != "") {
+ std::cerr << "Usage error: " << text << std::endl;
+ }
+
+ std::cerr << "Usage: " << bin_name_ << std::endl;
+ std::cerr << " -v: verbose output" << std::endl;
+ std::cerr << " -s: stand-alone mode (don't connect to BIND10)"
+ << std::endl;
+
+ std::cerr << getUsageText() << std::endl;
+}
+
+DControllerBase::~DControllerBase() {
+}
+
+}; // namespace isc::d2
+}; // namespace isc
diff --git a/src/bin/d2/d_controller.h b/src/bin/d2/d_controller.h
new file mode 100644
index 0000000..917ed64
--- /dev/null
+++ b/src/bin/d2/d_controller.h
@@ -0,0 +1,557 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D_CONTROLLER_H
+#define D_CONTROLLER_H
+
+#include <cc/data.h>
+#include <cc/session.h>
+#include <config/ccsession.h>
+#include <d2/d2_asio.h>
+#include <d2/d2_log.h>
+#include <d2/d_process.h>
+#include <exceptions/exceptions.h>
+#include <log/logger_support.h>
+
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+
+
+namespace isc {
+namespace d2 {
+
+/// @brief DControllerBase launch exit status values. Upon service shutdown
+/// normal or otherwise, the Controller's launch method will return one of
+/// these values.
+
+/// @brief Exception thrown when the command line is invalid.
+class InvalidUsage : public isc::Exception {
+public:
+ InvalidUsage(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown when the application process fails.
+class ProcessInitError: public isc::Exception {
+public:
+ ProcessInitError (const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown when the session start up fails.
+class SessionStartError: public isc::Exception {
+public:
+ SessionStartError (const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown when the application process encounters an
+/// operation in its event loop (i.e. run method).
+class ProcessRunError: public isc::Exception {
+public:
+ ProcessRunError (const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown when the session end fails.
+class SessionEndError: public isc::Exception {
+public:
+ SessionEndError (const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Exception thrown when the controller encounters an operational error.
+class DControllerBaseError : public isc::Exception {
+public:
+ DControllerBaseError (const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Defines a shared pointer to DControllerBase.
+class DControllerBase;
+typedef boost::shared_ptr<DControllerBase> DControllerBasePtr;
+
+/// @brief Defines a shared pointer to a Session.
+typedef boost::shared_ptr<isc::cc::Session> SessionPtr;
+
+/// @brief Defines a shared pointer to a ModuleCCSession.
+typedef boost::shared_ptr<isc::config::ModuleCCSession> ModuleCCSessionPtr;
+
+
+/// @brief Application Controller
+///
+/// DControllerBase is an abstract singleton which provides the framework and
+/// services for managing an application process that implements the
+/// DProcessBase interface. It allows the process to run either in
+/// integrated mode as a BIND10 module or stand-alone. It coordinates command
+/// line argument parsing, process instantiation and initialization, and runtime
+/// control through external command and configuration event handling.
+/// It creates the IOService instance which is used for runtime control
+/// events and passes the IOService into the application process at process
+/// creation. In integrated mode it is responsible for establishing BIND10
+/// session(s) and passes this IOService into the session creation method(s).
+/// It also provides the callback handlers for command and configuration events
+/// received from the external framework (aka BIND10). For example, when
+/// running in integrated mode and a user alters the configuration with the
+/// bindctl tool, BIND10 will emit a configuration message which is sensed by
+/// the controller's IOService. The IOService in turn invokes the configuration
+/// callback, DControllerBase::configHandler(). If the user issues a command
+/// such as shutdown via bindctl, BIND10 will emit a command message, which is
+/// sensed by controller's IOService which invokes the command callback,
+/// DControllerBase::commandHandler().
+///
+/// NOTE: Derivations must supply their own static singleton instance method(s)
+/// for creating and fetching the instance. The base class declares the instance
+/// member in order for it to be available for BIND10 callback functions. This
+/// would not be required if BIND10 supported instance method callbacks.
+class DControllerBase : public boost::noncopyable {
+public:
+ /// @brief Constructor
+ ///
+ /// @param app_name is display name of the application under control. This
+ /// name appears in log statements.
+ /// @param bin_name is the name of the application executable. Typically
+ /// this matches the BIND10 module name.
+ DControllerBase(const char* app_name, const char* bin_name);
+
+ /// @brief Destructor
+ virtual ~DControllerBase();
+
+ /// @brief Acts as the primary entry point into the controller execution
+ /// and provides the outermost application control logic:
+ ///
+ /// 1. parse command line arguments
+ /// 2. instantiate and initialize the application process
+ /// 3. establish BIND10 session(s) if in integrated mode
+ /// 4. start and wait on the application process event loop
+ /// 5. upon event loop completion, disconnect from BIND10 (if needed)
+ /// 6. exit to the caller
+ ///
+ /// It is intended to be called from main() and be given the command line
+ /// arguments. Note this method is deliberately not virtual to ensure the
+ /// proper sequence of events occur.
+ ///
+ /// This function can be run in the test mode. It prevents initialization
+ /// of D2 module logger. This is used in unit tests which initialize logger
+ /// in their main function. Such logger uses environmental variables to
+ /// control severity, verbosity etc. Reinitialization of logger by this
+ /// function would replace unit tests specific logger configuration with
+ /// this suitable for D2 running as a bind10 module.
+ ///
+ /// @param argc is the number of command line arguments supplied
+ /// @param argv is the array of string (char *) command line arguments
+ /// @param test_mode is a bool value which indicates if
+ /// @c DControllerBase::launch should be run in the test mode (if true).
+ /// This parameter doesn't have default value to force test implementers to
+ /// enable test mode explicitly.
+ ///
+ /// @throw throws one of the following exceptions:
+ /// InvalidUsage - Indicates invalid command line.
+ /// ProcessInitError - Failed to create and initialize application
+ /// process object.
+ /// SessionStartError - Could not connect to BIND10 (integrated mode only).
+ /// ProcessRunError - A fatal error occurred while in the application
+ /// process event loop.
+ /// SessionEndError - Could not disconnect from BIND10 (integrated mode
+ /// only).
+ void launch(int argc, char* argv[], const bool test_mode);
+
+ /// @brief A dummy configuration handler that always returns success.
+ ///
+ /// This configuration handler does not perform configuration
+ /// parsing and always returns success. A dummy handler should
+ /// be installed using \ref isc::config::ModuleCCSession ctor
+ /// to get the initial configuration. This initial configuration
+ /// comprises values for only those elements that were modified
+ /// the previous session. The D2 configuration parsing can't be
+ /// used to parse the initial configuration because it may need the
+ /// full configuration to satisfy dependencies between the
+ /// various configuration values. Installing the dummy handler
+ /// that guarantees to return success causes initial configuration
+ /// to be stored for the session being created and that it can
+ /// be later accessed with \ref isc::config::ConfigData::getFullConfig.
+ ///
+ /// @param new_config new configuration.
+ ///
+ /// @return success configuration status.
+ static isc::data::ConstElementPtr
+ dummyConfigHandler(isc::data::ConstElementPtr new_config);
+
+ /// @brief A callback for handling all incoming configuration updates.
+ ///
+ /// As a pointer to this method is used as a callback in ASIO for
+ /// ModuleCCSession, it has to be static. It acts as a wrapper around
+ /// the virtual instance method, updateConfig.
+ ///
+ /// @param new_config textual representation of the new configuration
+ ///
+ /// @return status of the config update
+ static isc::data::ConstElementPtr
+ configHandler(isc::data::ConstElementPtr new_config);
+
+ /// @brief A callback for handling all incoming commands.
+ ///
+ /// As a pointer to this method is used as a callback in ASIO for
+ /// ModuleCCSession, it has to be static. It acts as a wrapper around
+ /// the virtual instance method, executeCommand.
+ ///
+ /// @param command textual representation of the command
+ /// @param args parameters of the command. It can be NULL pointer if no
+ /// arguments exist for a particular command.
+ ///
+ /// @return status of the processed command
+ static isc::data::ConstElementPtr
+ commandHandler(const std::string& command, isc::data::ConstElementPtr args);
+
+ /// @brief Instance method invoked by the configuration event handler and
+ /// which processes the actual configuration update. Provides behavioral
+ /// path for both integrated and stand-alone modes. The current
+ /// implementation will merge the configuration update into the existing
+ /// configuration and then invoke the application process' configure method.
+ ///
+ /// @todo This implementation is will evolve as the D2 configuration
+ /// management task is implemented (trac #2957).
+ ///
+ /// @param new_config is the new configuration
+ ///
+ /// @return returns an Element that contains the results of configuration
+ /// update composed of an integer status value (0 means successful,
+ /// non-zero means failure), and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr
+ updateConfig(isc::data::ConstElementPtr new_config);
+
+
+ /// @brief Instance method invoked by the command event handler and which
+ /// processes the actual command directive.
+ ///
+ /// It supports the execution of:
+ ///
+ /// 1. Stock controller commands - commands common to all DControllerBase
+ /// derivations. Currently there is only one, the shutdown command.
+ ///
+ /// 2. Custom controller commands - commands that the deriving controller
+ /// class implements. These commands are executed by the deriving
+ /// controller.
+ ///
+ /// 3. Custom application commands - commands supported by the application
+ /// process implementation. These commands are executed by the application
+ /// process.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command.
+ ///
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value and a string explanation of the outcome.
+ /// The status value is one of the following:
+ /// D2::COMMAND_SUCCESS - Command executed successfully
+ /// D2::COMMAND_ERROR - Command is valid but suffered an operational
+ /// failure.
+ /// D2::COMMAND_INVALID - Command is not recognized as valid be either
+ /// the controller or the application process.
+ virtual isc::data::ConstElementPtr
+ executeCommand(const std::string& command, isc::data::ConstElementPtr args);
+
+protected:
+ /// @brief Virtual method that provides derivations the opportunity to
+ /// support additional command line options. It is invoked during command
+ /// line argument parsing (see parseArgs method) if the option is not
+ /// recognized as a stock DControllerBase option.
+ ///
+ /// @param option is the option "character" from the command line, without
+ /// any prefixing hyphen(s)
+ /// @param optarg is the argument value (if one) associated with the option
+ ///
+ /// @return must return true if the option was valid, false is it is
+ /// invalid. (Note the default implementation always returns false.)
+ virtual bool customOption(int option, char *optarg);
+
+ /// @brief Abstract method that is responsible for instantiating the
+ /// application process object. It is invoked by the controller after
+ /// command line argument parsing as part of the process initialization
+ /// (see initProcess method).
+ ///
+ /// @return returns a pointer to the new process object (DProcessBase*)
+ /// or NULL if the create fails.
+ /// Note this value is subsequently wrapped in a smart pointer.
+ virtual DProcessBase* createProcess() = 0;
+
+ /// @brief Virtual method that provides derivations the opportunity to
+ /// support custom external commands executed by the controller. This
+ /// method is invoked by the processCommand if the received command is
+ /// not a stock controller command.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command.
+ ///
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value and a string explanation of the outcome.
+ /// The status value is one of the following:
+ /// D2::COMMAND_SUCCESS - Command executed successfully
+ /// D2::COMMAND_ERROR - Command is valid but suffered an operational
+ /// failure.
+ /// D2::COMMAND_INVALID - Command is not recognized as a valid custom
+ /// controller command.
+ virtual isc::data::ConstElementPtr customControllerCommand(
+ const std::string& command, isc::data::ConstElementPtr args);
+
+ /// @brief Virtual method which is invoked after the controller successfully
+ /// establishes BIND10 connectivity. It provides an opportunity for the
+ /// derivation to execute any custom behavior associated with session
+ /// establishment.
+ ///
+ /// Note, it is not called when running stand-alone.
+ ///
+ /// @throw should throw a DControllerBaseError if it fails.
+ virtual void onSessionConnect(){};
+
+ /// @brief Virtual method which is invoked as the first action taken when
+ /// the controller is terminating the session(s) with BIND10. It provides
+ /// an opportunity for the derivation to execute any custom behavior
+ /// associated with session termination.
+ ///
+ /// Note, it is not called when running stand-alone.
+ ///
+ /// @throw should throw a DControllerBaseError if it fails.
+ virtual void onSessionDisconnect(){};
+
+ /// @brief Virtual method which can be used to contribute derivation
+ /// specific usage text. It is invoked by the usage() method under
+ /// invalid usage conditions.
+ ///
+ /// @return returns the desired text.
+ virtual const std::string getUsageText() const {
+ return ("");
+ }
+
+ /// @brief Virtual method which returns a string containing the option
+ /// letters for any custom command line options supported by the derivation.
+ /// These are added to the stock options of "s" and "v" during command
+ /// line interpretation.
+ ///
+ /// @return returns a string containing the custom option letters.
+ virtual const std::string getCustomOpts() const {
+ return ("");
+ }
+
+ /// @brief Fetches the name of the application under control.
+ ///
+ /// @return returns the controller service name string
+ const std::string getAppName() const {
+ return (app_name_);
+ }
+
+ /// @brief Fetches the name of the application executable.
+ ///
+ /// @return returns the controller logger name string
+ const std::string getBinName() const {
+ return (bin_name_);
+ }
+
+ /// @brief Supplies whether or not the controller is in stand alone mode.
+ ///
+ /// @return returns true if in stand alone mode, false otherwise
+ bool isStandAlone() const {
+ return (stand_alone_);
+ }
+
+ /// @brief Method for enabling or disabling stand alone mode.
+ ///
+ /// @param value is the new value to assign the flag.
+ void setStandAlone(bool value) {
+ stand_alone_ = value;
+ }
+
+ /// @brief Supplies whether or not verbose logging is enabled.
+ ///
+ /// @return returns true if verbose logging is enabled.
+ bool isVerbose() const {
+ return (verbose_);
+ }
+
+ /// @brief Method for enabling or disabling verbose logging.
+ ///
+ /// @param value is the new value to assign the flag.
+ void setVerbose(bool value) {
+ verbose_ = value;
+ }
+
+ /// @brief Getter for fetching the controller's IOService
+ ///
+ /// @return returns a pointer reference to the IOService.
+ IOServicePtr& getIOService() {
+ return (io_service_);
+ }
+
+ /// @brief Getter for fetching the name of the controller's BIND10 spec
+ /// file.
+ ///
+ /// @return returns the file name string.
+ const std::string getSpecFileName() const {
+ return (spec_file_name_);
+ }
+
+ /// @brief Setter for setting the name of the controller's BIND10 spec file.
+ ///
+ /// @param spec_file_name the file name string.
+ void setSpecFileName(const std::string& spec_file_name) {
+ spec_file_name_ = spec_file_name;
+ }
+
+ /// @brief Static getter which returns the singleton instance.
+ ///
+ /// @return returns a pointer reference to the private singleton instance
+ /// member.
+ static DControllerBasePtr& getController() {
+ return (controller_);
+ }
+
+ /// @brief Static setter which sets the singleton instance.
+ ///
+ /// @param controller is a pointer to the singleton instance.
+ ///
+ /// @throw throws DControllerBase error if an attempt is made to set the
+ /// instance a second time.
+ static void setController(const DControllerBasePtr& controller);
+
+private:
+ /// @brief Processes the command line arguments. It is the first step
+ /// taken after the controller has been launched. It combines the stock
+ /// list of options with those returned by getCustomOpts(), and uses
+ /// cstdlib's getopt to loop through the command line. The stock options
+ /// It handles stock options directly, and passes any custom options into
+ /// the customOption method. Currently there are only two stock options
+ /// -s for stand alone mode, and -v for verbose logging.
+ ///
+ /// @param argc is the number of command line arguments supplied
+ /// @param argv is the array of string (char *) command line arguments
+ ///
+ /// @throw throws InvalidUsage when there are usage errors.
+ void parseArgs(int argc, char* argv[]);
+
+ /// @brief Instantiates the application process and then initializes it.
+ /// This is the second step taken during launch, following successful
+ /// command line parsing. It is used to invoke the derivation-specific
+ /// implementation of createProcess, following by an invoking of the
+ /// newly instantiated process's init method.
+ ///
+ /// @throw throws DControllerBaseError or indirectly DProcessBaseError
+ /// if there is a failure creating or initializing the application process.
+ void initProcess();
+
+ /// @brief Establishes connectivity with BIND10. This method is used
+ /// invoked during launch, if running in integrated mode, following
+ /// successful process initialization. It is responsible for establishing
+ /// the BIND10 control and config sessions. During the session creation,
+ /// it passes in the controller's IOService and the callbacks for command
+ /// directives and config events. Lastly, it will invoke the onConnect
+ /// method providing the derivation an opportunity to execute any custom
+ /// logic associated with session establishment.
+ ///
+ /// @throw the BIND10 framework may throw std::exceptions.
+ void establishSession();
+
+ /// @brief Invokes the application process's event loop,(DBaseProcess::run).
+ /// It is called during launch only after successfully completing the
+ /// requested setup: command line parsing, application initialization,
+ /// and session establishment (if not stand-alone).
+ /// The process event loop is expected to only return upon application
+ /// shutdown either in response to the shutdown command or due to an
+ /// unrecoverable error.
+ ///
+ // @throw throws DControllerBaseError or indirectly DProcessBaseError
+ void runProcess();
+
+ /// @brief Terminates connectivity with BIND10. This method is invoked
+ /// in integrated mode after the application event loop has exited. It
+ /// first calls the onDisconnect method providing the derivation an
+ /// opportunity to execute custom logic if needed, and then terminates the
+ /// BIND10 config and control sessions.
+ ///
+ /// @throw the BIND10 framework may throw std:exceptions.
+ void disconnectSession();
+
+ /// @brief Initiates shutdown procedure. This method is invoked
+ /// by executeCommand in response to the shutdown command. It will invoke
+ /// the application process's shutdown method which causes the process to
+ /// to begin its shutdown process.
+ ///
+ /// Note, it is assumed that the process of shutting down is neither
+ /// instanteneous nor synchronous. This method does not "block" waiting
+ /// until the process has halted. Rather it is used to convey the
+ /// need to shutdown. A successful return indicates that the shutdown
+ /// has successfully commenced, but does not indicate that the process
+ /// has actually exited.
+ ///
+ /// @return returns an Element that contains the results of shutdown
+ /// command composed of an integer status value (0 means successful,
+ /// non-zero means failure), and a string explanation of the outcome.
+ isc::data::ConstElementPtr shutdown(isc::data::ConstElementPtr args);
+
+ /// @brief Prints the program usage text to std error.
+ ///
+ /// @param text is a string message which will preceded the usage text.
+ /// This is intended to be used for specific usage violation messages.
+ void usage(const std::string& text);
+
+private:
+ /// @brief Display name of the service under control. This name
+ /// appears in log statements.
+ std::string app_name_;
+
+ /// @brief Name of the service executable. By convention this matches
+ /// the BIND10 module name. It is also used to establish the logger
+ /// name.
+ std::string bin_name_;
+
+ /// @brief Indicates if the controller stand alone mode is enabled. When
+ /// enabled, the controller will not establish connectivity with BIND10.
+ bool stand_alone_;
+
+ /// @brief Indicates if the verbose logging mode is enabled.
+ bool verbose_;
+
+ /// @brief The absolute file name of the BIND10 spec file.
+ std::string spec_file_name_;
+
+ /// @brief Pointer to the instance of the process.
+ ///
+ /// This is required for config and command handlers to gain access to
+ /// the process
+ DProcessBasePtr process_;
+
+ /// @brief Shared pointer to an IOService object, used for ASIO operations.
+ IOServicePtr io_service_;
+
+ /// @brief Helper session object that represents raw connection to msgq.
+ SessionPtr cc_session_;
+
+ /// @brief Session that receives configuration and commands.
+ ModuleCCSessionPtr config_session_;
+
+ /// @brief Singleton instance value.
+ static DControllerBasePtr controller_;
+
+// DControllerTest is named a friend class to facilitate unit testing while
+// leaving the intended member scopes intact.
+friend class DControllerTest;
+};
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/d_process.h b/src/bin/d2/d_process.h
new file mode 100644
index 0000000..7ba74f9
--- /dev/null
+++ b/src/bin/d2/d_process.h
@@ -0,0 +1,217 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D_PROCESS_H
+#define D_PROCESS_H
+
+#include <cc/data.h>
+#include <d2/d2_asio.h>
+#include <d2/d_cfg_mgr.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <exceptions/exceptions.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Exception thrown if the process encountered an operational error.
+class DProcessBaseError : public isc::Exception {
+public:
+ DProcessBaseError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief String value for the shutdown command.
+static const std::string SHUT_DOWN_COMMAND("shutdown");
+
+/// @brief Returned by the process to indicate a command was successful.
+static const int COMMAND_SUCCESS = 0;
+
+/// @brief Returned by the process to indicates a command failed.
+static const int COMMAND_ERROR = 1;
+
+/// @brief Returned by the process to indicates a command is not valid.
+static const int COMMAND_INVALID = 2;
+
+/// @brief Application Process Interface
+///
+/// DProcessBase is an abstract class represents the primary "application"
+/// level object in a "managed" asynchronous application. It provides a uniform
+/// interface such that a managing layer can construct, initialize, and start
+/// the application's event loop. The event processing is centered around the
+/// use of isc::asiolink::io_service. The io_service is shared between the
+/// managing layer and the DProcessBase. This allows management layer IO such
+/// as directives to be sensed and handled, as well as processing IO activity
+/// specific to the application. In terms of management layer IO, there are
+/// methods shutdown, configuration updates, and commands unique to the
+/// application.
+class DProcessBase {
+public:
+ /// @brief Constructor
+ ///
+ /// @param app_name is a text label for the process. Generally used
+ /// in log statements, but otherwise arbitrary.
+ /// @param io_service is the io_service used by the caller for
+ /// asynchronous event handling.
+ /// @param cfg_mgr the configuration manager instance that handles
+ /// configuration parsing.
+ ///
+ /// @throw DProcessBaseError is io_service is NULL.
+ DProcessBase(const char* app_name, IOServicePtr io_service,
+ DCfgMgrBasePtr cfg_mgr)
+ : app_name_(app_name), io_service_(io_service), shut_down_flag_(false),
+ cfg_mgr_(cfg_mgr) {
+ if (!io_service_) {
+ isc_throw (DProcessBaseError, "IO Service cannot be null");
+ }
+
+ if (!cfg_mgr_) {
+ isc_throw (DProcessBaseError, "CfgMgr cannot be null");
+ }
+ };
+
+ /// @brief May be used after instantiation to perform initialization unique
+ /// to application. It must be invoked prior to invoking run. This would
+ /// likely include the creation of additional IO sources and their
+ /// integration into the io_service.
+ /// @throw DProcessBaseError if the initialization fails.
+ virtual void init() = 0;
+
+ /// @brief Implements the process's event loop. In its simplest form it
+ /// would an invocation io_service_->run(). This method should not exit
+ /// until the process itself is exiting due to a request to shutdown or
+ /// some anomaly is forcing an exit.
+ /// @throw DProcessBaseError if an operational error is encountered.
+ virtual void run() = 0;
+
+ /// @brief Initiates the process's shutdown process.
+ ///
+ /// This is last step in the shutdown event callback chain, that is
+ /// intended to notify the process it is to begin its shutdown process.
+ ///
+ /// @param args an Element set of shutdown arguments (if any) that are
+ /// supported by the process derivation.
+ ///
+ /// @return an Element that contains the results of argument processing,
+ /// consisting of an integer status value (0 means successful,
+ /// non-zero means failure), and a string explanation of the outcome.
+ ///
+ /// @throw DProcessBaseError if an operational error is encountered.
+ virtual isc::data::ConstElementPtr
+ shutdown(isc::data::ConstElementPtr args) = 0;
+
+ /// @brief Processes the given configuration.
+ ///
+ /// This method may be called multiple times during the process lifetime.
+ /// Certainly once during process startup, and possibly later if the user
+ /// alters configuration. This method must not throw, it should catch any
+ /// processing errors and return a success or failure answer as described
+ /// below.
+ ///
+ /// @param config_set a new configuration (JSON) for the process
+ /// @return an Element that contains the results of configuration composed
+ /// of an integer status value (0 means successful, non-zero means failure),
+ /// and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr configure(isc::data::ConstElementPtr
+ config_set) = 0;
+
+ /// @brief Processes the given command.
+ ///
+ /// This method is called to execute any custom commands supported by the
+ /// process. This method must not throw, it should catch any processing
+ /// errors and return a success or failure answer as described below.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command.
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value:
+ ///
+ /// - COMMAND_SUCCESS indicates a command was successful.
+ /// - COMMAND_ERROR indicates a valid command failed execute.
+ /// - COMMAND_INVALID indicates a command is not valid.
+ ///
+ /// and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr command(
+ const std::string& command, isc::data::ConstElementPtr args) = 0;
+
+ /// @brief Destructor
+ virtual ~DProcessBase(){};
+
+ /// @brief Checks if the process has been instructed to shut down.
+ ///
+ /// @return true if process shutdown flag is true.
+ bool shouldShutdown() const {
+ return (shut_down_flag_);
+ }
+
+ /// @brief Sets the process shut down flag to the given value.
+ ///
+ /// @param value is the new value to assign the flag.
+ void setShutdownFlag(bool value) {
+ shut_down_flag_ = value;
+ }
+
+ /// @brief Fetches the application name.
+ ///
+ /// @return application name string.
+ const std::string getAppName() const {
+ return (app_name_);
+ }
+
+ /// @brief Fetches the controller's IOService.
+ ///
+ /// @return a reference to the controller's IOService.
+ IOServicePtr& getIoService() {
+ return (io_service_);
+ }
+
+ /// @brief Convenience method for stopping IOservice processing.
+ /// Invoking this will cause the process to exit any blocking
+ /// IOService method such as run(). No further IO events will be
+ /// processed.
+ void stopIOService() {
+ io_service_->stop();
+ }
+
+ /// @brief Fetches the process's configuration manager.
+ ///
+ /// @return a reference to the configuration manager.
+ DCfgMgrBasePtr& getCfgMgr() {
+ return (cfg_mgr_);
+ }
+
+private:
+ /// @brief Text label for the process. Generally used in log statements,
+ /// but otherwise can be arbitrary.
+ std::string app_name_;
+
+ /// @brief The IOService to be used for asynchronous event handling.
+ IOServicePtr io_service_;
+
+ /// @brief Boolean flag set when shutdown has been requested.
+ bool shut_down_flag_;
+
+ /// @brief Pointer to the configuration manager.
+ DCfgMgrBasePtr cfg_mgr_;
+};
+
+/// @brief Defines a shared pointer to DProcessBase.
+typedef boost::shared_ptr<DProcessBase> DProcessBasePtr;
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/dhcp-ddns.spec b/src/bin/d2/dhcp-ddns.spec
new file mode 100644
index 0000000..f60ea9f
--- /dev/null
+++ b/src/bin/d2/dhcp-ddns.spec
@@ -0,0 +1,212 @@
+{
+"module_spec":
+{
+ "module_name": "DhcpDdns",
+ "module_description": "DHPC-DDNS Service",
+ "config_data": [
+ {
+ "item_name": "interface",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "eth0"
+ },
+
+ {
+ "item_name": "ip_address",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": "127.0.0.1"
+ },
+
+ {
+ "item_name": "port",
+ "item_type": "integer",
+ "item_optional": true,
+ "item_default": 51771
+ },
+ {
+ "item_name": "tsig_keys",
+ "item_type": "list",
+ "item_optional": true,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "tsig_key",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {"algorithm" : "hmac_md5"},
+ "map_item_spec": [
+ {
+ "item_name": "name",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+ {
+ "item_name": "algorithm",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+ {
+ "item_name": "secret",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ }]
+ }
+ },
+ {
+ "item_name": "forward_ddns",
+ "item_type": "map",
+ "item_optional": true,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "ddns_domains",
+ "item_type": "list",
+ "item_optional": false,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "ddns_domain",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "name",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+
+ {
+ "item_name": "key_name",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+
+ {
+ "item_name": "dns_servers",
+ "item_type": "list",
+ "item_optional": false,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "dns_server",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "hostname",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+ {
+ "item_name": "ip_address",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+ {
+ "item_name": "port",
+ "item_type": "integer",
+ "item_optional": true,
+ "item_default": 53
+ }]
+ }
+ }]
+ }
+ }]
+ },
+
+ {
+ "item_name": "reverse_ddns",
+ "item_type": "map",
+ "item_optional": true,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "ddns_domains",
+ "item_type": "list",
+ "item_optional": false,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "ddns_domain",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "name",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+
+ {
+ "item_name": "key_name",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+
+ {
+ "item_name": "dns_servers",
+ "item_type": "list",
+ "item_optional": false,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "dns_server",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "hostname",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+ {
+ "item_name": "ip_address",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+ {
+ "item_name": "port",
+ "item_type": "integer",
+ "item_optional": true,
+ "item_default": 53
+ }]
+ }
+ }]
+ }
+ }]
+ }],
+
+ "commands": [
+ {
+ "command_name": "shutdown",
+ "command_description": "Shuts down b10-dhcp-ddns module server.",
+ "command_args": [
+ {
+ "item_name": "type",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "normal",
+ "item_description": "values: normal (default), now, or drain_first"
+ }
+ ]
+ }
+ ]
+ }
+}
+
diff --git a/src/bin/d2/dns_client.cc b/src/bin/d2/dns_client.cc
new file mode 100644
index 0000000..9202e7d
--- /dev/null
+++ b/src/bin/d2/dns_client.cc
@@ -0,0 +1,262 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/dns_client.h>
+#include <d2/d2_log.h>
+#include <dns/messagerenderer.h>
+#include <limits>
+
+namespace isc {
+namespace d2 {
+
+namespace {
+
+// OutputBuffer objects are pre-allocated before data is written to them.
+// This is a default number of bytes for the buffers we create within
+// DNSClient class.
+const size_t DEFAULT_BUFFER_SIZE = 128;
+
+}
+
+using namespace isc::util;
+using namespace isc::asiolink;
+using namespace isc::asiodns;
+using namespace isc::dns;
+
+// This class provides the implementation for the DNSClient. This allows for
+// the separation of the DNSClient interface from the implementation details.
+// Currently, implementation uses IOFetch object to handle asynchronous
+// communication with the DNS. This design may be revisited in the future. If
+// implementation is changed, the DNSClient API will remain unchanged thanks
+// to this separation.
+class DNSClientImpl : public asiodns::IOFetch::Callback {
+public:
+ // A buffer holding response from a DNS.
+ util::OutputBufferPtr in_buf_;
+ // A caller-supplied object which will hold the parsed response from DNS.
+ // The response object is (or descends from) isc::dns::Message and is
+ // populated using Message::fromWire(). This method may only be called
+ // once in the lifetime of a Message instance. Therefore, response_ is a
+ // pointer reference thus allowing this class to replace the object
+ // pointed to with a new Message instance each time a message is
+ // received. This allows a single DNSClientImpl instance to be used for
+ // multiple, sequential IOFetch calls. (@todo Trac# 3286 has been opened
+ // against dns::Message::fromWire. Should the behavior of fromWire change
+ // the behavior here with could be rexamined).
+ D2UpdateMessagePtr& response_;
+ // A caller-supplied external callback which is invoked when DNS message
+ // exchange is complete or interrupted.
+ DNSClient::Callback* callback_;
+ // A Transport Layer protocol used to communicate with a DNS.
+ DNSClient::Protocol proto_;
+
+ // Constructor and Destructor
+ DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
+ DNSClient::Callback* callback,
+ const DNSClient::Protocol proto);
+ virtual ~DNSClientImpl();
+
+ // This internal callback is called when the DNS update message exchange is
+ // complete. It further invokes the external callback provided by a caller.
+ // Before external callback is invoked, an object of the D2UpdateMessage
+ // type, representing a response from the server is set.
+ virtual void operator()(asiodns::IOFetch::Result result);
+
+ // Starts asynchronous DNS Update.
+ void doUpdate(asiolink::IOService& io_service,
+ const asiolink::IOAddress& ns_addr,
+ const uint16_t ns_port,
+ D2UpdateMessage& update,
+ const unsigned int wait);
+
+ // This function maps the IO error to the DNSClient error.
+ DNSClient::Status getStatus(const asiodns::IOFetch::Result);
+};
+
+DNSClientImpl::DNSClientImpl(D2UpdateMessagePtr& response_placeholder,
+ DNSClient::Callback* callback,
+ const DNSClient::Protocol proto)
+ : in_buf_(new OutputBuffer(DEFAULT_BUFFER_SIZE)),
+ response_(response_placeholder), callback_(callback), proto_(proto) {
+
+ // Response should be an empty pointer. It gets populated by the
+ // operator() method.
+ if (response_) {
+ isc_throw(isc::BadValue, "Response buffer pointer should be null");
+ }
+
+ // @todo Currently we only support UDP. The support for TCP is planned for
+ // the future release.
+ if (proto_ == DNSClient::TCP) {
+ isc_throw(isc::NotImplemented, "TCP is currently not supported as a"
+ << " Transport protocol for DNS Updates; please use UDP");
+ }
+
+ // Given that we already eliminated the possibility that TCP is used, it
+ // would be sufficient to check that (proto != DNSClient::UDP). But, once
+ // support TCP is added the check above will disappear and the extra check
+ // will be needed here anyway.
+ // Note that cascaded check is used here instead of:
+ // if (proto_ != DNSClient::TCP && proto_ != DNSClient::UDP)..
+ // because some versions of GCC compiler complain that check above would
+ // be always 'false' due to limited range of enumeration. In fact, it is
+ // possible to pass out of range integral value through enum and it should
+ // be caught here.
+ if (proto_ != DNSClient::TCP) {
+ if (proto_ != DNSClient::UDP) {
+ isc_throw(isc::NotImplemented, "invalid transport protocol type '"
+ << proto_ << "' specified for DNS Updates");
+ }
+ }
+}
+
+DNSClientImpl::~DNSClientImpl() {
+}
+
+void
+DNSClientImpl::operator()(asiodns::IOFetch::Result result) {
+ // Get the status from IO. If no success, we just call user's callback
+ // and pass the status code.
+ DNSClient::Status status = getStatus(result);
+ if (status == DNSClient::SUCCESS) {
+ InputBuffer response_buf(in_buf_->getData(), in_buf_->getLength());
+ // Allocate a new response message. (Note that Message::fromWire
+ // may only be run once per message, so we need to start fresh
+ // each time.)
+ response_.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
+
+ // Server's response may be corrupted. In such case, fromWire will
+ // throw an exception. We want to catch this exception to return
+ // appropriate status code to the caller and log this event.
+ try {
+ response_->fromWire(response_buf);
+
+ } catch (const Exception& ex) {
+ status = DNSClient::INVALID_RESPONSE;
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
+ DHCP_DDNS_INVALID_RESPONSE).arg(ex.what());
+
+ }
+ }
+
+ // Once we are done with internal business, let's call a callback supplied
+ // by a caller.
+ if (callback_ != NULL) {
+ (*callback_)(status);
+ }
+}
+
+DNSClient::Status
+DNSClientImpl::getStatus(const asiodns::IOFetch::Result result) {
+ switch (result) {
+ case IOFetch::SUCCESS:
+ return (DNSClient::SUCCESS);
+
+ case IOFetch::TIME_OUT:
+ return (DNSClient::TIMEOUT);
+
+ case IOFetch::STOPPED:
+ return (DNSClient::IO_STOPPED);
+
+ default:
+ ;
+ }
+ return (DNSClient::OTHER);
+}
+
+void
+DNSClientImpl::doUpdate(asiolink::IOService& io_service,
+ const IOAddress& ns_addr,
+ const uint16_t ns_port,
+ D2UpdateMessage& update,
+ const unsigned int wait) {
+ // A renderer is used by the toWire function which creates the on-wire data
+ // from the DNS Update message. A renderer has its internal buffer where it
+ // renders data by default. However, this buffer can't be directly accessed.
+ // Fortunately, the renderer's API accepts user-supplied buffers. So, let's
+ // create our own buffer and pass it to the renderer so as the message is
+ // rendered to this buffer. Finally, we pass this buffer to IOFetch.
+ dns::MessageRenderer renderer;
+ OutputBufferPtr msg_buf(new OutputBuffer(DEFAULT_BUFFER_SIZE));
+ renderer.setBuffer(msg_buf.get());
+
+ // Render DNS Update message. This may throw a bunch of exceptions if
+ // invalid message object is given.
+ update.toWire(renderer);
+
+ // IOFetch has all the mechanisms that we need to perform asynchronous
+ // communication with the DNS server. The last but one argument points to
+ // this object as a completion callback for the message exchange. As a
+ // result operator()(Status) will be called.
+
+ // Timeout value is explicitly cast to the int type to avoid warnings about
+ // overflows when doing implicit cast. It should have been checked by the
+ // caller that the unsigned timeout value will fit into int.
+ IOFetch io_fetch(IOFetch::UDP, io_service, msg_buf, ns_addr, ns_port,
+ in_buf_, this, static_cast<int>(wait));
+
+ // Post the task to the task queue in the IO service. Caller will actually
+ // run these tasks by executing IOService::run.
+ io_service.post(io_fetch);
+}
+
+
+DNSClient::DNSClient(D2UpdateMessagePtr& response_placeholder,
+ Callback* callback, const DNSClient::Protocol proto)
+ : impl_(new DNSClientImpl(response_placeholder, callback, proto)) {
+}
+
+DNSClient::~DNSClient() {
+ delete (impl_);
+}
+
+unsigned int
+DNSClient::getMaxTimeout() {
+ static const unsigned int max_timeout = std::numeric_limits<int>::max();
+ return (max_timeout);
+}
+
+void
+DNSClient::doUpdate(asiolink::IOService&,
+ const IOAddress&,
+ const uint16_t,
+ D2UpdateMessage&,
+ const unsigned int,
+ const dns::TSIGKey&) {
+ isc_throw(isc::NotImplemented, "TSIG is currently not supported for"
+ "DNS Update message");
+}
+
+void
+DNSClient::doUpdate(asiolink::IOService& io_service,
+ const IOAddress& ns_addr,
+ const uint16_t ns_port,
+ D2UpdateMessage& update,
+ const unsigned int wait) {
+ // The underlying implementation which we use to send DNS Updates uses
+ // signed integers for timeout. If we want to avoid overflows we need to
+ // respect this limitation here.
+ if (wait > getMaxTimeout()) {
+ isc_throw(isc::BadValue, "A timeout value for DNS Update request must"
+ " not exceed " << getMaxTimeout()
+ << ". Provided timeout value is '" << wait << "'");
+ }
+ impl_->doUpdate(io_service, ns_addr, ns_port, update, wait);
+}
+
+
+
+} // namespace d2
+} // namespace isc
+
diff --git a/src/bin/d2/dns_client.h b/src/bin/d2/dns_client.h
new file mode 100644
index 0000000..5960f6a
--- /dev/null
+++ b/src/bin/d2/dns_client.h
@@ -0,0 +1,192 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DNS_CLIENT_H
+#define DNS_CLIENT_H
+
+#include <d2/d2_update_message.h>
+
+#include <asiolink/io_service.h>
+#include <util/buffer.h>
+
+#include <asiodns/io_fetch.h>
+#include <dns/tsig.h>
+
+namespace isc {
+namespace d2 {
+
+class DNSClient;
+typedef boost::shared_ptr<DNSClient> DNSClientPtr;
+
+/// DNSClient class implementation.
+class DNSClientImpl;
+
+/// @brief The @c DNSClient class handles communication with the DNS server.
+///
+/// Communication with the DNS server is asynchronous. Caller must provide a
+/// callback, which will be invoked when the response from the DNS server is
+/// received, a timeout has occurred or IO service has been stopped for any
+/// reason. The caller-supplied callback is called by the internal callback
+/// operator implemented by @c DNSClient. This callback is responsible for
+/// initializing the @c D2UpdateMessage instance which encapsulates the response
+/// from the DNS. This initialization does not take place if the response from
+/// DNS is not received.
+///
+/// Caller must supply a pointer to the @c D2UpdateMessage object, which will
+/// encapsulate DNS response, through class constructor. An exception will be
+/// thrown if the pointer is not initialized by the caller.
+///
+/// @todo Ultimately, this class will support both TCP and UDP Transport.
+/// Currently only UDP is supported and can be specified as a preferred
+/// protocol. @c DNSClient constructor will throw an exception if TCP is
+/// specified. Once both protocols are supported, the @c DNSClient logic will
+/// try to obey caller's preference. However, it may use the other protocol if
+/// on its own discretion, when there is a legitimate reason to do so. For
+/// example, if communication with the server using preferred protocol fails.
+class DNSClient {
+public:
+
+ /// @brief Transport layer protocol used by a DNS Client to communicate
+ /// with a server.
+ enum Protocol {
+ UDP,
+ TCP
+ };
+
+ /// @brief A status code of the DNSClient.
+ enum Status {
+ SUCCESS, ///< Response received and is ok.
+ TIMEOUT, ///< No response, timeout.
+ IO_STOPPED, ///< IO was stopped.
+ INVALID_RESPONSE, ///< Response received but invalid.
+ OTHER ///< Other, unclassified error.
+ };
+
+ /// @brief Callback for the @c DNSClient class.
+ ///
+ /// This is is abstract class which represents the external callback for the
+ /// @c DNSClient. Caller must implement this class and supply its instance
+ /// in the @c DNSClient constructor to get callbacks when the DNS Update
+ /// exchange is complete (@see @c DNSClient).
+ class Callback {
+ public:
+ /// @brief Virtual destructor.
+ virtual ~Callback() { }
+
+ /// @brief Function operator implementing a callback.
+ ///
+ /// @param status a @c DNSClient::Status enum representing status code
+ /// of DNSClient operation.
+ virtual void operator()(DNSClient::Status status) = 0;
+ };
+
+ /// @brief Constructor.
+ ///
+ /// @param response_placeholder Messge object pointer which will be updated
+ /// with dynamically allocated object holding the DNS server's response.
+ /// @param callback Pointer to an object implementing @c DNSClient::Callback
+ /// class. This object will be called when DNS message exchange completes or
+ /// if an error occurs. NULL value disables callback invocation.
+ /// @param proto caller's preference regarding Transport layer protocol to
+ /// be used by DNS Client to communicate with a server.
+ DNSClient(D2UpdateMessagePtr& response_placeholder, Callback* callback,
+ const Protocol proto = UDP);
+
+ /// @brief Virtual destructor, does nothing.
+ ~DNSClient();
+
+ ///
+ /// @name Copy constructor and assignment operator
+ ///
+ /// Copy constructor and assignment operator are private because there are
+ /// no use cases when @DNSClient instance will need to be copied. Also, it
+ /// is desired to avoid copying @DNSClient::impl_ pointer and external
+ /// callbacks.
+ ///
+ //@{
+private:
+ DNSClient(const DNSClient& source);
+ DNSClient& operator=(const DNSClient& source);
+ //@}
+
+public:
+
+ /// @brief Returns maximal allowed timeout value accepted by
+ /// @c DNSClient::doUpdate.
+ ///
+ /// @return maximal allowed timeout value accepted by @c DNSClient::doUpdate
+ static unsigned int getMaxTimeout();
+
+ /// @brief Start asynchronous DNS Update with TSIG.
+ ///
+ /// This function starts asynchronous DNS Update and returns. The DNS Update
+ /// will be executed by the specified IO service. Once the message exchange
+ /// with a DNS server is complete, timeout occurs or IO operation is
+ /// interrupted, the caller-supplied callback function will be invoked.
+ ///
+ /// An address and port of the DNS server is specified through the function
+ /// arguments so as the same instance of the @c DNSClient can be used to
+ /// initiate multiple message exchanges.
+ ///
+ /// @param io_service IO service to be used to run the message exchange.
+ /// @param ns_addr DNS server address.
+ /// @param ns_port DNS server port.
+ /// @param update A DNS Update message to be sent to the server.
+ /// @param wait A timeout (in seconds) for the response. If a response is
+ /// not received within the timeout, exchange is interrupted. This value
+ /// must not exceed maximal value for 'int' data type.
+ /// @param tsig_key An @c isc::dns::TSIGKey object representing TSIG
+ /// context which will be used to render the DNS Update message.
+ ///
+ /// @todo Implement TSIG Support. Currently any attempt to call this
+ /// function will result in exception.
+ void doUpdate(asiolink::IOService& io_service,
+ const asiolink::IOAddress& ns_addr,
+ const uint16_t ns_port,
+ D2UpdateMessage& update,
+ const unsigned int wait,
+ const dns::TSIGKey& tsig_key);
+
+ /// @brief Start asynchronous DNS Update without TSIG.
+ ///
+ /// This function starts asynchronous DNS Update and returns. The DNS Update
+ /// will be executed by the specified IO service. Once the message exchange
+ /// with a DNS server is complete, timeout occurs or IO operation is
+ /// interrupted, the caller-supplied callback function will be invoked.
+ ///
+ /// An address and port of the DNS server is specified through the function
+ /// arguments so as the same instance of the @c DNSClient can be used to
+ /// initiate multiple message exchanges.
+ ///
+ /// @param io_service IO service to be used to run the message exchange.
+ /// @param ns_addr DNS server address.
+ /// @param ns_port DNS server port.
+ /// @param update A DNS Update message to be sent to the server.
+ /// @param wait A timeout (in seconds) for the response. If a response is
+ /// not received within the timeout, exchange is interrupted. This value
+ /// must not exceed maximal value for 'int' data type.
+ void doUpdate(asiolink::IOService& io_service,
+ const asiolink::IOAddress& ns_addr,
+ const uint16_t ns_port,
+ D2UpdateMessage& update,
+ const unsigned int wait);
+
+private:
+ DNSClientImpl* impl_; ///< Pointer to DNSClient implementation.
+};
+
+} // namespace d2
+} // namespace isc
+
+#endif // DNS_CLIENT_H
diff --git a/src/bin/d2/labeled_value.cc b/src/bin/d2/labeled_value.cc
new file mode 100644
index 0000000..cf836f7
--- /dev/null
+++ b/src/bin/d2/labeled_value.cc
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/labeled_value.h>
+
+namespace isc {
+namespace d2 {
+
+/**************************** LabeledValue ****************************/
+
+LabeledValue::LabeledValue(const int value, const std::string& label)
+ : value_(value), label_(label) {
+ if (label.empty()) {
+ isc_throw(LabeledValueError, "labels cannot be empty");
+ }
+}
+
+LabeledValue::~LabeledValue(){
+}
+
+int
+LabeledValue::getValue() const {
+ return (value_);
+}
+
+std::string
+LabeledValue::getLabel() const {
+ return (label_);
+}
+
+bool
+LabeledValue::operator==(const LabeledValue& other) const {
+ return (this->value_ == other.value_);
+}
+
+bool
+LabeledValue::operator!=(const LabeledValue& other) const {
+ return (this->value_ != other.value_);
+}
+
+bool
+LabeledValue::operator<(const LabeledValue& other) const {
+ return (this->value_ < other.value_);
+}
+
+std::ostream& operator<<(std::ostream& os, const LabeledValue& vlp) {
+ os << vlp.getLabel();
+ return (os);
+}
+
+/**************************** LabeledValueSet ****************************/
+
+const char* LabeledValueSet::UNDEFINED_LABEL = "UNDEFINED";
+
+LabeledValueSet::LabeledValueSet(){
+}
+
+LabeledValueSet::~LabeledValueSet() {
+}
+
+void
+LabeledValueSet::add(LabeledValuePtr entry) {
+ if (!entry) {
+ isc_throw(LabeledValueError, "cannot add an null entry to set");
+ }
+
+ const int value = entry->getValue();
+ if (isDefined(value)) {
+ isc_throw(LabeledValueError,
+ "value: " << value << " is already defined as: "
+ << getLabel(value));
+ }
+
+ map_[entry->getValue()]=entry;
+}
+
+void
+LabeledValueSet::add(const int value, const std::string& label) {
+ add (LabeledValuePtr(new LabeledValue(value,label)));
+}
+
+const LabeledValuePtr&
+LabeledValueSet::get(int value) {
+ static LabeledValuePtr undefined;
+ LabeledValueMap::iterator it = map_.find(value);
+ if (it != map_.end()) {
+ return ((*it).second);
+ }
+
+ // Return an empty pointer when not found.
+ return (undefined);
+}
+
+bool
+LabeledValueSet::isDefined(const int value) const {
+ LabeledValueMap::const_iterator it = map_.find(value);
+ return (it != map_.end());
+}
+
+std::string
+LabeledValueSet::getLabel(const int value) const {
+ LabeledValueMap::const_iterator it = map_.find(value);
+ if (it != map_.end()) {
+ const LabeledValuePtr& ptr = (*it).second;
+ return (ptr->getLabel());
+ }
+
+ return (std::string(UNDEFINED_LABEL));
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/labeled_value.h b/src/bin/d2/labeled_value.h
new file mode 100644
index 0000000..965df9d
--- /dev/null
+++ b/src/bin/d2/labeled_value.h
@@ -0,0 +1,184 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LABELED_VALUE_H
+#define LABELED_VALUE_H
+
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+#include <ostream>
+#include <string>
+#include <map>
+
+/// @file labeled_value.h This file defines classes: LabeledValue and
+/// LabeledValueSet.
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if an error is encountered handling a LabeledValue.
+class LabeledValueError : public isc::Exception {
+public:
+ LabeledValueError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Implements the concept of a constant value with a text label.
+///
+/// This class implements an association between an constant integer value
+/// and a text label. It provides a single constructor, accessors for both
+/// the value and label, and boolean operators which treat the value as
+/// the "key" for comparisons. This allows them to be assembled into
+/// dictionaries of unique values. Note, that the labels are not required to
+/// be unique but in practice it makes little sense to for them to be
+/// otherwise.
+class LabeledValue {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param value the numeric constant value to be labeled.
+ /// @param label the text label to associate to this value.
+ ///
+ /// @throw LabeledValueError if label is empty.
+ LabeledValue(const int value, const std::string& label);
+
+ /// @brief Destructor.
+ ///
+ /// Destructor is virtual to permit derivations.
+ virtual ~LabeledValue();
+
+ /// @brief Gets the integer value of this instance.
+ ///
+ /// @return integer value of this instance.
+ int getValue() const;
+
+ /// @brief Gets the text label of this instance.
+ ///
+ /// @return The text label as string
+ std::string getLabel() const;
+
+ /// @brief Equality operator
+ ///
+ /// @return True if a.value_ is equal to b.value_.
+ bool operator==(const LabeledValue& other) const;
+
+ /// @brief Inequality operator
+ ///
+ /// @return True if a.value_ is not equal to b.value_.
+ bool operator!=(const LabeledValue& other) const;
+
+ /// @brief Less-than operator
+ ///
+ /// @return True if a.value_ is less than b.value_.
+ bool operator<(const LabeledValue& other) const;
+
+private:
+ /// @brief The numeric value to label.
+ int value_;
+
+ /// @brief The text label for the value.
+ std::string label_;
+};
+
+/// @brief Dumps the label to ostream.
+std::ostream& operator<<(std::ostream& os, const LabeledValue& vlp);
+
+/// @brief Defines a shared pointer to a LabeledValue instance.
+typedef boost::shared_ptr<LabeledValue> LabeledValuePtr;
+
+/// @brief Defines a map of pointers to LabeledValues keyed by value.
+typedef std::map<unsigned int, LabeledValuePtr> LabeledValueMap;
+
+
+/// @brief Implements a set of unique LabeledValues.
+///
+/// This class is intended to function as a dictionary of numeric values
+/// and the labels associated with them. It is essentially a thin wrapper
+/// around a std::map of LabeledValues, keyed by their values. This is handy
+/// for defining a set of "valid" constants while conveniently associating a
+/// text label with each value.
+///
+/// This class offers two variants of an add method for adding entries to the
+/// set, and accessors for finding an entry or an entry's label by value.
+/// Note that the add methods may throw but all accessors are exception safe.
+/// It is up to the caller to determine when and if an undefined value is
+/// exception-worthy.
+///
+/// More interestingly, a derivation of this class can be used to "define"
+/// valid instances of derivations of LabeledValue.
+class LabeledValueSet {
+public:
+ /// @brief Defines a text label returned by when value is not found.
+ static const char* UNDEFINED_LABEL;
+
+ /// @brief Constructor
+ ///
+ /// Constructs an empty set.
+ LabeledValueSet();
+
+ /// @brief Destructor
+ ///
+ /// Destructor is virtual to permit derivations.
+ virtual ~LabeledValueSet();
+
+ /// @brief Adds the given entry to the set
+ ///
+ /// @param entry is the entry to add.
+ ///
+ /// @throw LabeledValuePtr if the entry is null or the set already
+ /// contains an entry with the same value.
+ void add(LabeledValuePtr entry);
+
+ /// @brief Adds an entry to the set for the given value and label
+ ///
+ /// @param value the numeric constant value to be labeled.
+ /// @param label the text label to associate to this value.
+ ///
+ /// @throw LabeledValuePtr if the label is empty, or if the set
+ /// already contains an entry with the same value.
+ void add(const int value, const std::string& label);
+
+ /// @brief Fetches a pointer to the entry associated with value
+ ///
+ /// @param value is the value of the entry desired.
+ ///
+ /// @return A pointer to the entry if the entry was found otherwise the
+ /// pointer is empty.
+ const LabeledValuePtr& get(int value);
+
+ /// @brief Tests if the set contains an entry for the given value.
+ ///
+ /// @param value is the value of the entry to test.
+ ///
+ /// @return True if an entry for value exists in the set, false if not.
+ bool isDefined(const int value) const;
+
+ /// @brief Fetches the label for the given value
+ ///
+ /// @param value is the value for which the label is desired.
+ ///
+ /// @return the label of the value if defined, otherwise it returns
+ /// UNDEFINED_LABEL.
+ std::string getLabel(const int value) const;
+
+private:
+ /// @brief The map of labeled values.
+ LabeledValueMap map_;
+};
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/main.cc b/src/bin/d2/main.cc
new file mode 100644
index 0000000..4b5b8bc
--- /dev/null
+++ b/src/bin/d2/main.cc
@@ -0,0 +1,49 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <d2/d2_log.h>
+#include <d2/d2_controller.h>
+#include <exceptions/exceptions.h>
+#include <log/logger_support.h>
+#include <log/logger_manager.h>
+
+#include <iostream>
+
+using namespace isc::d2;
+using namespace std;
+
+/// This file contains entry point (main() function) for standard DHCP-DDNS
+/// process, b10-dhcp-ddns, component for BIND10 framework. It fetches
+/// the D2Controller singleton instance and invokes its launch method.
+/// The exit value of the program will be EXIT_SUCCESS if there were no
+/// errors, EXIT_FAILURE otherwise.
+int main(int argc, char* argv[]) {
+ int ret = EXIT_SUCCESS;
+
+ // Instantiate/fetch the DHCP-DDNS application controller singleton.
+ DControllerBasePtr& controller = D2Controller::instance();
+
+ // Launch the controller passing in command line arguments.
+ // Exit program with the controller's return code.
+ try {
+ // 'false' value disables test mode.
+ controller->launch(argc, argv, false);
+ } catch (const isc::Exception& ex) {
+ std::cerr << "Service failed:" << ex.what() << std::endl;
+ ret = EXIT_FAILURE;
+ }
+
+ return (ret);
+}
diff --git a/src/bin/d2/nc_add.cc b/src/bin/d2/nc_add.cc
new file mode 100644
index 0000000..abd22f6
--- /dev/null
+++ b/src/bin/d2/nc_add.cc
@@ -0,0 +1,697 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/nc_add.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+
+#include <util/buffer.h>
+#include <dns/rdataclass.h>
+
+namespace isc {
+namespace d2 {
+
+// NameAddTransaction states
+const int NameAddTransaction::ADDING_FWD_ADDRS_ST;
+const int NameAddTransaction::REPLACING_FWD_ADDRS_ST;
+const int NameAddTransaction::REPLACING_REV_PTRS_ST;
+
+// NameAddTransaction events
+const int NameAddTransaction::FQDN_IN_USE_EVT;
+const int NameAddTransaction::FQDN_NOT_IN_USE_EVT;
+
+NameAddTransaction::
+NameAddTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain)
+ : NameChangeTransaction(io_service, ncr, forward_domain, reverse_domain) {
+ if (ncr->getChangeType() != isc::dhcp_ddns::CHG_ADD) {
+ isc_throw (NameAddTransactionError,
+ "NameAddTransaction, request type must be CHG_ADD");
+ }
+}
+
+NameAddTransaction::~NameAddTransaction(){
+}
+
+void
+NameAddTransaction::defineEvents() {
+ // Call superclass impl first.
+ NameChangeTransaction::defineEvents();
+
+ // Define NameAddTransaction events.
+ defineEvent(FQDN_IN_USE_EVT, "FQDN_IN_USE_EVT");
+ defineEvent(FQDN_NOT_IN_USE_EVT, "FQDN_NOT_IN_USE_EVT");
+}
+
+void
+NameAddTransaction::verifyEvents() {
+ // Call superclass implementation first to verify its events. These are
+ // events common to all transactions, and they must be defined.
+ // SELECT_SERVER_EVT
+ // SERVER_SELECTED_EVT
+ // SERVER_IO_ERROR_EVT
+ // NO_MORE_SERVERS_EVT
+ // IO_COMPLETED_EVT
+ // UPDATE_OK_EVT
+ // UPDATE_FAILED_EVT
+ NameChangeTransaction::verifyEvents();
+
+ // Verify NameAddTransaction events by attempting to fetch them.
+ getEvent(FQDN_IN_USE_EVT);
+ getEvent(FQDN_NOT_IN_USE_EVT);
+}
+
+void
+NameAddTransaction::defineStates() {
+ // Call superclass impl first.
+ NameChangeTransaction::defineStates();
+
+ // Define NameAddTransaction states.
+ defineState(READY_ST, "READY_ST",
+ boost::bind(&NameAddTransaction::readyHandler, this));
+
+ defineState(SELECTING_FWD_SERVER_ST, "SELECTING_FWD_SERVER_ST",
+ boost::bind(&NameAddTransaction::selectingFwdServerHandler, this));
+
+ defineState(SELECTING_REV_SERVER_ST, "SELECTING_REV_SERVER_ST",
+ boost::bind(&NameAddTransaction::selectingRevServerHandler, this));
+
+ defineState(ADDING_FWD_ADDRS_ST, "ADDING_FWD_ADDRS_ST",
+ boost::bind(&NameAddTransaction::addingFwdAddrsHandler, this));
+
+ defineState(REPLACING_FWD_ADDRS_ST, "REPLACING_FWD_ADDRS_ST",
+ boost::bind(&NameAddTransaction::replacingFwdAddrsHandler, this));
+
+ defineState(REPLACING_REV_PTRS_ST, "REPLACING_REV_PTRS_ST",
+ boost::bind(&NameAddTransaction::replacingRevPtrsHandler, this));
+
+ defineState(PROCESS_TRANS_OK_ST, "PROCESS_TRANS_OK_ST",
+ boost::bind(&NameAddTransaction::processAddOkHandler, this));
+
+ defineState(PROCESS_TRANS_FAILED_ST, "PROCESS_TRANS_FAILED_ST",
+ boost::bind(&NameAddTransaction::processAddFailedHandler, this));
+
+}
+void
+NameAddTransaction::verifyStates() {
+ // Call superclass implementation first to verify its states. These are
+ // states common to all transactions, and they must be defined.
+ // READY_ST
+ // SELECTING_FWD_SERVER_ST
+ // SELECTING_REV_SERVER_ST
+ // PROCESS_TRANS_OK_ST
+ // PROCESS_TRANS_FAILED_ST
+ NameChangeTransaction::verifyStates();
+
+ // Verify NameAddTransaction states by attempting to fetch them.
+ getState(ADDING_FWD_ADDRS_ST);
+ getState(REPLACING_FWD_ADDRS_ST);
+ getState(REPLACING_REV_PTRS_ST);
+}
+
+void
+NameAddTransaction::readyHandler() {
+ switch(getNextEvent()) {
+ case START_EVT:
+ if (getForwardDomain()) {
+ // Request includes a forward change, do that first.
+ transition(SELECTING_FWD_SERVER_ST, SELECT_SERVER_EVT);
+ } else {
+ // Reverse change only, transition accordingly.
+ transition(SELECTING_REV_SERVER_ST, SELECT_SERVER_EVT);
+ }
+
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::selectingFwdServerHandler() {
+ switch(getNextEvent()) {
+ case SELECT_SERVER_EVT:
+ // First time through for this transaction, so initialize server
+ // selection.
+ initServerSelection(getForwardDomain());
+ break;
+ case SERVER_IO_ERROR_EVT:
+ // We failed to communicate with current server. Attempt to select
+ // another one below.
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+
+ // Select the next server from the list of forward servers.
+ if (selectNextServer()) {
+ // We have a server to try.
+ transition(ADDING_FWD_ADDRS_ST, SERVER_SELECTED_EVT);
+ }
+ else {
+ // Server list is exhausted, so fail the transaction.
+ transition(PROCESS_TRANS_FAILED_ST, NO_MORE_SERVERS_EVT);
+ }
+}
+
+void
+NameAddTransaction::addingFwdAddrsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildAddFwdAddressRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_ADD_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ if (rcode == dns::Rcode::NOERROR()) {
+ // We were able to add it. Mark it as done.
+ setForwardChangeCompleted(true);
+
+ // If request calls for reverse update then do that next,
+ // otherwise we can process ok.
+ if (getReverseDomain()) {
+ transition(SELECTING_REV_SERVER_ST, SELECT_SERVER_EVT);
+ } else {
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ }
+ } else if (rcode == dns::Rcode::YXDOMAIN()) {
+ // FQDN is in use so we need to attempt to replace
+ // forward address.
+ transition(REPLACING_FWD_ADDRS_ST, FQDN_IN_USE_EVT);
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should we try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_ADD_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_ADD_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_ADD_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_ADD_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::replacingFwdAddrsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case FQDN_IN_USE_EVT:
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildReplaceFwdAddressRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REPLACE_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ if (rcode == dns::Rcode::NOERROR()) {
+ // We were able to replace the forward mapping. Mark it as done.
+ setForwardChangeCompleted(true);
+
+ // If request calls for reverse update then do that next,
+ // otherwise we can process ok.
+ if (getReverseDomain()) {
+ transition(SELECTING_REV_SERVER_ST, SELECT_SERVER_EVT);
+ } else {
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ }
+ } else if (rcode == dns::Rcode::NXDOMAIN()) {
+ // FQDN is NOT in use so go back and do the forward add address.
+ // Covers the case that it was there when we tried to add it,
+ // but has since been removed per RFC 4703.
+ transition(ADDING_FWD_ADDRS_ST, SERVER_SELECTED_EVT);
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REPLACE_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REPLACE_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REPLACE_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_FORWARD_REPLACE_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::selectingRevServerHandler() {
+ switch(getNextEvent()) {
+ case SELECT_SERVER_EVT:
+ // First time through for this transaction, so initialize server
+ // selection.
+ initServerSelection(getReverseDomain());
+ break;
+ case SERVER_IO_ERROR_EVT:
+ // We failed to communicate with current server. Attempt to select
+ // another one below.
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+
+ // Select the next server from the list of forward servers.
+ if (selectNextServer()) {
+ // We have a server to try.
+ transition(REPLACING_REV_PTRS_ST, SERVER_SELECTED_EVT);
+ }
+ else {
+ // Server list is exhausted, so fail the transaction.
+ transition(PROCESS_TRANS_FAILED_ST, NO_MORE_SERVERS_EVT);
+ }
+}
+
+
+void
+NameAddTransaction::replacingRevPtrsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildReplaceRevPtrsRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REPLACE_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ if (rcode == dns::Rcode::NOERROR()) {
+ // We were able to update the reverse mapping. Mark it as done.
+ setReverseChangeCompleted(true);
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REPLACE_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REPLACE_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_REV_SERVER_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REPLACE_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_REV_SERVER_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_REVERSE_REPLACE_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::processAddOkHandler() {
+ switch(getNextEvent()) {
+ case UPDATE_OK_EVT:
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL, DHCP_DDNS_ADD_SUCCEEDED)
+ .arg(getNcr()->toText());
+ setNcrStatus(dhcp_ddns::ST_COMPLETED);
+ endModel();
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::processAddFailedHandler() {
+ switch(getNextEvent()) {
+ case UPDATE_FAILED_EVT:
+ case NO_MORE_SERVERS_EVT:
+ LOG_ERROR(dctl_logger, DHCP_DDNS_ADD_FAILED).arg(getNcr()->toText())
+ .arg(getEventLabel(getNextEvent()));
+ setNcrStatus(dhcp_ddns::ST_FAILED);
+ endModel();
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameAddTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameAddTransaction::buildAddFwdAddressRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getForwardDomain());
+
+ // Construct dns::Name from NCR fqdn.
+ dns::Name fqdn(dns::Name(getNcr()->getFqdn()));
+
+ // Content on this request is based on RFC 4703, section 5.3.1
+ // First build the Prerequisite Section.
+
+ // Create 'FQDN Is Not In Use' prerequisite and add it to the
+ // prerequisite section.
+ // Based on RFC 2136, section 2.4.5
+ dns::RRsetPtr prereq(new dns::RRset(fqdn, dns::RRClass::NONE(),
+ dns::RRType::ANY(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Next build the Update Section.
+
+ // Create the FQDN/IP 'add' RR and add it to the to update section.
+ // Based on RFC 2136, section 2.5.1
+ dns::RRsetPtr update(new dns::RRset(fqdn, dns::RRClass::IN(),
+ getAddressRRType(), dns::RRTTL(0)));
+
+ addLeaseAddressRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Now create the FQDN/DHCID 'add' RR and add it to update section.
+ // Based on RFC 2136, section 2.5.1
+ update.reset(new dns::RRset(fqdn, dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ addDhcidRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+void
+NameAddTransaction::buildReplaceFwdAddressRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getForwardDomain());
+
+ // Construct dns::Name from NCR fqdn.
+ dns::Name fqdn(dns::Name(getNcr()->getFqdn()));
+
+ // Content on this request is based on RFC 4703, section 5.3.2
+ // First build the Prerequisite Section.
+
+ // Create an 'FQDN Is In Use' prerequisite and add it to the
+ // pre-requisite section.
+ // Based on RFC 2136, section 2.4.4
+ dns::RRsetPtr prereq(new dns::RRset(fqdn, dns::RRClass::ANY(),
+ dns::RRType::ANY(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Create an DHCID matches prerequisite RR and add it to the
+ // pre-requisite section.
+ // Based on RFC 2136, section 2.4.2.
+ prereq.reset(new dns::RRset(fqdn, dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ addDhcidRdata(prereq);
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Next build the Update Section.
+
+ // Create the FQDN/IP 'delete' RR and add it to the update section.
+ // Based on RFC 2136, section 2.5.2
+ dns::RRsetPtr update(new dns::RRset(fqdn, dns::RRClass::ANY(),
+ getAddressRRType(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Create the FQDN/IP 'add' RR and add it to the update section.
+ // Based on RFC 2136, section 2.5.1
+ update.reset(new dns::RRset(fqdn, dns::RRClass::IN(),
+ getAddressRRType(), dns::RRTTL(0)));
+ addLeaseAddressRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+void
+NameAddTransaction::buildReplaceRevPtrsRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getReverseDomain());
+
+ // Create the reverse IP address "FQDN".
+ std::string rev_addr = D2CfgMgr::reverseIpAddress(getNcr()->getIpAddress());
+ dns::Name rev_ip(rev_addr);
+
+ // Content on this request is based on RFC 4703, section 5.4
+ // Reverse replacement has no prerequisites so straight on to
+ // building the Update section.
+
+ // Create the PTR 'delete' RR and add it to update section.
+ dns::RRsetPtr update(new dns::RRset(rev_ip, dns::RRClass::ANY(),
+ dns::RRType::PTR(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Create the DHCID 'delete' RR and add it to the update section.
+ update.reset(new dns::RRset(rev_ip, dns::RRClass::ANY(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Create the FQDN/IP PTR 'add' RR, add the FQDN as the PTR Rdata
+ // then add it to update section.
+ update.reset(new dns::RRset(rev_ip, dns::RRClass::IN(),
+ dns::RRType::PTR(), dns::RRTTL(0)));
+ addPtrRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Create the FQDN/IP PTR 'add' RR, add the DHCID Rdata
+ // then add it to update section.
+ update.reset(new dns::RRset(rev_ip, dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ addDhcidRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/nc_add.h b/src/bin/d2/nc_add.h
new file mode 100644
index 0000000..1fe167b
--- /dev/null
+++ b/src/bin/d2/nc_add.h
@@ -0,0 +1,451 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NC_ADD_H
+#define NC_ADD_H
+
+/// @file nc_add.h This file defines the class NameAddTransaction.
+
+#include <d2/nc_trans.h>
+#include <dns/rdata.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the NameAddTransaction encounters a general error.
+class NameAddTransactionError : public isc::Exception {
+public:
+ NameAddTransactionError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Embodies the "life-cycle" required to carry out a DDNS Add update.
+///
+/// NameAddTransaction implements a state machine for adding (or replacing) a
+/// forward and/or reverse DNS mapping. This state machine is based upon the
+/// processing logic described in RFC 4703, Sections 5.3 and 5.4. That logic
+/// may be paraphrased as follows:
+///
+/// @code
+///
+/// If the request includes a forward change:
+/// Select a forward server
+/// Send the server a request to add the forward entry
+/// If the server responds with already in use:
+/// Send a server a request to delete and then add forward entry
+///
+/// If the forward update is unsuccessful:
+/// abandon the update
+///
+/// If the request includes a reverse change:
+/// Select a reverse server
+/// Send a server a request to delete and then add reverse entry
+///
+/// @endcode
+///
+/// This class derives from NameChangeTransaction from which it inherits
+/// states, events, and methods common to NameChangeRequest processing.
+class NameAddTransaction : public NameChangeTransaction {
+public:
+
+ //@{ Additional states needed for NameAdd state model.
+ /// @brief State that attempts to add forward address records.
+ static const int ADDING_FWD_ADDRS_ST = NCT_DERIVED_STATE_MIN + 1;
+
+ /// @brief State that attempts to replace forward address records.
+ static const int REPLACING_FWD_ADDRS_ST = NCT_DERIVED_STATE_MIN + 2;
+
+ /// @brief State that attempts to replace reverse PTR records
+ static const int REPLACING_REV_PTRS_ST = NCT_DERIVED_STATE_MIN + 3;
+ //@}
+
+ //@{ Additional events needed for NameAdd state model.
+ /// @brief Event sent when an add attempt fails with address in use.
+ static const int FQDN_IN_USE_EVT = NCT_DERIVED_EVENT_MIN + 1;
+
+ /// @brief Event sent when replace attempt to fails with address not in use.
+ static const int FQDN_NOT_IN_USE_EVT = NCT_DERIVED_EVENT_MIN + 2;
+ //@}
+
+ /// @brief Constructor
+ ///
+ /// Instantiates an Add transaction that is ready to be started.
+ ///
+ /// @param io_service IO service to be used for IO processing
+ /// @param ncr is the NameChangeRequest to fulfill
+ /// @param forward_domain is the domain to use for forward DNS updates
+ /// @param reverse_domain is the domain to use for reverse DNS updates
+ ///
+ /// @throw NameAddTransaction error if given request is not a CHG_ADD,
+ /// NameChangeTransaction error for base class construction errors.
+ NameAddTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain);
+
+ /// @brief Destructor
+ virtual ~NameAddTransaction();
+
+protected:
+ /// @brief Adds events defined by NameAddTransaction to the event set.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then defines the
+ /// events unique to NCR Add transaction processing.
+ ///
+ /// @throw StateModelError if an event definition is invalid or a duplicate.
+ virtual void defineEvents();
+
+ /// @brief Validates the contents of the set of events.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then verifies the
+ /// Add transaction's. This tests that the needed events are in the event
+ /// dictionary.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyEvents();
+
+ /// @brief Adds states defined by NameAddTransaction to the state set.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then defines the
+ /// states unique to NCR Add transaction processing.
+ ///
+ /// @throw StateModelError if an state definition is invalid or a duplicate.
+ virtual void defineStates();
+
+ /// @brief Validates the contents of the set of states.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then verifies the
+ /// Add transaction's states. This tests that the needed states are in the
+ /// state dictionary.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyStates();
+
+ /// @brief State handler for READY_ST.
+ ///
+ /// Entered from:
+ /// - INIT_ST with next event of START_EVT
+ ///
+ /// The READY_ST is the state the model transitions into when the inherited
+ /// method, startTransaction() is invoked. This handler, therefore, is the
+ /// entry point into the state model execution.h Its primary task is to
+ /// determine whether to start with a forward DNS change or a reverse DNS
+ /// change.
+ ///
+ /// Transitions to:
+ /// - SELECTING_FWD_SERVER_ST with next event of SERVER_SELECT_ST if request
+ /// includes a forward change.
+ ///
+ /// - SELECTING_REV_SERVER_ST with next event of SERVER_SELECT_ST if request
+ /// includes only a reverse change.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not
+ /// START_EVT.
+ void readyHandler();
+
+ /// @brief State handler for SELECTING_FWD_SERVER_ST.
+ ///
+ /// Entered from:
+ /// - READY_ST with next event of SELECT_SERVER_EVT
+ /// - ADDING_FWD_ADDRS_ST with next event of SERVER_IO_ERROR_EVT
+ /// - REPLACING_FWD_ADDRS_ST with next event of SERVER_IO_ERROR_EVT
+ ///
+ /// Selects the server to be used from the forward domain for the forward
+ /// DNS update. If next event is SELECT_SERVER_EVT the handler initializes
+ /// the forward domain's server selection mechanism and then attempts to
+ /// select the next server. If next event is SERVER_IO_ERROR_EVT then the
+ /// handler simply attempts to select the next server.
+ ///
+ /// Transitions to:
+ /// - ADDING_FWD_ADDRS_ST with next event of SERVER_SELECTED upon successful
+ /// server selection
+ ///
+ /// - PROCESS_TRANS_FAILED with next event of NO_MORE_SERVERS_EVT upon
+ /// failure to select a server
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not
+ /// SELECT_SERVER_EVT or SERVER_IO_ERROR_EVT.
+ void selectingFwdServerHandler();
+
+ /// @brief State handler for SELECTING_REV_SERVER_ST.
+ ///
+ /// Entered from:
+ /// - READY_ST with next event of SELECT_SERVER_EVT
+ /// - ADDING_FWD_ADDRS_ST with next event of SELECT_SERVER_EVT
+ /// - REPLACING_FWD_ADDRS_ST with next event of SELECT_SERVER_EVT
+ /// - REPLACING_REV_PTRS_ST with next event of SERVER_IO_ERROR_EVT
+ ///
+ /// Selects the server to be used from the reverse domain for the reverse
+ /// DNS update. If next event is SELECT_SERVER_EVT the handler initializes
+ /// the reverse domain's server selection mechanism and then attempts to
+ /// select the next server. If next event is SERVER_IO_ERROR_EVT then the
+ /// handler simply attempts to select the next server.
+ ///
+ /// Transitions to:
+ /// - ADDING_REV_PTRS_ST with next event of SERVER_SELECTED upon successful
+ /// server selection
+ ///
+ /// - PROCESS_TRANS_FAILED with next event of NO_MORE_SERVERS_EVT upon
+ /// failure to select a server
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not
+ /// SELECT_SERVER_EVT or SERVER_IO_ERROR_EVT.
+ void selectingRevServerHandler();
+
+ /// @brief State handler for ADD_FWD_ADDRS_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_FWD_SERVER with next event of SERVER_SELECTED_EVT
+ /// - REPLACING_FWD_ADDRS_ST with next event of SERVER_SELECTED_EVT
+ ///
+ /// Attempts to add a forward DNS entry for a given FQDN. If this is
+ /// first invocation of the handler after transitioning into this state,
+ /// any previous update request context is deleted. If next event
+ /// is SERVER_SELECTED_EVT, the handler builds the forward add request,
+ /// schedules an asynchronous send via sendUpdate(), and returns. Note
+ /// that sendUpdate will post NOP_EVT as next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPLETED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - SELECTING_REV_SERVER_ST with next event of SELECT_SERVER_EVT upon
+ /// successful addition and the request includes a reverse DNS update.
+ ///
+ /// - PROCESS_TRANS_OK_ST with next event of UPDATE_OK_EVT upon successful
+ /// addition and no reverse DNS update is required.
+ ///
+ /// - REPLACING_FWD_ADDRS_ST with next event of FQDN_IN_USE_EVT if the DNS
+ /// server response indicates that an entry for the given FQDN already
+ /// exists.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with next event of UPDATE_FAILED_EVT if the
+ /// DNS server rejected the update for any other reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this states with next event of SERVER_SELECTED_EVT_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - SELECTING_FWD_SERVER_ST with next event of SERVER_IO_ERROR_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has been exhausted.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not
+ /// SERVER_SELECTED_EVT or IO_COMPLETE_EVT.
+ void addingFwdAddrsHandler();
+
+ /// @brief State handler for REPLACING_FWD_ADDRS_ST.
+ ///
+ /// Entered from:
+ /// - ADDING_FWD_ADDRS_ST with next event of FQDN_IN_USE_EVT
+ ///
+ /// Attempts to delete and then add a forward DNS entry for a given
+ /// FQDN. If this is first invocation of the handler after transitioning
+ /// into this state, any previous update request context is deleted. If
+ /// next event is FDQN_IN_USE_EVT or SERVER_SELECTED_EVT, the handler
+ /// builds the forward replacement request, schedules an asynchronous send
+ /// via sendUpdate(), and returns. Note that sendUpdate will post NOP_EVT
+ /// as the next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPLETED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - SELECTING_REV_SERVER_ST with a next event of SELECT_SERVER_EVT upon
+ /// successful replacement and the request includes a reverse DNS update.
+ ///
+ /// - PROCESS_TRANS_OK_ST with next event of UPDATE_OK_EVT upon successful
+ /// replacement and the request does not include a reverse DNS update.
+ ///
+ /// - ADDING_FWD_ADDR_STR with a next event of SERVER_SELECTED_EVT if the
+ /// DNS server response indicates that the FQDN is not in use. This could
+ /// occur if a previous add attempt indicated the FQDN was in use, but
+ /// that entry has since been removed by another entity prior to this
+ /// replacement attempt.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with a next event of UPDATE_FAILED_EVT if the
+ /// DNS server rejected the update for any other reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this state with a next event of SERVER_SELECTED_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - SELECTING_FWD_SERVER_ST with next event of SERVER_IO_ERROR_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has been exhausted.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not:
+ /// FQDN_IN_USE_EVT, SERVER_SELECTED_EVT or IO_COMPLETE_EVT.
+ void replacingFwdAddrsHandler();
+
+ /// @brief State handler for REPLACING_REV_PTRS_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_REV_SERVER_ST with a next event of SERVER_SELECTED_EVT
+ ///
+ /// Attempts to delete and then add a reverse DNS entry for a given FQDN.
+ /// If this is first invocation of the handler after transitioning into
+ /// this state, any previous update request context is deleted. If next
+ /// event is SERVER_SELECTED_EVT, the handler builds the reverse replacement
+ /// add request, schedules an asynchronous send via sendUpdate(), and
+ /// returns. Note that sendUpdate will post NOP_EVT as next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPLETED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - PROCESS_TRANS_OK_ST with a next event of UPDATE_OK_EVT upon
+ /// successful replacement.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with a next event of UPDATE_FAILED_EVT If the
+ /// DNS server rejected the update for any reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this state with a next event of SERVER_SELECTED_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - SELECTING_REV_SERVER_ST with next event of SERVER_IO_ERROR_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has been exhausted.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not:
+ /// SERVER_SELECTED_EVT or IO_COMPLETED_EVT
+ void replacingRevPtrsHandler();
+
+ /// @brief State handler for PROCESS_TRANS_OK_ST.
+ ///
+ /// Entered from:
+ /// - ADDING_FWD_ADDRS_ST with a next event of UPDATE_OK_EVT
+ /// - REPLACING_FWD_ADDRS_ST with a next event of UPDATE_OK_EVT
+ /// - REPLACING_REV_PTRS_ST with a next event of UPDATE_OK_EVT
+ ///
+ /// Sets the transaction action status to indicate success and ends
+ /// model execution.
+ ///
+ /// Transitions to:
+ /// - END_ST with a next event of END_EVT.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not:
+ /// UPDATE_OK_EVT
+ void processAddOkHandler();
+
+ /// @brief State handler for PROCESS_TRANS_FAILED_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_FWD_SERVER_ST with a next event of NO_MORE_SERVERS
+ /// - ADDING_FWD_ADDRS_ST with a next event of UPDATE_FAILED_EVT
+ /// - REPLACING_FWD_ADDRS_ST with a next event of UPDATE_FAILED_EVT
+ /// - SELECTING_REV_SERVER_ST with a next event of NO_MORE_SERVERS
+ /// - REPLACING_REV_PTRS_ST with a next event of UPDATE_FAILED_EVT
+ ///
+ /// Sets the transaction status to indicate failure and ends
+ /// model execution.
+ ///
+ /// Transitions to:
+ /// - END_ST with a next event of FAIL_EVT.
+ ///
+ /// @throw NameAddTransactionError if upon entry next event is not:
+ /// UPDATE_FAILED_EVT
+ void processAddFailedHandler();
+
+ /// @brief Builds a DNS request to add an forward DNS entry for an FQDN
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for adding a
+ /// forward DNS mapping. Once constructed, the request is stored as
+ /// the transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.3.1:
+ ///
+ /// Prerequisite RRsets:
+ /// 1. An assertion that the FQDN does not exist
+ ///
+ /// Updates RRsets:
+ /// 1. An FQDN/IP RR addition (type A for IPv4, AAAA for IPv6)
+ /// 2. An FQDN/DHCID RR addition (type DHCID)
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildAddFwdAddressRequest();
+
+ /// @brief Builds a DNS request to replace forward DNS entry for an FQDN
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for replacing a
+ /// forward DNS mapping. Once constructed, the request is stored as
+ /// the transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.3.2:
+ ///
+ /// Prerequisite RRsets:
+ /// 1. An assertion that the FQDN is in use
+ /// 2. An assertion that the FQDN/DHCID RR exists for the lease client's
+ /// DHCID.
+ ///
+ /// Updates RRsets:
+ /// 1. A deletion of any existing FQDN RRs (type A for IPv4, AAAA for IPv6)
+ /// 2. A FQDN/IP RR addition (type A for IPv4, AAAA for IPv6)
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildReplaceFwdAddressRequest();
+
+ /// @brief Builds a DNS request to replace a reverse DNS entry for an FQDN
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for replacing a
+ /// reverse DNS mapping. Once constructed, the request is stored as
+ /// the transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.4:
+ ///
+ /// Prerequisite RRsets:
+ /// - There are not prerequisites.
+ ///
+ /// Updates RRsets:
+ /// 1. A delete of any existing PTR RRs for the lease address
+ /// 2. A delete of any existing DHCID RRs for the lease address
+ /// 3. A PTR RR addition for the lease address and FQDN
+ /// 4. A DHCID RR addition for the lease address and lease client DHCID
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildReplaceRevPtrsRequest();
+};
+
+/// @brief Defines a pointer to a NameAddTransaction.
+typedef boost::shared_ptr<NameAddTransaction> NameAddTransactionPtr;
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/nc_remove.cc b/src/bin/d2/nc_remove.cc
new file mode 100644
index 0000000..770843e
--- /dev/null
+++ b/src/bin/d2/nc_remove.cc
@@ -0,0 +1,695 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/nc_remove.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+
+namespace isc {
+namespace d2 {
+
+
+// NameRemoveTransaction states
+const int NameRemoveTransaction::REMOVING_FWD_ADDRS_ST;
+const int NameRemoveTransaction::REMOVING_FWD_RRS_ST;
+const int NameRemoveTransaction::REMOVING_REV_PTRS_ST;
+
+// NameRemoveTransaction events
+// Currently NameRemoveTransaction does not define any events.
+
+NameRemoveTransaction::
+NameRemoveTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain)
+ : NameChangeTransaction(io_service, ncr, forward_domain, reverse_domain) {
+ if (ncr->getChangeType() != isc::dhcp_ddns::CHG_REMOVE) {
+ isc_throw (NameRemoveTransactionError,
+ "NameRemoveTransaction, request type must be CHG_REMOVE");
+ }
+}
+
+NameRemoveTransaction::~NameRemoveTransaction(){
+}
+
+void
+NameRemoveTransaction::defineEvents() {
+ // Call superclass impl first.
+ NameChangeTransaction::defineEvents();
+
+ // Define NameRemoveTransaction events.
+ // Currently NameRemoveTransaction does not define any events.
+ // defineEvent(TBD_EVENT, "TBD_EVT");
+}
+
+void
+NameRemoveTransaction::verifyEvents() {
+ // Call superclass implementation first to verify its events. These are
+ // events common to all transactions, and they must be defined.
+ // SELECT_SERVER_EVT
+ // SERVER_SELECTED_EVT
+ // SERVER_IO_ERROR_EVT
+ // NO_MORE_SERVERS_EVT
+ // IO_COMPLETED_EVT
+ // UPDATE_OK_EVT
+ // UPDATE_FAILED_EVT
+ NameChangeTransaction::verifyEvents();
+
+ // Verify NameRemoveTransaction events by attempting to fetch them.
+ // Currently NameRemoveTransaction does not define any events.
+ // getEvent(TBD_EVENT);
+}
+
+void
+NameRemoveTransaction::defineStates() {
+ // Call superclass impl first.
+ NameChangeTransaction::defineStates();
+
+ // Define NameRemoveTransaction states.
+ defineState(READY_ST, "READY_ST",
+ boost::bind(&NameRemoveTransaction::readyHandler, this));
+
+ defineState(SELECTING_FWD_SERVER_ST, "SELECTING_FWD_SERVER_ST",
+ boost::bind(&NameRemoveTransaction::selectingFwdServerHandler,
+ this));
+
+ defineState(SELECTING_REV_SERVER_ST, "SELECTING_REV_SERVER_ST",
+ boost::bind(&NameRemoveTransaction::selectingRevServerHandler,
+ this));
+
+ defineState(REMOVING_FWD_ADDRS_ST, "REMOVING_FWD_ADDRS_ST",
+ boost::bind(&NameRemoveTransaction::removingFwdAddrsHandler,
+ this));
+
+ defineState(REMOVING_FWD_RRS_ST, "REMOVING_FWD_RRS_ST",
+ boost::bind(&NameRemoveTransaction::removingFwdRRsHandler,
+ this));
+
+ defineState(REMOVING_REV_PTRS_ST, "REMOVING_REV_PTRS_ST",
+ boost::bind(&NameRemoveTransaction::removingRevPtrsHandler,
+ this));
+
+ defineState(PROCESS_TRANS_OK_ST, "PROCESS_TRANS_OK_ST",
+ boost::bind(&NameRemoveTransaction::processRemoveOkHandler,
+ this));
+
+ defineState(PROCESS_TRANS_FAILED_ST, "PROCESS_TRANS_FAILED_ST",
+ boost::bind(&NameRemoveTransaction::processRemoveFailedHandler,
+ this));
+}
+
+void
+NameRemoveTransaction::verifyStates() {
+ // Call superclass implementation first to verify its states. These are
+ // states common to all transactions, and they must be defined.
+ // READY_ST
+ // SELECTING_FWD_SERVER_ST
+ // SELECTING_REV_SERVER_ST
+ // PROCESS_TRANS_OK_ST
+ // PROCESS_TRANS_FAILED_ST
+ NameChangeTransaction::verifyStates();
+
+ // Verify NameRemoveTransaction states by attempting to fetch them.
+ getState(REMOVING_FWD_ADDRS_ST);
+ getState(REMOVING_FWD_RRS_ST);
+ getState(REMOVING_REV_PTRS_ST);
+}
+
+void
+NameRemoveTransaction::readyHandler() {
+ switch(getNextEvent()) {
+ case START_EVT:
+ if (getForwardDomain()) {
+ // Request includes a forward change, do that first.
+ transition(SELECTING_FWD_SERVER_ST, SELECT_SERVER_EVT);
+ } else {
+ // Reverse change only, transition accordingly.
+ transition(SELECTING_REV_SERVER_ST, SELECT_SERVER_EVT);
+ }
+
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameRemoveTransaction::selectingFwdServerHandler() {
+ switch(getNextEvent()) {
+ case SELECT_SERVER_EVT:
+ // First time through for this transaction, so initialize server
+ // selection.
+ initServerSelection(getForwardDomain());
+ break;
+ case SERVER_IO_ERROR_EVT:
+ // We failed to communicate with current server. Attempt to select
+ // another one below.
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+
+ // Select the next server from the list of forward servers.
+ if (selectNextServer()) {
+ // We have a server to try.
+ transition(REMOVING_FWD_ADDRS_ST, SERVER_SELECTED_EVT);
+ }
+ else {
+ // Server list is exhausted, so fail the transaction.
+ transition(PROCESS_TRANS_FAILED_ST, NO_MORE_SERVERS_EVT);
+ }
+}
+
+void
+NameRemoveTransaction::removingFwdAddrsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildRemoveFwdAddressRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_FORWARD_REMOVE_ADDRS_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ if ((rcode == dns::Rcode::NOERROR()) ||
+ (rcode == dns::Rcode::NXDOMAIN())) {
+ // We were able to remove it or it wasn't there, now we
+ // need to remove any other RRs for this FQDN.
+ transition(REMOVING_FWD_RRS_ST, UPDATE_OK_EVT);
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should we try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_ADDRS_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_ADDRS_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_ADDRS_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ retryTransition(SELECTING_FWD_SERVER_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_FORWARD_REMOVE_ADDRS_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+
+void
+NameRemoveTransaction::removingFwdRRsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case UPDATE_OK_EVT:
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildRemoveFwdRRsRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_FORWARD_REMOVE_RRS_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ // @todo Not sure if NXDOMAIN is ok here, but I think so.
+ // A Rcode of NXDOMAIN would mean there are no RRs for the FQDN,
+ // which is fine. We were asked to delete them, they are not there
+ // so all is well.
+ if ((rcode == dns::Rcode::NOERROR()) ||
+ (rcode == dns::Rcode::NXDOMAIN())) {
+ // We were able to remove the forward mapping. Mark it as done.
+ setForwardChangeCompleted(true);
+
+ // If request calls for reverse update then do that next,
+ // otherwise we can process ok.
+ if (getReverseDomain()) {
+ transition(SELECTING_REV_SERVER_ST, SELECT_SERVER_EVT);
+ } else {
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ }
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_RRS_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_RRS_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ // @note If we exhaust the IO retries for the current server
+ // due to IO failures, we will abort the remaining updates.
+ // The rational is that we are only in this state, if the remove
+ // of the forward address RR succeeded (removingFwdAddrsHandler)
+ // on the current server. Therefore we should not attempt another
+ // removal on a different server. This is perhaps a point
+ // for discussion.
+ // @todo Should we go ahead with the reverse remove?
+ retryTransition(PROCESS_TRANS_FAILED_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_FORWARD_REMOVE_RRS_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ // If we are out of retries on this server abandon the transaction.
+ // (Same logic as the case for TIMEOUT above).
+ retryTransition(PROCESS_TRANS_FAILED_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_FORWARD_REMOVE_RRS_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+
+void
+NameRemoveTransaction::selectingRevServerHandler() {
+ switch(getNextEvent()) {
+ case SELECT_SERVER_EVT:
+ // First time through for this transaction, so initialize server
+ // selection.
+ initServerSelection(getReverseDomain());
+ break;
+ case SERVER_IO_ERROR_EVT:
+ // We failed to communicate with current server. Attempt to select
+ // another one below.
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+
+ // Select the next server from the list of forward servers.
+ if (selectNextServer()) {
+ // We have a server to try.
+ transition(REMOVING_REV_PTRS_ST, SERVER_SELECTED_EVT);
+ }
+ else {
+ // Server list is exhausted, so fail the transaction.
+ transition(PROCESS_TRANS_FAILED_ST, NO_MORE_SERVERS_EVT);
+ }
+}
+
+
+void
+NameRemoveTransaction::removingRevPtrsHandler() {
+ if (doOnEntry()) {
+ // Clear the request on initial transition. This allows us to reuse
+ // the request on retries if necessary.
+ clearDnsUpdateRequest();
+ }
+
+ switch(getNextEvent()) {
+ case SERVER_SELECTED_EVT:
+ if (!getDnsUpdateRequest()) {
+ // Request hasn't been constructed yet, so build it.
+ try {
+ buildRemoveRevPtrsRequest();
+ } catch (const std::exception& ex) {
+ // While unlikely, the build might fail if we have invalid
+ // data. Should that be the case, we need to fail the
+ // transaction.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REMOVE_BUILD_FAILURE)
+ .arg(getNcr()->toText())
+ .arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ }
+ }
+
+ // Call sendUpdate() to initiate the async send. Note it also sets
+ // next event to NOP_EVT.
+ sendUpdate();
+ break;
+
+ case IO_COMPLETED_EVT: {
+ switch (getDnsUpdateStatus()) {
+ case DNSClient::SUCCESS: {
+ // We successfully received a response packet from the server.
+ const dns::Rcode& rcode = getDnsUpdateResponse()->getRcode();
+ if ((rcode == dns::Rcode::NOERROR()) ||
+ (rcode == dns::Rcode::NXDOMAIN())) {
+ // We were able to update the reverse mapping. Mark it as done.
+ // @todo For now we are also treating NXDOMAIN as success.
+ setReverseChangeCompleted(true);
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ } else {
+ // Per RFC4703 any other value means cease.
+ // If we get not authorized should try the next server in
+ // the list? @todo This needs some discussion perhaps.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REMOVE_REJECTED)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn())
+ .arg(rcode.getCode());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+
+ break;
+ }
+
+ case DNSClient::TIMEOUT:
+ case DNSClient::OTHER:
+ // We couldn't send to the current server, log it and set up
+ // to select the next server for a retry.
+ // @note For now we treat OTHER as an IO error like TIMEOUT. It
+ // is not entirely clear if this is accurate.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REMOVE_IO_ERROR)
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_REV_SERVER_ST);
+ break;
+
+ case DNSClient::INVALID_RESPONSE:
+ // A response was received but was corrupt. Retry it like an IO
+ // error.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REVERSE_REMOVE_RESP_CORRUPT)
+ .arg(getCurrentServer()->toText())
+ .arg(getNcr()->getFqdn());
+
+ // If we are out of retries on this server, we go back and start
+ // all over on a new server.
+ retryTransition(SELECTING_REV_SERVER_ST);
+ break;
+
+ default:
+ // Any other value and we will fail this transaction, something
+ // bigger is wrong.
+ LOG_ERROR(dctl_logger,
+ DHCP_DDNS_REVERSE_REMOVE_BAD_DNSCLIENT_STATUS)
+ .arg(getDnsUpdateStatus())
+ .arg(getNcr()->getFqdn())
+ .arg(getCurrentServer()->toText());
+
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ break;
+ } // end switch on dns_status
+
+ break;
+ } // end case IO_COMPLETE_EVT
+
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+
+void
+NameRemoveTransaction::processRemoveOkHandler() {
+ switch(getNextEvent()) {
+ case UPDATE_OK_EVT:
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL, DHCP_DDNS_REMOVE_SUCCEEDED)
+ .arg(getNcr()->toText());
+ setNcrStatus(dhcp_ddns::ST_COMPLETED);
+ endModel();
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameRemoveTransaction::processRemoveFailedHandler() {
+ switch(getNextEvent()) {
+ case UPDATE_FAILED_EVT:
+ case NO_MORE_SERVERS_EVT:
+ case SERVER_IO_ERROR_EVT:
+ LOG_ERROR(dctl_logger, DHCP_DDNS_REMOVE_FAILED).arg(getNcr()->toText())
+ .arg(getEventLabel(getNextEvent()));
+ setNcrStatus(dhcp_ddns::ST_FAILED);
+ endModel();
+ break;
+ default:
+ // Event is invalid.
+ isc_throw(NameRemoveTransactionError,
+ "Wrong event for context: " << getContextStr());
+ }
+}
+
+void
+NameRemoveTransaction::buildRemoveFwdAddressRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getForwardDomain());
+
+ // Content on this request is based on RFC 4703, section 5.5, paragraph 4.
+ // Construct dns::Name from NCR fqdn.
+ dns::Name fqdn(dns::Name(getNcr()->getFqdn()));
+ // First build the Prerequisite Section
+
+ // Create an DHCID matches prerequisite RR and add it to the
+ // pre-requisite section
+ // Based on RFC 2136, section 2.4.2.
+ dns::RRsetPtr prereq(new dns::RRset(fqdn, dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ addDhcidRdata(prereq);
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Next build the Update Section
+
+ // Create the FQDN/IP 'delete' RR and add it to the update section.
+ // Add the RR to update section.
+ // Based on 2136 section 2.5.4
+ dns::RRsetPtr update(new dns::RRset(fqdn, dns::RRClass::NONE(),
+ getAddressRRType(), dns::RRTTL(0)));
+ addLeaseAddressRdata(update);
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+void
+NameRemoveTransaction::buildRemoveFwdRRsRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getForwardDomain());
+
+ // Construct dns::Name from NCR fqdn.
+ dns::Name fqdn(dns::Name(getNcr()->getFqdn()));
+
+ // Content on this request is based on RFC 4703, section 5.5, paragraph 5.
+ // First build the Prerequisite Section.
+
+ // Now create an DHCID matches prerequisite RR.
+ // Set the RR's RData to DHCID.
+ // Add it to the pre-requisite section.
+ // Based on RFC 2136, section 2.4.2.
+ dns::RRsetPtr prereq(new dns::RRset(fqdn, dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ addDhcidRdata(prereq);
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Create an assertion that there are no A RRs for the FQDN.
+ // Add it to the pre-reqs.
+ // Based on RFC 2136, section 2.4.3.
+ prereq.reset(new dns::RRset(fqdn, dns::RRClass::NONE(),
+ dns::RRType::A(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Create an assertion that there are no A RRs for the FQDN.
+ // Add it to the pre-reqs.
+ // Based on RFC 2136, section 2.4.3.
+ prereq.reset(new dns::RRset(fqdn, dns::RRClass::NONE(),
+ dns::RRType::AAAA(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Next build the Update Section.
+
+ // Create the 'delete' of all RRs for FQDN.
+ // Set the message RData to lease address.
+ // Add the RR to update section.
+ // Based on RFC 2136, section 2.5.3.
+ dns::RRsetPtr update(new dns::RRset(fqdn, dns::RRClass::ANY(),
+ dns::RRType::ANY(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+void
+NameRemoveTransaction::buildRemoveRevPtrsRequest() {
+ // Construct an empty request.
+ D2UpdateMessagePtr request = prepNewRequest(getReverseDomain());
+
+ // Create the reverse IP address "FQDN".
+ std::string rev_addr = D2CfgMgr::reverseIpAddress(getNcr()->getIpAddress());
+ dns::Name rev_ip(rev_addr);
+
+ // Content on this request is based on RFC 4703, section 5.5, paragraph 2.
+ // First build the Prerequisite Section.
+ // (Note that per RFC 4703, section 5.4, there is no need to validate
+ // DHCID RR for PTR entries.)
+
+ // Create an assertion that the PTRDNAME in the PTR record matches the
+ // client's FQDN for the address that was released.
+ // Based on RFC 2136, section 3.2.3
+ dns::RRsetPtr prereq(new dns::RRset(rev_ip, dns::RRClass::IN(),
+ dns::RRType::PTR(), dns::RRTTL(0)));
+ addPtrRdata(prereq);
+ request->addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq);
+
+ // Now, build the Update section.
+
+ // Create a delete of any RRs for the FQDN and add it to update section.
+ // Based on RFC 2136, section 3.4.2.3
+ dns::RRsetPtr update(new dns::RRset(rev_ip, dns::RRClass::ANY(),
+ dns::RRType::ANY(), dns::RRTTL(0)));
+ request->addRRset(D2UpdateMessage::SECTION_UPDATE, update);
+
+ // Set the transaction's update request to the new request.
+ setDnsUpdateRequest(request);
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/nc_remove.h b/src/bin/d2/nc_remove.h
new file mode 100644
index 0000000..f7b0dcc
--- /dev/null
+++ b/src/bin/d2/nc_remove.h
@@ -0,0 +1,435 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NC_REMOVE_H
+#define NC_REMOVE_H
+
+/// @file nc_remove.h This file defines the class NameRemoveTransaction.
+
+#include <d2/nc_trans.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the NameRemoveTransaction encounters a general error.
+class NameRemoveTransactionError : public isc::Exception {
+public:
+ NameRemoveTransactionError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Embodies the "life-cycle" required to carry out a DDNS Remove update.
+///
+/// NameRemoveTransaction implements a state machine for removing a forward
+/// and/or reverse DNS mappings. This state machine is based upon the processing
+/// logic described in RFC 4703, Section 5.5. That logic may be paraphrased as
+/// follows:
+///
+/// @code
+///
+/// If the request includes a forward change:
+/// Select a forward server
+/// Send the server a request to remove client's specific forward address RR
+/// If it succeeds or the server responds with name no longer in use
+/// Send a server a request to delete any other RRs for that FQDN, such
+/// as the DHCID RR.
+/// otherwise
+/// abandon the update
+///
+/// If the request includes a reverse change:
+/// Select a reverse server
+/// Send a server a request to delete reverse entry (PTR RR)
+///
+/// @endcode
+///
+/// This class derives from NameChangeTransaction from which it inherits
+/// states, events, and methods common to NameChangeRequest processing.
+class NameRemoveTransaction : public NameChangeTransaction {
+public:
+
+ //@{ Additional states needed for NameRemove state model.
+ /// @brief State that attempts to remove specific forward address record.
+ static const int REMOVING_FWD_ADDRS_ST = NCT_DERIVED_STATE_MIN + 1;
+
+ /// @brief State that attempts to remove any other forward RRs for the DHCID
+ static const int REMOVING_FWD_RRS_ST = NCT_DERIVED_STATE_MIN + 2;
+
+ /// @brief State that attempts to remove reverse PTR records
+ static const int REMOVING_REV_PTRS_ST = NCT_DERIVED_STATE_MIN + 3;
+ //@}
+
+ //@{ Additional events needed for NameRemove state model.
+ /// @brief Event sent when replace attempt to fails with address not in use.
+ /// @todo Currently none have been identified.
+ //@}
+
+ /// @brief Constructor
+ ///
+ /// Instantiates an Remove transaction that is ready to be started.
+ ///
+ /// @param io_service IO service to be used for IO processing
+ /// @param ncr is the NameChangeRequest to fulfill
+ /// @param forward_domain is the domain to use for forward DNS updates
+ /// @param reverse_domain is the domain to use for reverse DNS updates
+ ///
+ /// @throw NameRemoveTransaction error if given request is not a CHG_REMOVE,
+ /// NameChangeTransaction error for base class construction errors.
+ NameRemoveTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain);
+
+ /// @brief Destructor
+ virtual ~NameRemoveTransaction();
+
+protected:
+ /// @brief Adds events defined by NameRemoveTransaction to the event set.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then defines the
+ /// events unique to NCR Remove transaction processing.
+ ///
+ /// @throw StateModelError if an event definition is invalid or a duplicate.
+ virtual void defineEvents();
+
+ /// @brief Validates the contents of the set of events.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then verifies the
+ /// Remove transaction's events. This tests that the needed events are in
+ /// the event dictionary.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyEvents();
+
+ /// @brief Adds states defined by NameRemoveTransaction to the state set.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then defines the
+ /// states unique to NCR Remove transaction processing.
+ ///
+ /// @throw StateModelError if an state definition is invalid or a duplicate.
+ virtual void defineStates();
+
+ /// @brief Validates the contents of the set of states.
+ ///
+ /// Invokes NameChangeTransaction's implementation and then verifies the
+ /// Remove transaction's states. This tests that the needed states are in
+ /// the state dictionary.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyStates();
+
+ /// @brief State handler for READY_ST.
+ ///
+ /// Entered from:
+ /// - INIT_ST with next event of START_EVT
+ ///
+ /// The READY_ST is the state the model transitions into when the inherited
+ /// method, startTransaction() is invoked. This handler, therefore, is the
+ /// entry point into the state model execution. Its primary task is to
+ /// determine whether to start with a forward DNS change or a reverse DNS
+ /// change.
+ ///
+ /// Transitions to:
+ /// - SELECTING_FWD_SERVER_ST with next event of SERVER_SELECT_ST if request
+ /// includes a forward change.
+ ///
+ /// - SELECTING_REV_SERVER_ST with next event of SERVER_SELECT_ST if request
+ /// includes only a reverse change.
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not
+ /// START_EVT.
+ void readyHandler();
+
+ /// @brief State handler for SELECTING_FWD_SERVER_ST.
+ ///
+ /// Entered from:
+ /// - READY_ST with next event of SELECT_SERVER_EVT
+ /// - REMOVING_FWD_ADDRS_ST with next event of SERVER_IO_ERROR_EVT
+ ///
+ /// Selects the server to be used from the forward domain for the forward
+ /// DNS update. If next event is SELECT_SERVER_EVT the handler initializes
+ /// the forward domain's server selection mechanism and then attempts to
+ /// select the next server. If next event is SERVER_IO_ERROR_EVT then the
+ /// handler simply attempts to select the next server.
+ ///
+ /// Transitions to:
+ /// - REMOVING_FWD_ADDRS_ST with next event of SERVER_SELECTED upon
+ /// successful server selection
+ ///
+ /// - PROCESS_TRANS_FAILED with next event of NO_MORE_SERVERS_EVT upon
+ /// failure to select a server
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not
+ /// SELECT_SERVER_EVT or SERVER_IO_ERROR_EVT.
+ void selectingFwdServerHandler();
+
+ /// @brief State handler for SELECTING_REV_SERVER_ST.
+ ///
+ /// Entered from:
+ /// - READY_ST with next event of SELECT_SERVER_EVT
+ /// - REMOVING_FWD_RRS_ST with next event of SELECT_SERVER_EVT
+ /// - REMOVING_REV_PTRS_ST with next event of SERVER_IO_ERROR_EVT
+ ///
+ /// Selects the server to be used from the reverse domain for the reverse
+ /// DNS update. If next event is SELECT_SERVER_EVT the handler initializes
+ /// the reverse domain's server selection mechanism and then attempts to
+ /// select the next server. If next event is SERVER_IO_ERROR_EVT then the
+ /// handler simply attempts to select the next server.
+ ///
+ /// Transitions to:
+ /// - REMOVING_REV_PTRS_ST with next event of SERVER_SELECTED upon
+ /// successful server selection
+ ///
+ /// - PROCESS_TRANS_FAILED with next event of NO_MORE_SERVERS_EVT upon
+ /// failure to select a server
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not
+ /// SELECT_SERVER_EVT or SERVER_IO_ERROR_EVT.
+ void selectingRevServerHandler();
+
+ /// @brief State handler for REMOVING_FWD_ADDRS_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_FWD_SERVER with next event of SERVER_SELECTED_EVT
+ ///
+ /// Attempts to remove the forward DNS entry for a given FQDN, provided
+ /// a DHCID RR exists which matches the requesting DHCID. If this is
+ /// first invocation of the handler after transitioning into this state,
+ /// any previous update request context is deleted. If next event
+ /// is SERVER_SELECTED_EVT, the handler builds the forward remove request,
+ /// schedules an asynchronous send via sendUpdate(), and returns. Note
+ /// that sendUpdate will post NOP_EVT as next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPELTED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - REMOVING_FWD_RRS_ST with next event of UPDATE_OK_EVT upon successful
+ /// removal or RCODE of indication FQDN is no longer in use (NXDOMAIN).
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with next event of UPDATE_FAILED_EVT if the
+ /// DNS server rejected the update for any reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this state with next event of SERVER_SELECTED_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - SELECTING_FWD_SERVER_ST with next event of SERVER_IO_ERROR_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has been exhausted.
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not
+ /// SERVER_SELECTED_EVT or IO_COMPLETE_EVT
+ void removingFwdAddrsHandler();
+
+ /// @brief State handler for REMOVING_FWD_RRS_ST.
+ ///
+ /// Entered from:
+ /// - REMOVING_FWD_ADDRS_ST with next event of UPDATE_OK_EVT
+ ///
+ /// Attempts to delete any remaining RRs associated with the given FQDN
+ /// such as the DHCID RR. If this is first invocation of the handler after
+ /// transitioning into this state, any previous update request context is
+ /// deleted and the handler builds the forward remove request. It then
+ /// schedules an asynchronous send via sendUpdate(),
+ /// and returns. Note that sendUpdate will post NOP_EVT as the next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPELTED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - SELECTING_REV_SERVER_ST with a next event of SELECT_SERVER_EVT upon
+ /// successful completion and the request includes a reverse DNS update.
+ ///
+ /// - PROCESS_TRANS_OK_ST with next event of UPDATE_OK_EVT upon successful
+ /// completion and the request does not include a reverse DNS update.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with a next event of UPDATE_FAILED_EVT if the
+ /// DNS server rejected the update for any other reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this state with a next event of SERVER_SELECTED_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with a next event of SERVER_IO_ERROR_EVT if
+ /// there we have reached maximum number of retries without success on the
+ /// current server.
+ ///
+ /// @note If we exhaust the IO retries for the current server due to IO
+ /// failures, we will abort the remaining updates. The rational is that
+ /// we are only in this state, if the remove of the forward address RR
+ /// succeeded (removingFwdAddrsHandler) on the current server so we should
+ /// not attempt another removal on a different server. This is perhaps a
+ /// point for discussion. @todo Should we go ahead with the reverse remove?
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not:
+ /// UPDATE_OK_EVT or IO_COMPLETE_EVT
+ void removingFwdRRsHandler();
+
+ /// @brief State handler for REMOVING_REV_PTRS_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_REV_SERVER_ST with a next event of SERVER_SELECTED_EVT
+ ///
+ /// Attempts to delete a reverse DNS entry for a given FQDN. If this is
+ /// first invocation of the handler after transitioning into this state,
+ /// any previous update request context is deleted. If next event is
+ /// SERVER_SELECTED_EVT, the handler builds the reverse remove request,
+ /// schedules an asynchronous send via sendUpdate(), and then returns.
+ /// Note that sendUpdate will post NOP_EVT as next event.
+ ///
+ /// Posting the NOP_EVT will cause runModel() to suspend execution of
+ /// the state model thus affecting a "wait" for the update IO to complete.
+ /// Update completion occurs via the DNSClient callback operator() method
+ /// inherited from NameChangeTransaction. When invoked this callback will
+ /// post a next event of IO_COMPLETED_EVT and then invoke runModel which
+ /// resumes execution of the state model.
+ ///
+ /// When the handler is invoked with a next event of IO_COMPELTED_EVT,
+ /// the DNS update status is checked and acted upon accordingly:
+ ///
+ /// Transitions to:
+ /// - PROCESS_TRANS_OK_ST with a next event of UPDATE_OK_EVT upon
+ /// successful completion.
+ ///
+ /// - PROCESS_TRANS_FAILED_ST with a next event of UPDATE_FAILED_EVT If the
+ /// DNS server rejected the update for any reason or the IO completed
+ /// with an unrecognized status.
+ ///
+ /// - RE-ENTER this state with a next event of SERVER_SELECTED_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has not been exhausted.
+ ///
+ /// - SELECTING_REV_SERVER_ST with next event of SERVER_IO_ERROR_EVT if
+ /// there was an IO error communicating with the server and the number of
+ /// per server retries has been exhausted.
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not:
+ /// SERVER_SELECTED_EVT or IO_COMPLETED_EVT
+ void removingRevPtrsHandler();
+
+ /// @brief State handler for PROCESS_TRANS_OK_ST.
+ ///
+ /// Entered from:
+ /// - REMOVING_FWD_RRS_ST with a next event of UPDATE_OK_EVT
+ /// - REMOVING_REV_PTRS_ST with a next event of UPDATE_OK_EVT
+ ///
+ /// Sets the transaction action status to indicate success and ends
+ /// model execution.
+ ///
+ /// Transitions to:
+ /// - END_ST with a next event of END_EVT.
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not:
+ /// UPDATE_OK_EVT
+ void processRemoveOkHandler();
+
+ /// @brief State handler for PROCESS_TRANS_FAILED_ST.
+ ///
+ /// Entered from:
+ /// - SELECTING_FWD_SERVER_ST with a next event of NO_MORE_SERVERS
+ /// - REMOVING_FWD_ADDRS_ST with a next event of UPDATE_FAILED_EVT
+ /// - REMOVING_FWD_RRS_ST with a next event of UPDATE_FAILED_EVT
+ /// - REMOVING_FWD_RRS_ST with a next event of SERVER_IO_ERROR_EVT
+ /// - SELECTING_REV_SERVER_ST with a next event of NO_MORE_SERVERS
+ /// - REMOVING_REV_PTRS_ST with a next event of UPDATE_FAILED_EVT
+ ///
+ /// Sets the transaction status to indicate failure and ends
+ /// model execution.
+ ///
+ /// Transitions to:
+ /// - END_ST with a next event of FAIL_EVT.
+ ///
+ /// @throw NameRemoveTransactionError if upon entry next event is not:
+ /// UPDATE_FAILED_EVT
+ void processRemoveFailedHandler();
+
+ /// @brief Builds a DNS request to remove a forward DNS address for a FQDN.
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for removing a
+ /// forward DNS address mapping. Once constructed, the request is stored as
+ /// the transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.5, paragraph 4.
+ ///
+ /// Prerequisite RRsets:
+ /// 1. An assertion that a matching DHCID RR exists
+ ///
+ /// Updates RRsets:
+ /// 1. A delete of the FQDN/IP RR (type A for IPv4, AAAA for IPv6)
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildRemoveFwdAddressRequest();
+
+ /// @brief Builds a DNS request to remove all forward DNS RRs for a FQDN.
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for removing any
+ /// remaining forward DNS RRs, once all A or AAAA entries for the FQDN
+ /// have been removed. Once constructed, the request is stored as the
+ /// transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.5, paragraph 5.
+ ///
+ /// Prerequisite RRsets:
+ /// 1. An assertion that a matching DHCID RR exists
+ /// 2. An assertion that no A RRs for the FQDN exist
+ /// 3. An assertion that no AAAA RRs for the FQDN exist
+ ///
+ /// Updates RRsets:
+ /// 1. A delete of all RRs for the FQDN
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildRemoveFwdRRsRequest();
+
+ /// @brief Builds a DNS request to remove a reverse DNS entry for a FQDN
+ ///
+ /// Constructs a DNS update request, based upon the NCR, for removing a
+ /// reverse DNS mapping. Once constructed, the request is stored as
+ /// the transaction's DNS update request.
+ ///
+ /// The request content is adherent to RFC 4703 section 5.5, paragraph 2:
+ ///
+ /// Prerequisite RRsets:
+ /// 1. An assertion that a PTR record matching the client's FQDN exists.
+ ///
+ /// Updates RRsets:
+ /// 1. A delete of all RRs for the FQDN
+ ///
+ /// @throw This method does not throw but underlying methods may.
+ void buildRemoveRevPtrsRequest();
+};
+
+/// @brief Defines a pointer to a NameRemoveTransaction.
+typedef boost::shared_ptr<NameRemoveTransaction> NameRemoveTransactionPtr;
+
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/nc_trans.cc b/src/bin/d2/nc_trans.cc
new file mode 100644
index 0000000..08e443d
--- /dev/null
+++ b/src/bin/d2/nc_trans.cc
@@ -0,0 +1,440 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/nc_trans.h>
+#include <dns/rdata.h>
+
+namespace isc {
+namespace d2 {
+
+// Common transaction states
+const int NameChangeTransaction::READY_ST;
+const int NameChangeTransaction::SELECTING_FWD_SERVER_ST;
+const int NameChangeTransaction::SELECTING_REV_SERVER_ST;
+const int NameChangeTransaction::PROCESS_TRANS_OK_ST;
+const int NameChangeTransaction::PROCESS_TRANS_FAILED_ST;
+
+const int NameChangeTransaction::NCT_DERIVED_STATE_MIN;
+
+// Common transaction events
+const int NameChangeTransaction::SELECT_SERVER_EVT;
+const int NameChangeTransaction::SERVER_SELECTED_EVT;
+const int NameChangeTransaction::SERVER_IO_ERROR_EVT;
+const int NameChangeTransaction::NO_MORE_SERVERS_EVT;
+const int NameChangeTransaction::IO_COMPLETED_EVT;
+const int NameChangeTransaction::UPDATE_OK_EVT;
+const int NameChangeTransaction::UPDATE_FAILED_EVT;
+
+const int NameChangeTransaction::NCT_DERIVED_EVENT_MIN;
+
+const unsigned int NameChangeTransaction::DNS_UPDATE_DEFAULT_TIMEOUT;
+const unsigned int NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+
+NameChangeTransaction::
+NameChangeTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain)
+ : io_service_(io_service), ncr_(ncr), forward_domain_(forward_domain),
+ reverse_domain_(reverse_domain), dns_client_(), dns_update_request_(),
+ dns_update_status_(DNSClient::OTHER), dns_update_response_(),
+ forward_change_completed_(false), reverse_change_completed_(false),
+ current_server_list_(), current_server_(), next_server_pos_(0),
+ update_attempts_(0) {
+ // @todo if io_service is NULL we are multi-threading and should
+ // instantiate our own
+ if (!io_service_) {
+ isc_throw(NameChangeTransactionError, "IOServicePtr cannot be null");
+ }
+
+ if (!ncr_) {
+ isc_throw(NameChangeTransactionError,
+ "NameChangeRequest cannot be null");
+ }
+
+ if (ncr_->isForwardChange() && !(forward_domain_)) {
+ isc_throw(NameChangeTransactionError,
+ "Forward change must have a forward domain");
+ }
+
+ if (ncr_->isReverseChange() && !(reverse_domain_)) {
+ isc_throw(NameChangeTransactionError,
+ "Reverse change must have a reverse domain");
+ }
+}
+
+NameChangeTransaction::~NameChangeTransaction(){
+}
+
+void
+NameChangeTransaction::startTransaction() {
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
+ DHCP_DDNS_STARTING_TRANSACTION)
+ .arg(getTransactionKey().toStr());
+
+ setNcrStatus(dhcp_ddns::ST_PENDING);
+ startModel(READY_ST);
+}
+
+void
+NameChangeTransaction::operator()(DNSClient::Status status) {
+ // Stow the completion status and re-enter the run loop with the event
+ // set to indicate IO completed.
+ // runModel is exception safe so we are good to call it here.
+ // It won't exit until we hit the next IO wait or the state model ends.
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
+ DHCP_DDNS_UPDATE_RESPONSE_RECEIVED)
+ .arg(getTransactionKey().toStr())
+ .arg(current_server_->toText())
+ .arg(status);
+
+ setDnsUpdateStatus(status);
+ runModel(IO_COMPLETED_EVT);
+}
+
+void
+NameChangeTransaction::sendUpdate(bool /* use_tsig_ */) {
+ try {
+ ++update_attempts_;
+ // @todo add logic to add/replace TSIG key info in request if
+ // use_tsig_ is true. We should be able to navigate to the TSIG key
+ // for the current server. If not we would need to add that.
+
+ // @todo time out should ultimately be configurable, down to
+ // server level?
+ dns_client_->doUpdate(*io_service_, current_server_->getIpAddress(),
+ current_server_->getPort(), *dns_update_request_,
+ DNS_UPDATE_DEFAULT_TIMEOUT);
+
+ // Message is on its way, so the next event should be NOP_EVT.
+ postNextEvent(NOP_EVT);
+ LOG_DEBUG(dctl_logger, DBGLVL_TRACE_DETAIL,
+ DHCP_DDNS_UPDATE_REQUEST_SENT)
+ .arg(getTransactionKey().toStr())
+ .arg(current_server_->toText());
+ } catch (const std::exception& ex) {
+ // We were unable to initiate the send.
+ // It is presumed that any throw from doUpdate is due to a programmatic
+ // error, such as an unforeseen permutation of data, rather than an IO
+ // failure. IO errors should be caught by the underlying asiolink
+ // mechanisms and manifested as an unsuccessful IO status in the
+ // DNSClient callback. Any problem here most likely means the request
+ // is corrupt in some way and cannot be completed, therefore we will
+ // log it and transition it to failure.
+ LOG_ERROR(dctl_logger, DHCP_DDNS_TRANS_SEND_ERROR).arg(ex.what());
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+}
+
+void
+NameChangeTransaction::defineEvents() {
+ // Call superclass impl first.
+ StateModel::defineEvents();
+
+ // Define NCT events.
+ defineEvent(SELECT_SERVER_EVT, "SELECT_SERVER_EVT");
+ defineEvent(SERVER_SELECTED_EVT, "SERVER_SELECTED_EVT");
+ defineEvent(SERVER_IO_ERROR_EVT, "SERVER_IO_ERROR_EVT");
+ defineEvent(NO_MORE_SERVERS_EVT, "NO_MORE_SERVERS_EVT");
+ defineEvent(IO_COMPLETED_EVT, "IO_COMPLETED_EVT");
+ defineEvent(UPDATE_OK_EVT, "UPDATE_OK_EVT");
+ defineEvent(UPDATE_FAILED_EVT, "UPDATE_FAILED_EVT");
+}
+
+void
+NameChangeTransaction::verifyEvents() {
+ // Call superclass impl first.
+ StateModel::verifyEvents();
+
+ // Verify NCT events.
+ getEvent(SELECT_SERVER_EVT);
+ getEvent(SERVER_SELECTED_EVT);
+ getEvent(SERVER_IO_ERROR_EVT);
+ getEvent(NO_MORE_SERVERS_EVT);
+ getEvent(IO_COMPLETED_EVT);
+ getEvent(UPDATE_OK_EVT);
+ getEvent(UPDATE_FAILED_EVT);
+}
+
+void
+NameChangeTransaction::defineStates() {
+ // Call superclass impl first.
+ StateModel::defineStates();
+ // This class is "abstract" in that it does not supply handlers for its
+ // states, derivations must do that therefore they must define them.
+}
+
+void
+NameChangeTransaction::verifyStates() {
+ // Call superclass impl first.
+ StateModel::verifyStates();
+
+ // Verify NCT states. This ensures that derivations provide the handlers.
+ getState(READY_ST);
+ getState(SELECTING_FWD_SERVER_ST);
+ getState(SELECTING_REV_SERVER_ST);
+ getState(PROCESS_TRANS_OK_ST);
+ getState(PROCESS_TRANS_FAILED_ST);
+}
+
+void
+NameChangeTransaction::onModelFailure(const std::string& explanation) {
+ setNcrStatus(dhcp_ddns::ST_FAILED);
+ LOG_ERROR(dctl_logger, DHCP_DDNS_STATE_MODEL_UNEXPECTED_ERROR)
+ .arg(explanation);
+}
+
+void
+NameChangeTransaction::retryTransition(const int fail_to_state) {
+ if (update_attempts_ < MAX_UPDATE_TRIES_PER_SERVER) {
+ // Re-enter the current state with same server selected.
+ transition(getCurrState(), SERVER_SELECTED_EVT);
+ } else {
+ // Transition to given fail_to_state state if we are out
+ // of retries.
+ transition(fail_to_state, SERVER_IO_ERROR_EVT);
+ }
+}
+
+void
+NameChangeTransaction::setDnsUpdateRequest(D2UpdateMessagePtr& request) {
+ dns_update_request_ = request;
+}
+
+void
+NameChangeTransaction::clearDnsUpdateRequest() {
+ update_attempts_ = 0;
+ dns_update_request_.reset();
+}
+
+void
+NameChangeTransaction::setDnsUpdateStatus(const DNSClient::Status& status) {
+ dns_update_status_ = status;
+}
+
+void
+NameChangeTransaction::setDnsUpdateResponse(D2UpdateMessagePtr& response) {
+ dns_update_response_ = response;
+}
+
+void
+NameChangeTransaction::clearDnsUpdateResponse() {
+ dns_update_response_.reset();
+}
+
+void
+NameChangeTransaction::setForwardChangeCompleted(const bool value) {
+ forward_change_completed_ = value;
+}
+
+void
+NameChangeTransaction::setReverseChangeCompleted(const bool value) {
+ reverse_change_completed_ = value;
+}
+
+void
+NameChangeTransaction::setUpdateAttempts(const size_t value) {
+ update_attempts_ = value;
+}
+
+D2UpdateMessagePtr
+NameChangeTransaction::prepNewRequest(DdnsDomainPtr domain) {
+ if (!domain) {
+ isc_throw(NameChangeTransactionError,
+ "prepNewRequest - domain cannot be null");
+ }
+
+ try {
+ // Create a "blank" update request.
+ D2UpdateMessagePtr request(new D2UpdateMessage(D2UpdateMessage::
+ OUTBOUND));
+ // Construct the Zone Section.
+ dns::Name zone_name(domain->getName());
+ request->setZone(zone_name, dns::RRClass::IN());
+ return (request);
+ } catch (const std::exception& ex) {
+ isc_throw(NameChangeTransactionError, "Cannot create new request :"
+ << ex.what());
+ }
+}
+
+void
+NameChangeTransaction::addLeaseAddressRdata(dns::RRsetPtr& rrset) {
+ if (!rrset) {
+ isc_throw(NameChangeTransactionError,
+ "addLeaseAddressRdata - RRset cannot cannot be null");
+ }
+
+ try {
+ // Manufacture an RData from the lease address then add it to the RR.
+ dns::rdata::ConstRdataPtr rdata;
+ if (ncr_->isV4()) {
+ rdata.reset(new dns::rdata::in::A(ncr_->getIpAddress()));
+ } else {
+ rdata.reset(new dns::rdata::in::AAAA(ncr_->getIpAddress()));
+ }
+ rrset->addRdata(rdata);
+ } catch (const std::exception& ex) {
+ isc_throw(NameChangeTransactionError, "Cannot add address rdata: "
+ << ex.what());
+ }
+}
+
+void
+NameChangeTransaction::addDhcidRdata(dns::RRsetPtr& rrset) {
+ if (!rrset) {
+ isc_throw(NameChangeTransactionError,
+ "addDhcidRdata - RRset cannot cannot be null");
+ }
+
+ try {
+ const std::vector<uint8_t>& ncr_dhcid = ncr_->getDhcid().getBytes();
+ util::InputBuffer buffer(ncr_dhcid.data(), ncr_dhcid.size());
+ dns::rdata::ConstRdataPtr rdata (new dns::rdata::in::
+ DHCID(buffer, ncr_dhcid.size()));
+ rrset->addRdata(rdata);
+ } catch (const std::exception& ex) {
+ isc_throw(NameChangeTransactionError, "Cannot add DCHID rdata: "
+ << ex.what());
+ }
+
+}
+
+void
+NameChangeTransaction::addPtrRdata(dns::RRsetPtr& rrset) {
+ if (!rrset) {
+ isc_throw(NameChangeTransactionError,
+ "addPtrRdata - RRset cannot cannot be null");
+ }
+
+ try {
+ dns::rdata::ConstRdataPtr rdata(new dns::rdata::generic::
+ PTR(getNcr()->getFqdn()));
+ rrset->addRdata(rdata);
+ } catch (const std::exception& ex) {
+ isc_throw(NameChangeTransactionError, "Cannot add PTR rdata: "
+ << ex.what());
+ }
+}
+
+const dhcp_ddns::NameChangeRequestPtr&
+NameChangeTransaction::getNcr() const {
+ return (ncr_);
+}
+
+const TransactionKey&
+NameChangeTransaction::getTransactionKey() const {
+ return (ncr_->getDhcid());
+}
+
+dhcp_ddns::NameChangeStatus
+NameChangeTransaction::getNcrStatus() const {
+ return (ncr_->getStatus());
+}
+
+DdnsDomainPtr&
+NameChangeTransaction::getForwardDomain() {
+ return (forward_domain_);
+}
+
+DdnsDomainPtr&
+NameChangeTransaction::getReverseDomain() {
+ return (reverse_domain_);
+}
+
+void
+NameChangeTransaction::initServerSelection(const DdnsDomainPtr& domain) {
+ if (!domain) {
+ isc_throw(NameChangeTransactionError,
+ "initServerSelection called with an empty domain");
+ }
+ current_server_list_ = domain->getServers();
+ next_server_pos_ = 0;
+ current_server_.reset();
+}
+
+bool
+NameChangeTransaction::selectNextServer() {
+ if ((current_server_list_) &&
+ (next_server_pos_ < current_server_list_->size())) {
+ current_server_ = (*current_server_list_)[next_server_pos_];
+ // Toss out any previous response.
+ dns_update_response_.reset();
+
+ // @todo Protocol is set on DNSClient constructor. We need
+ // to propagate a configuration value downward, probably starting
+ // at global, then domain, then server
+ // Once that is supported we need to add it here.
+ dns_client_.reset(new DNSClient(dns_update_response_ , this,
+ DNSClient::UDP));
+ ++next_server_pos_;
+ return (true);
+ }
+
+ return (false);
+}
+
+const DNSClientPtr&
+NameChangeTransaction::getDNSClient() const {
+ return (dns_client_);
+}
+
+const DnsServerInfoPtr&
+NameChangeTransaction::getCurrentServer() const {
+ return (current_server_);
+}
+
+void
+NameChangeTransaction::setNcrStatus(const dhcp_ddns::NameChangeStatus& status) {
+ return (ncr_->setStatus(status));
+}
+
+const D2UpdateMessagePtr&
+NameChangeTransaction::getDnsUpdateRequest() const {
+ return (dns_update_request_);
+}
+
+DNSClient::Status
+NameChangeTransaction::getDnsUpdateStatus() const {
+ return (dns_update_status_);
+}
+
+const D2UpdateMessagePtr&
+NameChangeTransaction::getDnsUpdateResponse() const {
+ return (dns_update_response_);
+}
+
+bool
+NameChangeTransaction::getForwardChangeCompleted() const {
+ return (forward_change_completed_);
+}
+
+bool
+NameChangeTransaction::getReverseChangeCompleted() const {
+ return (reverse_change_completed_);
+}
+
+size_t
+NameChangeTransaction::getUpdateAttempts() const {
+ return (update_attempts_);
+}
+
+const dns::RRType&
+NameChangeTransaction::getAddressRRType() const {
+ return (ncr_->isV4() ? dns::RRType::A() : dns::RRType::AAAA());
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/nc_trans.h b/src/bin/d2/nc_trans.h
new file mode 100644
index 0000000..e729fa5
--- /dev/null
+++ b/src/bin/d2/nc_trans.h
@@ -0,0 +1,560 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NC_TRANS_H
+#define NC_TRANS_H
+
+/// @file nc_trans.h This file defines the class NameChangeTransaction.
+
+#include <exceptions/exceptions.h>
+#include <d2/d2_asio.h>
+#include <d2/d2_config.h>
+#include <d2/dns_client.h>
+#include <d2/state_model.h>
+#include <dhcp_ddns/ncr_msg.h>
+
+#include <boost/shared_ptr.hpp>
+#include <map>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the transaction encounters a general error.
+class NameChangeTransactionError : public isc::Exception {
+public:
+ NameChangeTransactionError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines the type used as the unique key for transactions.
+typedef isc::dhcp_ddns::D2Dhcid TransactionKey;
+
+/// @brief Embodies the "life-cycle" required to carry out a DDNS update.
+///
+/// NameChangeTransaction is the base class that provides the common state
+/// model mechanics and services performing the DNS updates needed to carry out
+/// a DHCP_DDNS request as described by a NameChangeRequest. It is derived
+/// from StateModel which supplies a simple, general purpose FSM implementation.
+///
+/// Upon construction, each transaction has all of the information and
+/// resources required to carry out its assigned request, including the list(s)
+/// of DNS server(s) needed. It is responsible for knowing what conversations
+/// it must have with which servers and in the order necessary to fulfill the
+/// request. Upon fulfillment of the request, the transaction's work is complete
+/// and it is destroyed.
+///
+/// Fulfillment of the request is carried out through the performance of the
+/// transaction's state model. Using a state driven implementation accounts
+/// for the conditional processing flow necessary to meet the DDNS RFCs as well
+/// as the asynchronous nature of IO with DNS servers.
+///
+/// Derivations of the class are responsible for defining the state model and
+/// conversations necessary to carry out the specific of request.
+///
+/// Conversations with DNS servers are done through the use of the DNSClient
+/// class. The DNSClient provides a IOService-based means a service which
+/// performs a single, packet exchange with a given DNS server. It sends a
+/// single update to the server and returns the response, asynchronously,
+/// through a callback. At each point in a transaction's state model, where
+/// an update is to be sent, the model "suspends" until notified by the
+/// DNSClient via the callback. Suspension is done by posting a
+/// StateModel::NOP_EVT as the next event, stopping the state model execution.
+///
+/// Resuming state model execution when a DNS update completes is done by a
+/// call to StateModel::runStateModel() from within the DNSClient callback,
+/// with an event value of IO_COMPLETED_EVT (described below).
+///
+/// This class defines a set of events and states that are a common to all
+/// transactions. Each derivation may add define additional states and events
+/// as needed, but it must support the common set. NameChangeTransaction
+/// does not supply any state handlers. These are the sole responsibility of
+/// derivations.
+class NameChangeTransaction : public DNSClient::Callback, public StateModel {
+public:
+
+ //@{ States common to all transactions.
+
+ /// @brief State from which a transaction is started.
+ static const int READY_ST = SM_DERIVED_STATE_MIN + 1;
+
+ /// @brief State in which forward DNS server selection is done.
+ ///
+ /// Within this state, the actual selection of the next forward server
+ /// to use is conducted. Upon conclusion of this state the next server
+ /// is either selected or it should transition out with NO_MORE_SERVERS_EVT
+ /// event.
+ static const int SELECTING_FWD_SERVER_ST = SM_DERIVED_STATE_MIN + 2;
+
+ /// @brief State in which reverse DNS server selection is done.
+ ///
+ /// Within this state, the actual selection of the next reverse server
+ /// to use is conducted. Upon conclusion of this state the next server
+ /// is either selected or it should transition out with NO_MORE_SERVERS_EVT
+ /// event.
+ static const int SELECTING_REV_SERVER_ST = SM_DERIVED_STATE_MIN + 3;
+
+ /// @brief State which processes successful transaction conclusion.
+ static const int PROCESS_TRANS_OK_ST = SM_DERIVED_STATE_MIN + 4;
+
+ /// @brief State which processes an unsuccessful transaction conclusion.
+ static const int PROCESS_TRANS_FAILED_ST = SM_DERIVED_STATE_MIN + 5;
+
+ /// @brief Value at which custom states in a derived class should begin.
+ static const int NCT_DERIVED_STATE_MIN = SM_DERIVED_STATE_MIN + 101;
+ //@}
+
+ //@{ Events common to all transactions.
+ /// @brief Issued when a server needs to be selected.
+ static const int SELECT_SERVER_EVT = SM_DERIVED_EVENT_MIN + 1;
+
+ /// @brief Issued when a server has been selected.
+ static const int SERVER_SELECTED_EVT = SM_DERIVED_EVENT_MIN + 2;
+
+ /// @brief Issued when an update fails due to an IO error.
+ static const int SERVER_IO_ERROR_EVT = SM_DERIVED_EVENT_MIN + 3;
+
+ /// @brief Issued when there are no more servers from which to select.
+ /// This occurs when none of the servers in the list can be reached to
+ /// perform the update.
+
+ static const int NO_MORE_SERVERS_EVT =SM_DERIVED_EVENT_MIN + 4;
+ /// @brief Issued when a DNS update packet exchange has completed.
+ /// This occurs whenever the DNSClient callback is invoked whether the
+ /// exchange was successful or not.
+
+ static const int IO_COMPLETED_EVT = SM_DERIVED_EVENT_MIN + 5;
+ /// @brief Issued when the attempted update successfully completed.
+ /// This occurs when an DNS update packet was successfully processed
+ /// by the server.
+
+ static const int UPDATE_OK_EVT = SM_DERIVED_EVENT_MIN + 6;
+
+ /// @brief Issued when the attempted update fails to complete.
+ /// This occurs when an DNS update packet fails to process. The nature of
+ /// the failure is given by the DNSClient return status and the response
+ /// packet (if one was received).
+ static const int UPDATE_FAILED_EVT = SM_DERIVED_EVENT_MIN + 7;
+
+ /// @brief Value at which custom events in a derived class should begin.
+ static const int NCT_DERIVED_EVENT_MIN = SM_DERIVED_EVENT_MIN + 101;
+ //@}
+
+ /// @brief Defualt time to assign to a single DNS udpate.
+ /// @todo This value will be made configurable in the very near future
+ /// under trac3268. For now we will define it to 100 milliseconds
+ /// so unit tests will run within a reasonable amount of time.
+ static const unsigned int DNS_UPDATE_DEFAULT_TIMEOUT = 100;
+
+ /// @brief Maximum times to attempt a single update on a given server.
+ static const unsigned int MAX_UPDATE_TRIES_PER_SERVER = 3;
+
+ /// @brief Constructor
+ ///
+ /// Instantiates a transaction that is ready to be started.
+ ///
+ /// @param io_service IO service to be used for IO processing
+ /// @param ncr is the NameChangeRequest to fulfill
+ /// @param forward_domain is the domain to use for forward DNS updates
+ /// @param reverse_domain is the domain to use for reverse DNS updates
+ ///
+ /// @throw NameChangeTransactionError if given an null request,
+ /// if forward change is enabled but forward domain is null, if
+ /// reverse change is enabled but reverse domain is null.
+ NameChangeTransaction(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain);
+
+ /// @brief Destructor
+ virtual ~NameChangeTransaction();
+
+ /// @brief Begins execution of the transaction.
+ ///
+ /// This method invokes StateModel::startModel() with a value of READY_ST.
+ /// This causes transaction's state model to attempt to begin execution
+ /// with the state handler for READY_ST.
+ void startTransaction();
+
+ /// @brief Serves as the DNSClient IO completion event handler.
+ ///
+ /// This is the implementation of the method inherited by our derivation
+ /// from DNSClient::Callback. When the DNSClient completes an update it
+ /// invokes this method as the completion handler. This method stores
+ /// the given status and invokes runStateModel() with an event value of
+ /// IO_COMPLETED_EVT.
+ ///
+ /// @param status is the outcome of the DNS update packet exchange.
+ /// This method is exception safe.
+ virtual void operator()(DNSClient::Status status);
+
+protected:
+ /// @brief Send the update request to the current server.
+ ///
+ /// This method increments the update attempt count and then passes the
+ /// current update request to the DNSClient instance to be sent to the
+ /// currently selected server. Since the send is asynchronous, the method
+ /// posts NOP_EVT as the next event and then returns.
+ ///
+ /// @param use_tsig True if the update should be include a TSIG key. This
+ /// is not yet implemented.
+ ///
+ /// If an exception occurs it will be logged and and the transaction will
+ /// be failed.
+ virtual void sendUpdate(bool use_tsig = false);
+
+ /// @brief Adds events defined by NameChangeTransaction to the event set.
+ ///
+ /// This method adds the events common to NCR transaction processing to
+ /// the set of define events. It invokes the superclass's implementation
+ /// first to maintain the hierarchical chain of event definition.
+ /// Derivations of NameChangeTransaction must invoke its implementation
+ /// in like fashion.
+ ///
+ /// @throw StateModelError if an event definition is invalid or a duplicate.
+ virtual void defineEvents();
+
+ /// @brief Validates the contents of the set of events.
+ ///
+ /// This method verifies that the events defined by both the superclass and
+ /// this class are defined. As with defineEvents, this method calls the
+ /// superclass's implementation first, to verify events defined by it and
+ /// then this implementation to verify events defined by
+ /// NameChangeTransaction.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyEvents();
+
+ /// @brief Adds states defined by NameChangeTransaction to the state set.
+ ///
+ /// This method adds the states common to NCR transaction processing to
+ /// the dictionary of states. It invokes the superclass's implementation
+ /// first to maintain the hierarchical chain of state definition.
+ /// Derivations of NameChangeTransaction must invoke its implementation
+ /// in like fashion.
+ ///
+ /// @throw StateModelError if an state definition is invalid or a duplicate.
+ virtual void defineStates();
+
+ /// @brief Validates the contents of the set of states.
+ ///
+ /// This method verifies that the states defined by both the superclass and
+ /// this class are defined. As with defineStates, this method calls the
+ /// superclass's implementation first, to verify states defined by it and
+ /// then this implementation to verify states defined by
+ /// NameChangeTransaction.
+ ///
+ /// @throw StateModelError if an event value is undefined.
+ virtual void verifyStates();
+
+ /// @brief Handler for fatal model execution errors.
+ ///
+ /// This handler is called by the StateModel implementation when the model
+ /// execution encounters a model violation: attempt to call an unmapped
+ /// state, an event not valid for the current state, or an uncaught
+ /// exception thrown during a state handler invocation. When such an
+ /// error occurs the transaction is deemed inoperable, and further model
+ /// execution cannot be performed. It marks the transaction as failed by
+ /// setting the NCR status to dhcp_ddns::ST_FAILED
+ ///
+ /// @param explanation is text detailing the error
+ virtual void onModelFailure(const std::string& explanation);
+
+ /// @brief Determines the state and next event based on update attempts.
+ ///
+ /// This method will post a next event of SERVER_SELECTED_EVT to the
+ /// current state if the number of update attempts has not reached the
+ /// maximum allowed.
+ ///
+ /// If the maximum number of attempts has been reached, it will transition
+ /// to the given state with a next event of SERVER_IO_ERROR_EVT.
+ ///
+ /// @param fail_to_state State to transition to if maximum attempts
+ /// have been tried.
+ ///
+ void retryTransition(const int fail_to_state);
+
+ /// @brief Sets the update request packet to the given packet.
+ ///
+ /// @param request is the new request packet to assign.
+ void setDnsUpdateRequest(D2UpdateMessagePtr& request);
+
+ /// @brief Destroys the current update request packet and resets
+ /// udpate attempts count.
+ void clearDnsUpdateRequest();
+
+ /// @brief Sets the update status to the given status value.
+ ///
+ /// @param status is the new value for the update status.
+ void setDnsUpdateStatus(const DNSClient::Status& status);
+
+ /// @brief Sets the update response packet to the given packet.
+ ///
+ /// @param response is the new response packet to assign.
+ void setDnsUpdateResponse(D2UpdateMessagePtr& response);
+
+ /// @brief Destroys the current update response packet.
+ void clearDnsUpdateResponse();
+
+ /// @brief Sets the forward change completion flag to the given value.
+ ///
+ /// @param value is the new value to assign to the flag.
+ void setForwardChangeCompleted(const bool value);
+
+ /// @brief Sets the reverse change completion flag to the given value.
+ ///
+ /// @param value is the new value to assign to the flag.
+ void setReverseChangeCompleted(const bool value);
+
+ /// @brief Sets the status of the transaction's NameChangeRequest
+ ///
+ /// @param status is the new value to assign to the NCR status.
+ void setNcrStatus(const dhcp_ddns::NameChangeStatus& status);
+
+ /// @brief Initializes server selection from the given DDNS domain.
+ ///
+ /// Method prepares internal data to conduct server selection from the
+ /// list of servers supplied by the given domain. This method should be
+ /// called when a transaction is ready to begin selecting servers from
+ /// a new list. Typically this will be prior to starting the updates for
+ /// a given DNS direction.
+ ///
+ /// @param domain is the domain from which server selection is to be
+ /// conducted.
+ void initServerSelection(const DdnsDomainPtr& domain);
+
+ /// @brief Selects the next server in the current server list.
+ ///
+ /// This method is used to iterate over the list of servers. If there are
+ /// no more servers in the list, it returns false. Otherwise it sets the
+ /// current server to the next server and creates a new DNSClient
+ /// instance.
+ ///
+ /// @return True if a server has been selected, false if there are no more
+ /// servers from which to select.
+ bool selectNextServer();
+
+ /// @brief Sets the update attempt count to the given value.
+ ///
+ /// @param value is the new value to assign.
+ void setUpdateAttempts(const size_t value);
+
+ /// @brief Fetches the IOService the transaction uses for IO processing.
+ ///
+ /// @return returns a const pointer to the IOService.
+ const IOServicePtr& getIOService() {
+ return (io_service_);
+ }
+
+ /// @brief Creates a new DNS update request based on the given domain.
+ ///
+ /// Constructs a new "empty", OUTBOUND, request with the message id set
+ /// and zone section populated based on the given domain.
+ /// It is declared virtual for test purposes.
+ ///
+ /// @return A D2UpdateMessagePtr to the new request.
+ ///
+ /// @throw NameChangeTransactionError if request cannot be constructed.
+ virtual D2UpdateMessagePtr prepNewRequest(DdnsDomainPtr domain);
+
+ /// @brief Adds an RData for the lease address to the given RRset.
+ ///
+ /// Creates an in::A() or in:AAAA() RData instance from the NCR
+ /// lease address and adds it to the given RRset.
+ ///
+ /// @param rrset RRset to which to add the RData
+ ///
+ /// @throw NameChangeTransactionError if RData cannot be constructed or
+ /// the RData cannot be added to the given RRset.
+ void addLeaseAddressRdata(dns::RRsetPtr& rrset);
+
+ /// @brief Adds an RData for the lease client's DHCID to the given RRset.
+ ///
+ /// Creates an in::DHCID() RData instance from the NCR DHCID and adds
+ /// it to the given RRset.
+ ///
+ /// @param rrset RRset to which to add the RData
+ ///
+ /// @throw NameChangeTransactionError if RData cannot be constructed or
+ /// the RData cannot be added to the given RRset.
+ void addDhcidRdata(dns::RRsetPtr& rrset);
+
+ /// @brief Adds an RData for the lease FQDN to the given RRset.
+ ///
+ /// Creates an in::PTR() RData instance from the NCR FQDN and adds
+ /// it to the given RRset.
+ ///
+ /// @param rrset RRset to which to add the RData
+ ///
+ /// @throw NameChangeTransactionError if RData cannot be constructed or
+ /// the RData cannot be added to the given RRset.
+ void addPtrRdata(dns::RRsetPtr& rrset);
+
+public:
+ /// @brief Fetches the NameChangeRequest for this transaction.
+ ///
+ /// @return A const pointer reference to the NameChangeRequest.
+ const dhcp_ddns::NameChangeRequestPtr& getNcr() const;
+
+ /// @brief Fetches the unique key that identifies this transaction.
+ ///
+ /// Transactions are uniquely identified by a TransactionKey. Currently
+ /// this is wrapper around a D2Dhcid.
+ ///
+ /// @return A const reference to the TransactionKey.
+ const TransactionKey& getTransactionKey() const;
+
+ /// @brief Fetches the NameChangeRequest status of the transaction.
+ ///
+ /// This is the current status of the NameChangeRequest, not to
+ /// be confused with the state of the transaction. Once the transaction
+ /// is reached its conclusion, the request will end up with a final
+ /// status.
+ ///
+ /// @return A dhcp_ddns::NameChangeStatus representing the current
+ /// status of the transaction.
+ dhcp_ddns::NameChangeStatus getNcrStatus() const;
+
+ /// @brief Fetches the forward DdnsDomain.
+ ///
+ /// @return A pointer reference to the forward DdnsDomain. If
+ /// the request does not include a forward change, the pointer will empty.
+ DdnsDomainPtr& getForwardDomain();
+
+ /// @brief Fetches the reverse DdnsDomain.
+ ///
+ /// @return A pointer reference to the reverse DdnsDomain. If
+ /// the request does not include a reverse change, the pointer will empty.
+ DdnsDomainPtr& getReverseDomain();
+
+ /// @brief Fetches the currently selected server.
+ ///
+ /// @return A const pointer reference to the DnsServerInfo of the current
+ /// server.
+ const DnsServerInfoPtr& getCurrentServer() const;
+
+ /// @brief Fetches the DNSClient instance
+ ///
+ /// @return A const pointer reference to the DNSClient
+ const DNSClientPtr& getDNSClient() const;
+
+ /// @brief Fetches the current DNS update request packet.
+ ///
+ /// @return A const pointer reference to the current D2UpdateMessage
+ /// request.
+ const D2UpdateMessagePtr& getDnsUpdateRequest() const;
+
+ /// @brief Fetches the most recent DNS update status.
+ ///
+ /// @return A DNSClient::Status indicating the result of the most recent
+ /// DNS update to complete.
+ DNSClient::Status getDnsUpdateStatus() const;
+
+ /// @brief Fetches the most recent DNS update response packet.
+ ///
+ /// @return A const pointer reference to the D2UpdateMessage most recently
+ /// received.
+ const D2UpdateMessagePtr& getDnsUpdateResponse() const;
+
+ /// @brief Returns whether the forward change has completed or not.
+ ///
+ /// The value returned is only meaningful if the NameChangeRequest calls
+ /// for a forward change to be done. The value returned indicates if
+ /// forward change has been completed successfully.
+ ///
+ /// @return True if the forward change has been completed, false otherwise.
+ bool getForwardChangeCompleted() const;
+
+ /// @brief Returns whether the reverse change has completed or not.
+ ///
+ /// The value returned is only meaningful if the NameChangeRequest calls
+ /// for a reverse change to be done. The value returned indicates if
+ /// reverse change has been completed successfully.
+ ///
+ /// @return True if the reverse change has been completed, false otherwise.
+ bool getReverseChangeCompleted() const;
+
+ /// @brief Fetches the update attempt count for the current update.
+ ///
+ /// @return size_t which is the number of times the current request has
+ /// been attempted against the current server.
+ size_t getUpdateAttempts() const;
+
+ /// @brief Returns the DHCP data type for the lease address
+ ///
+ /// @return constant reference to dns::RRType::A() if the lease address
+ /// is IPv4 or dns::RRType::AAAA() if the lease address is IPv6.
+ const dns::RRType& getAddressRRType() const;
+
+private:
+ /// @brief The IOService which should be used to for IO processing.
+ IOServicePtr io_service_;
+
+ /// @brief The NameChangeRequest that the transaction is to fulfill.
+ dhcp_ddns::NameChangeRequestPtr ncr_;
+
+ /// @brief The forward domain that matches the request.
+ ///
+ /// The forward "domain" is DdnsDomain which contains all of the information
+ /// necessary, including the list of DNS servers to be used for a forward
+ /// change.
+ DdnsDomainPtr forward_domain_;
+
+ /// @brief The reverse domain that matches the request.
+ ///
+ /// The reverse "domain" is DdnsDomain which contains all of the information
+ /// necessary, including the list of DNS servers to be used for a reverse
+ /// change.
+ DdnsDomainPtr reverse_domain_;
+
+ /// @brief The DNSClient instance that will carry out DNS packet exchanges.
+ DNSClientPtr dns_client_;
+
+ /// @brief The DNS current update request packet.
+ D2UpdateMessagePtr dns_update_request_;
+
+ /// @brief The outcome of the most recently completed DNS packet exchange.
+ DNSClient::Status dns_update_status_;
+
+ /// @brief The DNS update response packet most recently received.
+ D2UpdateMessagePtr dns_update_response_;
+
+ /// @brief Indicator for whether or not the forward change completed ok.
+ bool forward_change_completed_;
+
+ /// @brief Indicator for whether or not the reverse change completed ok.
+ bool reverse_change_completed_;
+
+ /// @brief Pointer to the current server selection list.
+ DnsServerInfoStoragePtr current_server_list_;
+
+ /// @brief Pointer to the currently selected server.
+ DnsServerInfoPtr current_server_;
+
+ /// @brief Next server position in the list.
+ ///
+ /// This value is always the position of the next selection in the server
+ /// list, which may be beyond the end of the list.
+ size_t next_server_pos_;
+
+ /// @brief Number of transmit attempts for the current request.
+ size_t update_attempts_;
+};
+
+/// @brief Defines a pointer to a NameChangeTransaction.
+typedef boost::shared_ptr<NameChangeTransaction> NameChangeTransactionPtr;
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/spec_config.h.pre.in b/src/bin/d2/spec_config.h.pre.in
new file mode 100644
index 0000000..6d48a7e
--- /dev/null
+++ b/src/bin/d2/spec_config.h.pre.in
@@ -0,0 +1,15 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#define D2_SPECFILE_LOCATION "@prefix@/share/@PACKAGE@/dhcp-ddns.spec"
diff --git a/src/bin/d2/state_model.cc b/src/bin/d2/state_model.cc
new file mode 100644
index 0000000..c5a0209
--- /dev/null
+++ b/src/bin/d2/state_model.cc
@@ -0,0 +1,387 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/state_model.h>
+
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+/********************************** State *******************************/
+
+State::State(const int value, const std::string& label, StateHandler handler)
+ : LabeledValue(value, label), handler_(handler) {
+}
+
+State::~State() {
+}
+
+void
+State::run() {
+ (handler_)();
+}
+
+/********************************** StateSet *******************************/
+
+StateSet::StateSet() {
+}
+
+StateSet::~StateSet() {
+}
+
+void
+StateSet::add(const int value, const std::string& label, StateHandler handler) {
+ try {
+ LabeledValueSet::add(LabeledValuePtr(new State(value, label, handler)));
+ } catch (const std::exception& ex) {
+ isc_throw(StateModelError, "StateSet: cannot add state :" << ex.what());
+ }
+
+}
+
+const StatePtr
+StateSet::getState(int value) {
+ if (!isDefined(value)) {
+ isc_throw(StateModelError," StateSet: state is undefined");
+ }
+
+ // Since we have to use dynamic casting, to get a state pointer
+ // we can't return a reference.
+ StatePtr state = boost::dynamic_pointer_cast<State>(get(value));
+ return (state);
+}
+
+/********************************** StateModel *******************************/
+
+
+// Common state model states
+const int StateModel::NEW_ST;
+const int StateModel::END_ST;
+
+const int StateModel::SM_DERIVED_STATE_MIN;
+
+// Common state model events
+const int StateModel::NOP_EVT;
+const int StateModel::START_EVT;
+const int StateModel::END_EVT;
+const int StateModel::FAIL_EVT;
+
+const int StateModel::SM_DERIVED_EVENT_MIN;
+
+StateModel::StateModel() : events_(), states_(), dictionaries_initted_(false),
+ curr_state_(NEW_ST), prev_state_(NEW_ST),
+ last_event_(NOP_EVT), next_event_(NOP_EVT),
+ on_entry_flag_(false), on_exit_flag_(false) {
+}
+
+StateModel::~StateModel(){
+}
+
+void
+StateModel::startModel(const int start_state) {
+ // Intialize dictionaries of events and states.
+ initDictionaries();
+
+ // Set the current state to starting state and enter the run loop.
+ setState(start_state);
+
+ // Start running the model.
+ runModel(START_EVT);
+}
+
+void
+StateModel::runModel(unsigned int run_event) {
+ /// If the dictionaries aren't built bail out.
+ if (!dictionaries_initted_) {
+ abortModel("runModel invoked before model has been initialized");
+ }
+
+ try {
+ // Seed the loop with the given event as the next to process.
+ postNextEvent(run_event);
+ do {
+ // Invoke the current state's handler. It should consume the
+ // next event, then determine what happens next by setting
+ // current state and/or the next event.
+ getState(curr_state_)->run();
+
+ // Keep going until a handler sets next event to a NOP_EVT.
+ } while (!isModelDone() && getNextEvent() != NOP_EVT);
+ }
+ catch (const std::exception& ex) {
+ // The model has suffered an unexpected exception. This constitutes
+ // a model violation and indicates a programmatic shortcoming.
+ // In theory, the model should account for all error scenarios and
+ // deal with them accordingly. Transition to END_ST with FAILED_EVT
+ // via abortModel.
+ abortModel(ex.what());
+ }
+}
+
+void
+StateModel::nopStateHandler() {
+}
+
+void
+StateModel::initDictionaries() {
+ // First let's build and verify the dictionary of events.
+ try {
+ defineEvents();
+ verifyEvents();
+ } catch (const std::exception& ex) {
+ isc_throw(StateModelError, "Event set is invalid: " << ex.what());
+ }
+
+ // Next let's build and verify the dictionary of states.
+ try {
+ defineStates();
+ verifyStates();
+ } catch (const std::exception& ex) {
+ isc_throw(StateModelError, "State set is invalid: " << ex.what());
+ }
+
+ // Record that we are good to go.
+ dictionaries_initted_ = true;
+}
+
+void
+StateModel::defineEvent(unsigned int event_value, const std::string& label) {
+ if (!isModelNew()) {
+ // Don't allow for self-modifying models.
+ isc_throw(StateModelError, "Events may only be added to a new model."
+ << event_value << " - " << label);
+ }
+
+ // Attempt to add the event to the set.
+ try {
+ events_.add(event_value, label);
+ } catch (const std::exception& ex) {
+ isc_throw(StateModelError, "Error adding event: " << ex.what());
+ }
+}
+
+const EventPtr&
+StateModel::getEvent(unsigned int event_value) {
+ if (!events_.isDefined(event_value)) {
+ isc_throw(StateModelError,
+ "Event value is not defined:" << event_value);
+ }
+
+ return (events_.get(event_value));
+}
+
+void
+StateModel::defineState(unsigned int state_value, const std::string& label,
+ StateHandler handler) {
+ if (!isModelNew()) {
+ // Don't allow for self-modifying maps.
+ isc_throw(StateModelError, "States may only be added to a new model."
+ << state_value << " - " << label);
+ }
+
+ // Attempt to add the state to the set.
+ try {
+ states_.add(state_value, label, handler);
+ } catch (const std::exception& ex) {
+ isc_throw(StateModelError, "Error adding state: " << ex.what());
+ }
+}
+
+const StatePtr
+StateModel::getState(unsigned int state_value) {
+ if (!states_.isDefined(state_value)) {
+ isc_throw(StateModelError,
+ "State value is not defined:" << state_value);
+ }
+
+ return (states_.getState(state_value));
+}
+
+void
+StateModel::defineEvents() {
+ defineEvent(NOP_EVT, "NOP_EVT");
+ defineEvent(START_EVT, "START_EVT");
+ defineEvent(END_EVT, "END_EVT");
+ defineEvent(FAIL_EVT, "FAIL_EVT");
+}
+
+void
+StateModel::verifyEvents() {
+ getEvent(NOP_EVT);
+ getEvent(START_EVT);
+ getEvent(END_EVT);
+ getEvent(FAIL_EVT);
+}
+
+void
+StateModel::defineStates() {
+ defineState(NEW_ST, "NEW_ST",
+ boost::bind(&StateModel::nopStateHandler, this));
+ defineState(END_ST, "END_ST",
+ boost::bind(&StateModel::nopStateHandler, this));
+}
+
+void
+StateModel::verifyStates() {
+ getState(NEW_ST);
+ getState(END_ST);
+}
+
+void
+StateModel::onModelFailure(const std::string&) {
+ // Empty implementation to make deriving classes simpler.
+}
+
+void
+StateModel::transition(unsigned int state, unsigned int event) {
+ setState(state);
+ postNextEvent(event);
+}
+
+void
+StateModel::endModel() {
+ transition(END_ST, END_EVT);
+}
+
+void
+StateModel::abortModel(const std::string& explanation) {
+ transition(END_ST, FAIL_EVT);
+
+ std::ostringstream stream ;
+ stream << explanation << " : " << getContextStr();
+ onModelFailure(stream.str());
+}
+
+void
+StateModel::setState(unsigned int state) {
+ if (state != END_ST && !states_.isDefined(state)) {
+ isc_throw(StateModelError,
+ "Attempt to set state to an undefined value: " << state );
+ }
+
+ prev_state_ = curr_state_;
+ curr_state_ = state;
+
+ // Set the "do" flags if we are transitioning.
+ on_entry_flag_ = ((state != END_ST) && (prev_state_ != curr_state_));
+
+ // At this time they are calculated the same way.
+ on_exit_flag_ = on_entry_flag_;
+}
+
+void
+StateModel::postNextEvent(unsigned int event_value) {
+ // Check for FAIL_EVT as special case of model error before events are
+ // defined.
+ if (event_value != FAIL_EVT && !events_.isDefined(event_value)) {
+ isc_throw(StateModelError,
+ "Attempt to post an undefined event, value: " << event_value);
+ }
+
+ last_event_ = next_event_;
+ next_event_ = event_value;
+}
+
+bool
+StateModel::doOnEntry() {
+ bool ret = on_entry_flag_;
+ on_entry_flag_ = false;
+ return (ret);
+}
+
+bool
+StateModel::doOnExit() {
+ bool ret = on_exit_flag_;
+ on_exit_flag_ = false;
+ return (ret);
+}
+
+unsigned int
+StateModel::getCurrState() const {
+ return (curr_state_);
+}
+
+unsigned int
+StateModel::getPrevState() const {
+ return (prev_state_);
+}
+
+unsigned int
+StateModel::getLastEvent() const {
+ return (last_event_);
+}
+
+unsigned int
+StateModel::getNextEvent() const {
+ return (next_event_);
+}
+bool
+StateModel::isModelNew() const {
+ return (curr_state_ == NEW_ST);
+}
+
+bool
+StateModel::isModelRunning() const {
+ return ((curr_state_ != NEW_ST) && (curr_state_ != END_ST));
+}
+
+bool
+StateModel::isModelWaiting() const {
+ return (isModelRunning() && (next_event_ == NOP_EVT));
+}
+
+bool
+StateModel::isModelDone() const {
+ return (curr_state_ == END_ST);
+}
+
+bool
+StateModel::didModelFail() const {
+ return (isModelDone() && (next_event_ == FAIL_EVT));
+}
+
+std::string
+StateModel::getStateLabel(const int state) const {
+ return (states_.getLabel(state));
+}
+
+std::string
+StateModel::getEventLabel(const int event) const {
+ return (events_.getLabel(event));
+}
+
+std::string
+StateModel::getContextStr() const {
+ std::ostringstream stream;
+ stream << "current state: [ "
+ << curr_state_ << " " << getStateLabel(curr_state_)
+ << " ] next event: [ "
+ << next_event_ << " " << getEventLabel(next_event_) << " ]";
+ return(stream.str());
+}
+
+std::string
+StateModel::getPrevContextStr() const {
+ std::ostringstream stream;
+ stream << "previous state: [ "
+ << prev_state_ << " " << getStateLabel(prev_state_)
+ << " ] last event: [ "
+ << next_event_ << " " << getEventLabel(last_event_) << " ]";
+ return(stream.str());
+}
+
+} // namespace isc::d2
+} // namespace isc
diff --git a/src/bin/d2/state_model.h b/src/bin/d2/state_model.h
new file mode 100644
index 0000000..b7320eb
--- /dev/null
+++ b/src/bin/d2/state_model.h
@@ -0,0 +1,679 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef STATE_MODEL_H
+#define STATE_MODEL_H
+
+/// @file state_model.h This file defines the class StateModel.
+
+#include <exceptions/exceptions.h>
+#include <d2/d2_config.h>
+#include <d2/dns_client.h>
+#include <d2/labeled_value.h>
+#include <dhcp_ddns/ncr_msg.h>
+
+#include <boost/shared_ptr.hpp>
+#include <map>
+#include <string>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Thrown if the state machine encounters a general error.
+class StateModelError : public isc::Exception {
+public:
+ StateModelError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Define an Event.
+typedef LabeledValue Event;
+
+/// @brief Define Event pointer.
+typedef LabeledValuePtr EventPtr;
+
+/// @brief Defines a pointer to an instance method for handling a state.
+typedef boost::function<void()> StateHandler;
+
+/// @brief Defines a State within the State Model.
+///
+/// This class provides the means to define a state within a set or dictionary
+/// of states, and assign the state an handler method to execute the state's
+/// actions. It derives from LabeledValue which allows a set of states to be
+/// keyed by integer constants.
+class State : public LabeledValue {
+public:
+ /// @brief Constructor
+ ///
+ /// @param value is the numeric value of the state
+ /// @param label is the text label to assign to the state
+ /// @param handler is the bound instance method which handles the state's
+ /// action.
+ ///
+ /// A typical invocation might look this:
+ ///
+ /// @code
+ /// State(SOME_INT_VAL, "SOME_INT_VAL",
+ /// boost::bind(&StateModelDerivation::someHandler, this));
+ /// @endcode
+ ///
+ /// @throw StateModelError if label is null or blank.
+ State(const int value, const std::string& label, StateHandler handler);
+
+ /// @brief Destructor
+ virtual ~State();
+
+ /// @brief Invokes the State's handler.
+ void run();
+
+private:
+ /// @brief Bound instance method pointer to the state's handler method.
+ StateHandler handler_;
+};
+
+/// @brief Defines a shared pointer to a State.
+typedef boost::shared_ptr<State> StatePtr;
+
+/// @brief Implements a unique set or dictionary of states.
+///
+/// This class provides the means to construct and access a unique set of
+/// states. This provide the ability to validate state values, look up their
+/// text labels, and their handlers.
+class StateSet : public LabeledValueSet {
+public:
+ /// @brief Constructor
+ StateSet();
+
+ /// @brief Destructor
+ virtual ~StateSet();
+
+ /// @brief Adds a state definition to the set of states.
+ ///
+ /// @param value is the numeric value of the state
+ /// @param label is the text label to assig to the state
+ /// @param handler is the bound instance method which handles the state's
+ ///
+ /// @throw StateModelError if the value is already defined in the set, or
+ /// if the label is null or blank.
+ void add(const int value, const std::string& label, StateHandler handler);
+
+ /// @brief Fetches a state for the given value.
+ ///
+ /// @param value the numeric value of the state desired
+ ///
+ /// @return A constant pointer the State found.
+ /// Note, this relies on dynamic cast and cannot return a pointer reference.
+ ///
+ /// @throw StateModelError if the value is undefined.
+ const StatePtr getState(int value);
+};
+
+/// @brief Implements a finite state machine.
+///
+/// StateModel is an abstract class that provides the structure and mechanics
+/// of a basic finite state machine.
+///
+/// The state model implementation used is a very basic approach. The model
+/// uses numeric constants to identify events and states, and maintains
+/// dictionaries of defined events and states. Event and state definitions
+/// include a text label for logging purposes. Additionally, each state
+/// definition includes a state handler. State handlers are methods which
+/// implement the actions that need to occur when the model is "in" a given
+/// state. The implementation provides methods to add entries to and verify
+/// the contents of both dictionaries.
+///
+/// During model execution, the following context is tracked:
+///
+/// * current state - The current state of the model
+/// * previous state - The state the model was in prior to the current state
+/// * next event - The next event to be consumed
+/// * last event - The event most recently consumed
+///
+/// When invoked, a state handler determines what it should do based upon the
+/// next event including what the next state and event should be. In other
+/// words the state transition knowledge is distributed among the state
+/// handlers rather than encapsulated in some form of state transition table.
+///
+/// Events "posted" from within the state handlers are "internally" triggered
+/// events. Events "posted" from outside the state model, such as through
+/// the invocation of a callback are "externally" triggered.
+///
+/// StateModel defines two states:
+///
+/// * NEW_ST - State that a model is in following instantiation. It remains in
+/// this state until model execution has begun.
+/// * END_ST - State that a model is in once it has reached its conclusion.
+///
+/// and the following events:
+///
+/// * START_EVT - Event used to start model execution.
+/// * NOP_EVT - Event used to signify that the model stop and wait for an
+/// external event, such as the completion of an asynchronous IO operation.
+/// * END_EVT - Event used to trigger a normal conclusion of the model. This
+/// means only that the model was traversed from start to finish, without any
+/// model violations (i.e. invalid state, event, or transition) or uncaught
+/// exceptions.
+/// * FAIL_EVT - Event to trigger an abnormal conclusion of the model. This
+/// event is posted internally when model execution fails due to a model
+/// violation or uncaught exception. It signifies that the model has reached
+/// an inoperable condition.
+///
+/// Derivations add their own states and events appropriate for their state
+/// model. Note that NEW_ST and END_ST do not support handlers. No work can
+/// be done (events consumed) prior to starting the model nor can work be done
+/// once the model has ended.
+///
+/// Model execution consists of iteratively invoking the state handler
+/// indicated by the current state which should consume the next event. As the
+/// handlers post events and/or change the state, the model is traversed. The
+/// loop stops whenever the model cannot continue without an externally
+/// triggered event or when it has reached its final state. In the case of
+/// the former, the loop may be re-entered upon arrival of the external event.
+///
+/// This loop is implemented in the runModel method. This method accepts an
+/// event as argument which it "posts" as the next event. It then retrieves the
+/// handler for the current state from the handler map and invokes it. runModel
+/// repeats this process until it either a NOP_EVT posts or the state changes
+/// to END_ST. In other words each invocation of runModel causes the model to
+/// be traversed from the current state until it must wait or ends.
+///
+/// Re-entering the "loop" upon the occurrence of an external event is done by
+/// invoking runModel with the appropriate event. As before, runModel will
+/// loop until either the NOP_EVT occurs or until the model reaches its end.
+///
+/// A StateModel (derivation) is in the NEW_ST when constructed and remains
+/// there until it has been "started". Starting the model is done by invoking
+/// the startModel method which accepts a state as a parameter. This parameter
+/// specifies the "start" state and it becomes the current state.
+
+/// The first task undertaken by startModel is to initialize and verify the
+/// the event and state dictionaries. The following virtual methods are
+/// provided for this:
+///
+/// * defineEvents - define events
+/// * verifyEvents - verifies that the expected events are defined
+/// * defineStates - defines states
+/// * verifyStates - verifies that the expected states are defined
+///
+/// The concept behind the verify methods is to provide an initial sanity
+/// check of the dictionaries. This should help avoid using undefined event
+/// or state values accidentally.
+///
+/// These methods are intended to be implemented by each "layer" in a StateModel
+/// derivation hierarchy. This allows each layer to define additional events
+/// and states.
+///
+/// Once the dictionaries have been properly initialized, the startModel method
+/// invokes runModel with an event of START_EVT. From this point forward and
+/// until the model reaches the END_ST or fails, it is considered to be
+/// "running". If the model encounters a NOP_EVT then it is "running" and
+/// "waiting". If the model reaches END_ST with an END_EVT it is considered
+/// "done". If the model fails (END_ST with a FAILED_EVT) it is considered
+/// "done" and "failed". There are several boolean status methods which may
+/// be used to check these conditions.
+///
+/// To progress from one state to the another, state handlers invoke use
+/// the method, transition. This method accepts a state and an event as
+/// parameters. These values become the current state and the next event
+/// respectively. This has the effect of entering the given state having posted
+/// the given event. The postEvent method may be used to post a new event
+/// to the current state.
+///
+/// Bringing the model to a normal end is done by invoking the endModel method
+/// which transitions the model to END_ST with END_EVT. Bringing the model to
+/// an abnormal end is done via the abortModel method, which transitions the
+/// model to END_ST with FAILED_EVT.
+class StateModel {
+public:
+
+ //@{ States common to all models.
+ /// @brief State that a state model is in immediately after construction.
+ static const int NEW_ST = 0;
+
+ /// @brief Final state, all the state model has reached its conclusion.
+ static const int END_ST = 1;
+
+ /// @brief Value at which custom states in a derived class should begin.
+ static const int SM_DERIVED_STATE_MIN = 11;
+ //@}
+
+ //@{ Events common to all state models.
+ /// @brief Signifies that no event has occurred.
+ /// This is event used to interrupt the event loop to allow waiting for
+ /// an IO event or when there is no more work to be done.
+ static const int NOP_EVT = 0;
+
+ /// @brief Event issued to start the model execution.
+ static const int START_EVT = 1;
+
+ /// @brief Event issued to end the model execution.
+ static const int END_EVT = 2;
+
+ /// @brief Event issued to abort the model execution.
+ static const int FAIL_EVT = 3;
+
+ /// @brief Value at which custom events in a derived class should begin.
+ static const int SM_DERIVED_EVENT_MIN = 11;
+ //@}
+
+ /// @brief Constructor
+ StateModel();
+
+ /// @brief Destructor
+ virtual ~StateModel();
+
+ /// @brief Begins execution of the model.
+ ///
+ /// This method invokes initDictionaries method to initialize the event
+ /// and state dictionaries and then starts the model execution setting
+ /// the current state to the given start state, and the event to START_EVT.
+ ///
+ /// @param start_state is the state in which to begin execution.
+ ///
+ /// @throw StateModelError or others indirectly, as this method calls
+ /// dictionary define and verify methods.
+ void startModel(const int start_state);
+
+ /// @brief Processes events through the state model
+ ///
+ /// This method implements the state model "execution loop". It uses
+ /// the given event as the next event to process and begins invoking
+ /// the state handler for the current state. As described above, the
+ /// invoked state handler consumes the next event and then determines the
+ /// next event and the current state as required to implement the business
+ /// logic. The method continues to loop until the next event posted is
+ /// NOP_EVT or the model ends.
+ ///
+ /// Any exception thrown during the loop is caught, logged, and the
+ /// model is aborted with a FAIL_EVT. The derivation's state
+ /// model is expected to account for any possible errors so any that
+ /// escape are treated as unrecoverable.
+ ///
+ /// @param event is the next event to process
+ ///
+ /// This method is guaranteed not to throw.
+ void runModel(unsigned int event);
+
+ /// @brief Conducts a normal transition to the end of the model.
+ ///
+ /// This method posts an END_EVT and sets the current state to END_ST.
+ /// It should be called by any state handler in the model from which
+ /// an exit leads to the model end. In other words, if the transition
+ /// out of a particular state is to the end of the model, that state's
+ /// handler should call endModel.
+ void endModel();
+
+ /// @brief An empty state handler.
+ ///
+ /// This method is primarily used to permit special states, NEW_ST and
+ /// END_ST to be included in the state dictionary. Currently it is an
+ /// empty method.
+ void nopStateHandler();
+
+protected:
+ /// @brief Initializes the event and state dictionaries.
+ ///
+ /// This method invokes the define and verify methods for both events and
+ /// states to initialize their respective dictionaries.
+ ///
+ /// @throw StateModelError or others indirectly, as this method calls
+ /// dictionary define and verify methods.
+ void initDictionaries();
+
+ /// @brief Populates the set of events.
+ ///
+ /// This method is used to construct the set of valid events. Each class
+ /// within a StateModel derivation hierarchy uses this method to add any
+ /// events it defines to the set. Each derivation's implementation must
+ /// also call it's superclass's implementation. This allows each class
+ /// within the hierarchy to make contributions to the set of defined
+ /// events. Implementations use the method, defineEvent(), to add event
+ /// definitions. An example of the derivation's implementation follows:
+ ///
+ /// @code
+ /// void StateModelDerivation::defineEvents() {
+ /// // Call the superclass implementation.
+ /// StateModelDerivation::defineEvents();
+ ///
+ /// // Add the events defined by the derivation.
+ /// defineEvent(SOME_CUSTOM_EVT_1, "CUSTOM_EVT_1");
+ /// defineEvent(SOME_CUSTOM_EVT_2, "CUSTOM_EVT_2");
+ /// :
+ /// }
+ /// @endcode
+ virtual void defineEvents();
+
+ /// @brief Adds an event value and associated label to the set of events.
+ ///
+ /// @param value is the numeric value of the event
+ /// @param label is the text label of the event used in log messages and
+ /// exceptions.
+ ///
+ /// @throw StateModelError if the model has already been started, if
+ /// the value is already defined, or if the label is empty.
+ void defineEvent(unsigned int value, const std::string& label);
+
+ /// @brief Fetches the event referred to by value.
+ ///
+ /// @param value is the numeric value of the event desired.
+ ///
+ /// @return returns a constant pointer reference to the event if found
+ ///
+ /// @throw StateModelError if the event is not defined.
+ const EventPtr& getEvent(unsigned int value);
+
+ /// @brief Validates the contents of the set of events.
+ ///
+ /// This method is invoked immediately after the defineEvents method and
+ /// is used to verify that all the required events are defined. If the
+ /// event set is determined to be invalid this method should throw a
+ /// StateModelError. As with the defineEvents method, each class within
+ /// a StateModel derivation hierarchy must supply an implementation
+ /// which calls it's superclass's implementation as well as verifying any
+ /// events added by the derivation. Validating an event is accomplished
+ /// by simply attempting to fetch an event by its value from the event set.
+ /// An example of the derivation's implementation follows:
+ ///
+ /// @code
+ /// void StateModelDerivation::verifyEvents() {
+ /// // Call the superclass implementation.
+ /// StateModelDerivation::verifyEvents();
+ ///
+ /// // Verify the events defined by the derivation.
+ /// getEvent(SOME_CUSTOM_EVT_1, "CUSTOM_EVT_1");
+ /// getEvent(SOME_CUSTOM_EVT_2, "CUSTOM_EVT_2");
+ /// :
+ /// }
+ /// @endcode
+ virtual void verifyEvents();
+
+ /// @brief Populates the set of states.
+ ///
+ /// This method is used to construct the set of valid states. Each class
+ /// within a StateModel derivation hierarchy uses this method to add any
+ /// states it defines to the set. Each derivation's implementation must
+ /// also call it's superclass's implementation. This allows each class
+ /// within the hierarchy to make contributions to the set of defined
+ /// states. Implementations use the method, defineState(), to add state
+ /// definitions. An example of the derivation's implementation follows:
+ ///
+ /// @code
+ /// void StateModelDerivation::defineStates() {
+ /// // Call the superclass implementation.
+ /// StateModelDerivation::defineStates();
+ ///
+ /// // Add the states defined by the derivation.
+ /// defineState(SOME_ST, "SOME_ST",
+ /// boost::bind(&StateModelDerivation::someHandler, this));
+ /// :
+ /// }
+ /// @endcode
+ virtual void defineStates();
+
+ /// @brief Adds an state value and associated label to the set of states.
+ ///
+ /// @param value is the numeric value of the state
+ /// @param label is the text label of the state used in log messages and
+ /// exceptions.
+ /// @param handler is the bound instance method which implements the state's
+ /// actions.
+ ///
+ /// @throw StateModelError if the model has already been started, if
+ /// the value is already defined, or if the label is empty.
+ void defineState(unsigned int value, const std::string& label,
+ StateHandler handler);
+
+ /// @brief Fetches the state referred to by value.
+ ///
+ /// @param value is the numeric value of the state desired.
+ ///
+ /// @return returns a constant pointer to the state if found
+ ///
+ /// @throw StateModelError if the state is not defined.
+ const StatePtr getState(unsigned int value);
+
+ /// @brief Validates the contents of the set of states.
+ ///
+ /// This method is invoked immediately after the defineStates method and
+ /// is used to verify that all the required states are defined. If the
+ /// state set is determined to be invalid this method should throw a
+ /// StateModelError. As with the defineStates method, each class within
+ /// a StateModel derivation hierarchy must supply an implementation
+ /// which calls it's superclass's implementation as well as verifying any
+ /// states added by the derivation. Validating an state is accomplished
+ /// by simply attempting to fetch the state by its value from the state set.
+ /// An example of the derivation's implementation follows:
+ ///
+ /// @code
+ /// void StateModelDerivation::verifyStates() {
+ /// // Call the superclass implementation.
+ /// StateModelDerivation::verifyStates();
+ ///
+ /// // Verify the states defined by the derivation.
+ /// getState(SOME_CUSTOM_EVT_2);
+ /// :
+ /// }
+ /// @endcode
+ virtual void verifyStates();
+
+ /// @brief Handler for fatal model execution errors.
+ ///
+ /// This method is called when an unexpected error renders during
+ /// model execution, such as a state handler throwing an exception.
+ /// It provides derivations an opportunity to act accordingly by setting
+ /// the appropriate status or taking other remedial action. This allows
+ /// the model execution loop to remain exception safe. This default
+ /// implementation does nothing.
+ ///
+ /// @param explanation text detailing the error and state machine context
+ virtual void onModelFailure(const std::string& explanation);
+
+ /// @brief Sets up the model to transition into given state with a given
+ /// event.
+ ///
+ /// This updates the model's notion of the current state and the next
+ /// event to process. State handlers use this method to move from one state
+ /// to the next.
+ ///
+ /// @param state the new value to assign to the current state.
+ /// @param event the new value to assign to the next event.
+ ///
+ /// @throw StateModelError if the state is invalid.
+ void transition(unsigned int state, unsigned int event);
+
+ /// @brief Aborts model execution.
+ ///
+ /// This method posts a FAILED_EVT and sets the current state to END_ST.
+ /// It is called internally when a model violation occurs. Violations are
+ /// any sort of inconsistency such as attempting to reference an invalid
+ /// state, or if the next event is not valid for the current state, or a
+ /// state handler throws an uncaught exception.
+ ///
+ /// @param explanation is text detailing the reason for aborting.
+ void abortModel(const std::string& explanation);
+
+ /// @brief Sets the current state to the given state value.
+ ///
+ /// This updates the model's notion of the current state and is the
+ /// state whose handler will be executed on the next iteration of the run
+ /// loop. This is intended primarily for internal use and testing. It is
+ /// unlikely that transitioning to a new state without a new event is of
+ /// much use.
+ ///
+ /// @param state the new value to assign to the current state.
+ ///
+ /// @throw StateModelError if the state is invalid.
+ void setState(unsigned int state);
+
+ /// @brief Sets the next event to the given event value.
+ ///
+ /// This updates the model's notion of the next event and is the
+ /// event that will be passed into the current state's handler on the next
+ /// iteration of the run loop.
+ ///
+ /// @param event the numeric event value to post as the next event.
+ ///
+ /// @throw StateModelError if the event is undefined
+ void postNextEvent(unsigned int event);
+
+ /// @brief Checks if on entry flag is true.
+ ///
+ /// This method acts as a one-shot test of whether or not the model is
+ /// transitioning into a new state. It returns true if the on-entry flag
+ /// is true upon entering this method and will set the flag false prior
+ /// to exit. It may be used within state handlers to perform steps that
+ /// should only occur upon entry into the state.
+ ///
+ /// @return true if the on entry flag is true, false otherwise.
+ bool doOnEntry();
+
+ /// @brief Checks if on exit flag is true.
+ ///
+ /// This method acts as a one-shot test of whether or not the model is
+ /// transitioning out of the current state. It returns true if the
+ /// on-exit flag is true upon entering this method and will set the flag
+ /// false prior to exiting. It may be used within state handlers to perform
+ /// steps that should only occur upon exiting out of the current state.
+ ///
+ /// @return true if the on entry flag is true, false otherwise.
+ bool doOnExit();
+
+public:
+ /// @brief Fetches the model's current state.
+ ///
+ /// This returns the model's notion of the current state. It is the
+ /// state whose handler will be executed on the next iteration of the run
+ /// loop.
+ ///
+ /// @return An unsigned int representing the current state.
+ unsigned int getCurrState() const;
+
+ /// @brief Fetches the model's previous state.
+ ///
+ /// @return An unsigned int representing the previous state.
+ unsigned int getPrevState() const;
+
+ /// @brief Fetches the model's last event.
+ ///
+ /// @return An unsigned int representing the last event.
+ unsigned int getLastEvent() const;
+
+ /// @brief Fetches the model's next event.
+ ///
+ /// This returns the model's notion of the next event. It is the
+ /// event that will be passed into the current state's handler on the next
+ /// iteration of the run loop.
+ ///
+ /// @return An unsigned int representing the next event.
+ unsigned int getNextEvent() const;
+
+ /// @brief Returns whether or not the model is new.
+ ///
+ /// @return Boolean true if the model has not been started.
+ bool isModelNew() const;
+
+ /// @brief Returns whether or not the model is running.
+ ///
+ /// @return Boolean true if the model has been started but has not yet
+ /// ended.
+ bool isModelRunning() const;
+
+ /// @brief Returns whether or not the model is waiting.
+ ///
+ /// @return Boolean true if the model is running but is waiting for an
+ /// external event for resumption.
+ bool isModelWaiting() const;
+
+ /// @brief Returns whether or not the model has finished execution.
+ ///
+ /// @return Boolean true if the model has reached the END_ST.
+ bool isModelDone() const;
+
+ /// @brief Returns whether or not the model failed.
+ ///
+ /// @return Boolean true if the model has reached the END_ST and the last
+ /// event indicates a model violation, FAILED_EVT.
+ bool didModelFail() const;
+
+ /// @brief Fetches the label associated with an event value.
+ ///
+ /// @param event is the numeric event value for which the label is desired.
+ ///
+ /// @return Returns a string containing the event label or
+ /// LabeledValueSet::UNDEFINED_LABEL if the value is undefined.
+ std::string getEventLabel(const int event) const;
+
+ /// @brief Fetches the label associated with an state value.
+ ///
+ /// @param state is the numeric state value for which the label is desired.
+ ///
+ /// @return Returns a const char* containing the state label or
+ /// LabeledValueSet::UNDEFINED_LABEL if the value is undefined.
+ std::string getStateLabel(const int state) const;
+
+ /// @brief Convenience method which returns a string rendition of the
+ /// current state and next event.
+ ///
+ /// The string will be of the form:
+ ///
+ /// current state: [ {state} {label} ] next event: [ {event} {label} ]
+ ///
+ /// @return Returns a std::string of the format described above.
+ std::string getContextStr() const;
+
+ /// @brief Convenience method which returns a string rendition of the
+ /// previous state and last event.
+ ///
+ /// The string will be of the form:
+ ///
+ /// previous state: [ {state} {label} ] last event: [ {event} {label} ]
+ ///
+ /// @return Returns a std::string of the format described above.
+ std::string getPrevContextStr() const;
+
+private:
+ /// @brief The dictionary of valid events.
+ LabeledValueSet events_;
+
+ /// @brief The dictionary of valid states.
+ StateSet states_;
+
+ /// @brief Indicates if the event and state dictionaries have been initted.
+ bool dictionaries_initted_;
+
+ /// @brief The current state within the model's state model.
+ unsigned int curr_state_;
+
+ /// @brief The previous state within the model's state model.
+ unsigned int prev_state_;
+
+ /// @brief The event last processed by the model.
+ unsigned int last_event_;
+
+ /// @brief The event the model should process next.
+ unsigned int next_event_;
+
+ /// @brief Indicates if state entry logic should be executed.
+ bool on_entry_flag_;
+
+ /// @brief Indicates if state exit logic should be executed.
+ bool on_exit_flag_;
+};
+
+/// @brief Defines a pointer to a StateModel.
+typedef boost::shared_ptr<StateModel> StateModelPtr;
+
+} // namespace isc::d2
+} // namespace isc
+#endif
diff --git a/src/bin/d2/tests/.gitignore b/src/bin/d2/tests/.gitignore
new file mode 100644
index 0000000..3e1a1b7
--- /dev/null
+++ b/src/bin/d2/tests/.gitignore
@@ -0,0 +1,2 @@
+/d2_unittests
+/test_data_files_config.h
diff --git a/src/bin/d2/tests/Makefile.am b/src/bin/d2/tests/Makefile.am
new file mode 100644
index 0000000..8b5e351
--- /dev/null
+++ b/src/bin/d2/tests/Makefile.am
@@ -0,0 +1,111 @@
+PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
+
+PYTESTS = d2_test.py
+EXTRA_DIST = $(PYTESTS)
+
+# Explicitly specify paths to dynamic libraries required by loadable python
+# modules. That is required on Mac OS systems. Otherwise we will get exception
+# about python not being able to load liblog library.
+LIBRARY_PATH_PLACEHOLDER =
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local:
+ for pytest in $(PYTESTS) ; do \
+ echo Running test: $$pytest ; \
+ PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
+ B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
+ $(LIBRARY_PATH_PLACEHOLDER) \
+ $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+ done
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
+AM_CPPFLAGS += -I$(top_srcdir)/src/bin
+AM_CPPFLAGS += -I$(top_builddir)/src/lib/cc
+AM_CPPFLAGS += -I$(top_srcdir)/src/lib/asiolink
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_top_srcdir)/src/lib/testutils/testdata\"
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/d2/tests\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+if USE_CLANGPP
+# Disable unused parameter warning caused by some Boost headers when compiling with clang
+AM_CXXFLAGS += -Wno-unused-parameter
+endif
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+
+TESTS += d2_unittests
+
+d2_unittests_SOURCES = ../d2_asio.h
+d2_unittests_SOURCES += ../d2_log.h ../d2_log.cc
+d2_unittests_SOURCES += ../d_process.h
+d2_unittests_SOURCES += ../d_controller.cc ../d2_controller.h
+d2_unittests_SOURCES += ../d2_process.cc ../d2_process.h
+d2_unittests_SOURCES += ../d2_controller.cc ../d2_controller.h
+d2_unittests_SOURCES += ../d_cfg_mgr.cc ../d_cfg_mgr.h
+d2_unittests_SOURCES += ../d2_config.cc ../d2_config.h
+d2_unittests_SOURCES += ../d2_cfg_mgr.cc ../d2_cfg_mgr.h
+d2_unittests_SOURCES += ../d2_queue_mgr.cc ../d2_queue_mgr.h
+d2_unittests_SOURCES += ../d2_update_message.cc ../d2_update_message.h
+d2_unittests_SOURCES += ../d2_update_mgr.cc ../d2_update_mgr.h
+d2_unittests_SOURCES += ../d2_zone.cc ../d2_zone.h
+d2_unittests_SOURCES += ../dns_client.cc ../dns_client.h
+d2_unittests_SOURCES += ../labeled_value.cc ../labeled_value.h
+d2_unittests_SOURCES += ../nc_add.cc ../nc_add.h
+d2_unittests_SOURCES += ../nc_remove.cc ../nc_remove.h
+d2_unittests_SOURCES += ../nc_trans.cc ../nc_trans.h
+d2_unittests_SOURCES += ../state_model.cc ../state_model.h
+d2_unittests_SOURCES += d_test_stubs.cc d_test_stubs.h
+d2_unittests_SOURCES += d2_unittests.cc
+d2_unittests_SOURCES += d2_process_unittests.cc
+d2_unittests_SOURCES += d_controller_unittests.cc
+d2_unittests_SOURCES += d2_controller_unittests.cc
+d2_unittests_SOURCES += d_cfg_mgr_unittests.cc
+d2_unittests_SOURCES += d2_cfg_mgr_unittests.cc
+d2_unittests_SOURCES += d2_queue_mgr_unittests.cc
+d2_unittests_SOURCES += d2_update_message_unittests.cc
+d2_unittests_SOURCES += d2_update_mgr_unittests.cc
+d2_unittests_SOURCES += d2_zone_unittests.cc
+d2_unittests_SOURCES += dns_client_unittests.cc
+d2_unittests_SOURCES += labeled_value_unittests.cc
+d2_unittests_SOURCES += nc_add_unittests.cc
+d2_unittests_SOURCES += nc_remove_unittests.cc
+d2_unittests_SOURCES += nc_test_utils.cc nc_test_utils.h
+d2_unittests_SOURCES += nc_trans_unittests.cc
+d2_unittests_SOURCES += state_model_unittests.cc
+nodist_d2_unittests_SOURCES = ../d2_messages.h ../d2_messages.cc
+
+d2_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+d2_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+d2_unittests_LDADD = $(GTEST_LDADD)
+d2_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+d2_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+
+endif
+
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/bin/d2/tests/d2_cfg_mgr_unittests.cc b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc
new file mode 100644
index 0000000..645bbae
--- /dev/null
+++ b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc
@@ -0,0 +1,1260 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/module_spec.h>
+#include <d2/d2_config.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d_test_stubs.h>
+#include <test_data_files_config.h>
+
+#include <boost/foreach.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+std::string specfile(const std::string& name) {
+ return (std::string(D2_SRC_DIR) + "/" + name);
+}
+
+/// @brief Test fixture class for testing D2CfgMgr class.
+/// It maintains an member instance of D2CfgMgr and provides methods for
+/// converting JSON strings to configuration element sets, checking parse
+/// results, and accessing the configuration context.
+class D2CfgMgrTest : public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ D2CfgMgrTest():cfg_mgr_(new D2CfgMgr) {
+ }
+
+ /// @brief Destructor
+ ~D2CfgMgrTest() {
+ }
+
+ /// @brief Configuration manager instance.
+ D2CfgMgrPtr cfg_mgr_;
+};
+
+/// @brief Tests that the spec file is valid.
+/// Verifies that the BIND10 DHCP-DDNS configuration specification file
+// is valid.
+TEST(D2SpecTest, basicSpec) {
+ ASSERT_NO_THROW(isc::config::
+ moduleSpecFromFile(specfile("dhcp-ddns.spec")));
+}
+
+/// @brief Convenience function which compares the contents of the given
+/// DnsServerInfo against the given set of values.
+///
+/// It is structured in such a way that each value is checked, and output
+/// is generate for all that do not match.
+///
+/// @param server is a pointer to the server to check against.
+/// @param hostname is the value to compare against server's hostname_.
+/// @param ip_address is the string value to compare against server's
+/// ip_address_.
+/// @param port is the value to compare against server's port.
+///
+/// @return returns true if there is a match across the board, otherwise it
+/// returns false.
+bool checkServer(DnsServerInfoPtr server, const char* hostname,
+ const char *ip_address, uint32_t port)
+{
+ // Return value, assume its a match.
+ bool result = true;
+
+ if (!server) {
+ EXPECT_TRUE(server);
+ return false;
+ }
+
+ // Check hostname.
+ if (server->getHostname() != hostname) {
+ EXPECT_EQ(hostname, server->getHostname());
+ result = false;
+ }
+
+ // Check IP address.
+ if (server->getIpAddress().toText() != ip_address) {
+ EXPECT_EQ(ip_address, server->getIpAddress().toText());
+ result = false;
+ }
+
+ // Check port.
+ if (server->getPort() != port) {
+ EXPECT_EQ (port, server->getPort());
+ result = false;
+ }
+
+ return (result);
+}
+
+/// @brief Convenience function which compares the contents of the given
+/// TSIGKeyInfo against the given set of values.
+///
+/// It is structured in such a way that each value is checked, and output
+/// is generate for all that do not match.
+///
+/// @param key is a pointer to the key to check against.
+/// @param name is the value to compare against key's name_.
+/// @param algorithm is the string value to compare against key's algorithm.
+/// @param secret is the value to compare against key's secret.
+///
+/// @return returns true if there is a match across the board, otherwise it
+/// returns false.
+bool checkKey(TSIGKeyInfoPtr key, const char* name,
+ const char *algorithm, const char* secret)
+{
+ // Return value, assume its a match.
+ bool result = true;
+ if (!key) {
+ EXPECT_TRUE(key);
+ return false;
+ }
+
+ // Check name.
+ if (key->getName() != name) {
+ EXPECT_EQ(name, key->getName());
+ result = false;
+ }
+
+ // Check algorithm.
+ if (key->getAlgorithm() != algorithm) {
+ EXPECT_EQ(algorithm, key->getAlgorithm());
+ result = false;
+ }
+
+ // Check secret.
+ if (key->getSecret() != secret) {
+ EXPECT_EQ (secret, key->getSecret());
+ result = false;
+ }
+
+ return (result);
+}
+
+/// @brief Test fixture class for testing DnsServerInfo parsing.
+class TSIGKeyInfoTest : public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ TSIGKeyInfoTest() {
+ reset();
+ }
+
+ /// @brief Destructor
+ ~TSIGKeyInfoTest() {
+ }
+
+ /// @brief Wipe out the current storage and parser and replace
+ /// them with new ones.
+ void reset() {
+ keys_.reset(new TSIGKeyInfoMap());
+ parser_.reset(new TSIGKeyInfoParser("test", keys_));
+ }
+
+ /// @brief Storage for "committing" keys.
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Pointer to the current parser instance.
+ isc::dhcp::ParserPtr parser_;
+};
+
+/// @brief Test fixture class for testing DnsServerInfo parsing.
+class DnsServerInfoTest : public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ DnsServerInfoTest() {
+ reset();
+ }
+
+ /// @brief Destructor
+ ~DnsServerInfoTest() {
+ }
+
+ /// @brief Wipe out the current storage and parser and replace
+ /// them with new ones.
+ void reset() {
+ servers_.reset(new DnsServerInfoStorage());
+ parser_.reset(new DnsServerInfoParser("test", servers_));
+ }
+
+ /// @brief Storage for "committing" servers.
+ DnsServerInfoStoragePtr servers_;
+
+ /// @brief Pointer to the current parser instance.
+ isc::dhcp::ParserPtr parser_;
+};
+
+
+/// @brief Test fixture class for testing DDnsDomain parsing.
+class DdnsDomainTest : public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ DdnsDomainTest() {
+ reset();
+ }
+
+ /// @brief Destructor
+ ~DdnsDomainTest() {
+ }
+
+ /// @brief Wipe out the current storage and parser and replace
+ /// them with new ones.
+ void reset() {
+ keys_.reset(new TSIGKeyInfoMap());
+ domains_.reset(new DdnsDomainMap());
+ parser_.reset(new DdnsDomainParser("test", domains_, keys_));
+ }
+
+ /// @brief Add TSIGKeyInfos to the key map
+ ///
+ /// @param name the name of the key
+ /// @param algorithm the algorithm of the key
+ /// @param secret the secret value of the key
+ void addKey(const std::string& name, const std::string& algorithm,
+ const std::string& secret) {
+ TSIGKeyInfoPtr key_info(new TSIGKeyInfo(name, algorithm, secret));
+ (*keys_)[name]=key_info;
+ }
+
+ /// @brief Storage for "committing" domains.
+ DdnsDomainMapPtr domains_;
+
+ /// @brief Storage for TSIGKeys
+ TSIGKeyInfoMapPtr keys_;
+
+ /// @brief Pointer to the current parser instance.
+ isc::dhcp::ParserPtr parser_;
+};
+
+/// @brief Tests the enforcement of data validation when parsing TSIGKeyInfos.
+/// It verifies that:
+/// 1. Name cannot be blank.
+/// 2. Algorithm cannot be blank.
+/// 3. Secret cannot be blank.
+/// @TODO TSIG keys are not fully functional. Only basic validation is
+/// currently supported. This test will need to expand as they evolve.
+TEST_F(TSIGKeyInfoTest, invalidEntry) {
+ // Config with a blank name entry.
+ std::string config = "{"
+ " \"name\": \"\" , "
+ " \"algorithm\": \"md5\" , "
+ " \"secret\": \"0123456789\" "
+ "}";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that build succeeds but commit fails on blank name.
+ EXPECT_NO_THROW(parser_->build(config_set_));
+ EXPECT_THROW(parser_->commit(), D2CfgError);
+
+ // Config with a blank algorithm entry.
+ config = "{"
+ " \"name\": \"d2_key_one\" , "
+ " \"algorithm\": \"\" , "
+ " \"secret\": \"0123456789\" "
+ "}";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that build succeeds but commit fails on blank algorithm.
+ EXPECT_NO_THROW(parser_->build(config_set_));
+ EXPECT_THROW(parser_->commit(), D2CfgError);
+
+ // Config with a blank secret entry.
+ config = "{"
+ " \"name\": \"d2_key_one\" , "
+ " \"algorithm\": \"md5\" , "
+ " \"secret\": \"\" "
+ "}";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that build succeeds but commit fails on blank secret.
+ EXPECT_NO_THROW(parser_->build(config_set_));
+ EXPECT_THROW(parser_->commit(), D2CfgError);
+}
+
+/// @brief Verifies that TSIGKeyInfo parsing creates a proper TSIGKeyInfo
+/// when given a valid combination of entries.
+TEST_F(TSIGKeyInfoTest, validEntry) {
+ // Valid entries for TSIG key, all items are required.
+ std::string config = "{"
+ " \"name\": \"d2_key_one\" , "
+ " \"algorithm\": \"md5\" , "
+ " \"secret\": \"0123456789\" "
+ "}";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that it builds and commits without throwing.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_NO_THROW(parser_->commit());
+
+ // Verify the correct number of keys are present
+ int count = keys_->size();
+ EXPECT_EQ(1, count);
+
+ // Find the key and retrieve it.
+ TSIGKeyInfoMap::iterator gotit = keys_->find("d2_key_one");
+ ASSERT_TRUE(gotit != keys_->end());
+ TSIGKeyInfoPtr& key = gotit->second;
+
+ // Verify the key contents.
+ EXPECT_TRUE(checkKey(key, "d2_key_one", "md5", "0123456789"));
+}
+
+/// @brief Verifies that attempting to parse an invalid list of TSIGKeyInfo
+/// entries is detected.
+TEST_F(TSIGKeyInfoTest, invalidTSIGKeyList) {
+ // Construct a list of keys with an invalid key entry.
+ std::string config = "["
+
+ " { \"name\": \"key1\" , "
+ " \"algorithm\": \"algo1\" ,"
+ " \"secret\": \"secret11\" "
+ " },"
+ " { \"name\": \"key2\" , "
+ " \"algorithm\": \"\" ,"
+ " \"secret\": \"secret12\" "
+ " },"
+ " { \"name\": \"key3\" , "
+ " \"algorithm\": \"algo3\" ,"
+ " \"secret\": \"secret13\" "
+ " }"
+ " ]";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Create the list parser.
+ isc::dhcp::ParserPtr parser;
+ ASSERT_NO_THROW(parser.reset(new TSIGKeyInfoListParser("test", keys_)));
+
+ // Verify that the list builds without errors.
+ ASSERT_NO_THROW(parser->build(config_set_));
+
+ // Verify that the list commit fails.
+ EXPECT_THROW(parser->commit(), D2CfgError);
+}
+
+/// @brief Verifies that attempting to parse an invalid list of TSIGKeyInfo
+/// entries is detected.
+TEST_F(TSIGKeyInfoTest, duplicateTSIGKey) {
+ // Construct a list of keys with an invalid key entry.
+ std::string config = "["
+
+ " { \"name\": \"key1\" , "
+ " \"algorithm\": \"algo1\" ,"
+ " \"secret\": \"secret11\" "
+ " },"
+ " { \"name\": \"key2\" , "
+ " \"algorithm\": \"algo2\" ,"
+ " \"secret\": \"secret12\" "
+ " },"
+ " { \"name\": \"key1\" , "
+ " \"algorithm\": \"algo3\" ,"
+ " \"secret\": \"secret13\" "
+ " }"
+ " ]";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Create the list parser.
+ isc::dhcp::ParserPtr parser;
+ ASSERT_NO_THROW(parser.reset(new TSIGKeyInfoListParser("test", keys_)));
+
+ // Verify that the list builds without errors.
+ ASSERT_NO_THROW(parser->build(config_set_));
+
+ // Verify that the list commit fails.
+ EXPECT_THROW(parser->commit(), D2CfgError);
+}
+
+/// @brief Verifies a valid list of TSIG Keys parses correctly.
+TEST_F(TSIGKeyInfoTest, validTSIGKeyList) {
+ // Construct a valid list of keys.
+ std::string config = "["
+
+ " { \"name\": \"key1\" , "
+ " \"algorithm\": \"algo1\" ,"
+ " \"secret\": \"secret1\" "
+ " },"
+ " { \"name\": \"key2\" , "
+ " \"algorithm\": \"algo2\" ,"
+ " \"secret\": \"secret2\" "
+ " },"
+ " { \"name\": \"key3\" , "
+ " \"algorithm\": \"algo3\" ,"
+ " \"secret\": \"secret3\" "
+ " }"
+ " ]";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the list builds and commits without errors.
+ // Create the list parser.
+ isc::dhcp::ParserPtr parser;
+ ASSERT_NO_THROW(parser.reset(new TSIGKeyInfoListParser("test", keys_)));
+ ASSERT_NO_THROW(parser->build(config_set_));
+ ASSERT_NO_THROW(parser->commit());
+
+ // Verify the correct number of keys are present
+ int count = keys_->size();
+ ASSERT_EQ(3, count);
+
+ // Find the 1st key and retrieve it.
+ TSIGKeyInfoMap::iterator gotit = keys_->find("key1");
+ ASSERT_TRUE(gotit != keys_->end());
+ TSIGKeyInfoPtr& key = gotit->second;
+
+ // Verify the key contents.
+ EXPECT_TRUE(checkKey(key, "key1", "algo1", "secret1"));
+
+ // Find the 2nd key and retrieve it.
+ gotit = keys_->find("key2");
+ ASSERT_TRUE(gotit != keys_->end());
+ key = gotit->second;
+
+ // Verify the key contents.
+ EXPECT_TRUE(checkKey(key, "key2", "algo2", "secret2"));
+
+ // Find the 3rd key and retrieve it.
+ gotit = keys_->find("key3");
+ ASSERT_TRUE(gotit != keys_->end());
+ key = gotit->second;
+
+ // Verify the key contents.
+ EXPECT_TRUE(checkKey(key, "key3", "algo3", "secret3"));
+}
+
+/// @brief Tests the enforcement of data validation when parsing DnsServerInfos.
+/// It verifies that:
+/// 1. Specifying both a hostname and an ip address is not allowed.
+/// 2. Specifying both blank a hostname and blank ip address is not allowed.
+/// 3. Specifying a negative port number is not allowed.
+TEST_F(DnsServerInfoTest, invalidEntry) {
+ // Create a config in which both host and ip address are supplied.
+ // Verify that it builds without throwing but commit fails.
+ std::string config = "{ \"hostname\": \"pegasus.tmark\", "
+ " \"ip_address\": \"127.0.0.1\" } ";
+ ASSERT_TRUE(fromJSON(config));
+ EXPECT_NO_THROW(parser_->build(config_set_));
+ EXPECT_THROW(parser_->commit(), D2CfgError);
+
+ // Neither host nor ip address supplied
+ // Verify that it builds without throwing but commit fails.
+ config = "{ \"hostname\": \"\", "
+ " \"ip_address\": \"\" } ";
+ ASSERT_TRUE(fromJSON(config));
+ EXPECT_NO_THROW(parser_->build(config_set_));
+ EXPECT_THROW(parser_->commit(), D2CfgError);
+
+ // Create a config with a negative port number.
+ // Verify that build fails.
+ config = "{ \"ip_address\": \"192.168.5.6\" ,"
+ " \"port\": -100 }";
+ ASSERT_TRUE(fromJSON(config));
+ EXPECT_THROW (parser_->build(config_set_), isc::BadValue);
+}
+
+
+/// @brief Verifies that DnsServerInfo parsing creates a proper DnsServerInfo
+/// when given a valid combination of entries.
+/// It verifies that:
+/// 1. A DnsServerInfo entry is correctly made, when given only a hostname.
+/// 2. A DnsServerInfo entry is correctly made, when given ip address and port.
+/// 3. A DnsServerInfo entry is correctly made, when given only an ip address.
+TEST_F(DnsServerInfoTest, validEntry) {
+ // Valid entries for dynamic host
+ std::string config = "{ \"hostname\": \"pegasus.tmark\" }";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that it builds and commits without throwing.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_NO_THROW(parser_->commit());
+
+ // Verify the correct number of servers are present
+ int count = servers_->size();
+ EXPECT_EQ(1, count);
+
+ // Verify the server exists and has the correct values.
+ DnsServerInfoPtr server = (*servers_)[0];
+ EXPECT_TRUE(checkServer(server, "pegasus.tmark",
+ DnsServerInfo::EMPTY_IP_STR,
+ DnsServerInfo::STANDARD_DNS_PORT));
+
+ // Start over for a new test.
+ reset();
+
+ // Valid entries for static ip
+ config = " { \"ip_address\": \"127.0.0.1\" , "
+ " \"port\": 100 }";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that it builds and commits without throwing.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_NO_THROW(parser_->commit());
+
+ // Verify the correct number of servers are present
+ count = servers_->size();
+ EXPECT_EQ(1, count);
+
+ // Verify the server exists and has the correct values.
+ server = (*servers_)[0];
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.1", 100));
+
+ // Start over for a new test.
+ reset();
+
+ // Valid entries for static ip, no port
+ config = " { \"ip_address\": \"192.168.2.5\" }";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that it builds and commits without throwing.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_NO_THROW(parser_->commit());
+
+ // Verify the correct number of servers are present
+ count = servers_->size();
+ EXPECT_EQ(1, count);
+
+ // Verify the server exists and has the correct values.
+ server = (*servers_)[0];
+ EXPECT_TRUE(checkServer(server, "", "192.168.2.5",
+ DnsServerInfo::STANDARD_DNS_PORT));
+}
+
+/// @brief Verifies that attempting to parse an invalid list of DnsServerInfo
+/// entries is detected.
+TEST_F(ConfigParseTest, invalidServerList) {
+ // Construct a list of servers with an invalid server entry.
+ std::string config = "[ { \"hostname\": \"one.tmark\" }, "
+ "{ \"hostname\": \"\" }, "
+ "{ \"hostname\": \"three.tmark\" } ]";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Create the server storage and list parser.
+ DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+ isc::dhcp::ParserPtr parser;
+ ASSERT_NO_THROW(parser.reset(new DnsServerInfoListParser("test", servers)));
+
+ // Verify that the list builds without errors.
+ ASSERT_NO_THROW(parser->build(config_set_));
+
+ // Verify that the list commit fails.
+ EXPECT_THROW(parser->commit(), D2CfgError);
+}
+
+/// @brief Verifies that a list of DnsServerInfo entries parses correctly given
+/// a valid configuration.
+TEST_F(ConfigParseTest, validServerList) {
+ // Create a valid list of servers.
+ std::string config = "[ { \"hostname\": \"one.tmark\" }, "
+ "{ \"hostname\": \"two.tmark\" }, "
+ "{ \"hostname\": \"three.tmark\" } ]";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Create the server storage and list parser.
+ DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+ isc::dhcp::ParserPtr parser;
+ ASSERT_NO_THROW(parser.reset(new DnsServerInfoListParser("test", servers)));
+
+ // Verfiy that the list builds and commits without error.
+ ASSERT_NO_THROW(parser->build(config_set_));
+ ASSERT_NO_THROW(parser->commit());
+
+ // Verify that the server storage contains the correct number of servers.
+ int count = servers->size();
+ EXPECT_EQ(3, count);
+
+ // Verify the first server exists and has the correct values.
+ DnsServerInfoPtr server = (*servers)[0];
+ EXPECT_TRUE(checkServer(server, "one.tmark", DnsServerInfo::EMPTY_IP_STR,
+ DnsServerInfo::STANDARD_DNS_PORT));
+
+ // Verify the second server exists and has the correct values.
+ server = (*servers)[1];
+ EXPECT_TRUE(checkServer(server, "two.tmark", DnsServerInfo::EMPTY_IP_STR,
+ DnsServerInfo::STANDARD_DNS_PORT));
+
+ // Verify the third server exists and has the correct values.
+ server = (*servers)[2];
+ EXPECT_TRUE(checkServer(server, "three.tmark", DnsServerInfo::EMPTY_IP_STR,
+ DnsServerInfo::STANDARD_DNS_PORT));
+}
+
+/// @brief Tests the enforcement of data validation when parsing DdnsDomains.
+/// It verifies that:
+/// 1. Domain storage cannot be null when constructing a DdnsDomainParser.
+/// 2. The name entry is not optional.
+/// 3. The server list man not be empty.
+/// 4. That a mal-formed server entry is detected.
+/// 5. That an undefined key name is detected.
+TEST_F(DdnsDomainTest, invalidDdnsDomainEntry) {
+ // Verify that attempting to construct the parser with null storage fails.
+ DdnsDomainMapPtr domains;
+ ASSERT_THROW(isc::dhcp::ParserPtr(
+ new DdnsDomainParser("test", domains, keys_)), D2CfgError);
+
+ // Create a domain configuration without a name
+ std::string config = "{ \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" , "
+ " \"port\": 100 },"
+ " { \"ip_address\": \"127.0.0.2\" , "
+ " \"port\": 200 },"
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the domain configuration builds but commit fails.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_THROW(parser_->commit(), isc::dhcp::DhcpConfigError);
+
+ // Create a domain configuration with an empty server list.
+ config = "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " ] } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the domain configuration build fails.
+ ASSERT_THROW(parser_->build(config_set_), D2CfgError);
+
+ // Create a domain configuration with a mal-formed server entry.
+ config = "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": -1 } ] } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the domain configuration build fails.
+ ASSERT_THROW(parser_->build(config_set_), isc::BadValue);
+
+ // Create a domain configuration without an defined key name
+ config = "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the domain configuration build succeeds but commit fails.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_THROW(parser_->commit(), D2CfgError);
+}
+
+/// @brief Verifies the basics of parsing DdnsDomains.
+/// It verifies that:
+/// 1. Valid construction of DdnsDomainParser functions.
+/// 2. Given a valid, configuration entry, DdnsDomainParser parses
+/// correctly.
+/// (It indirectly verifies the operation of DdnsDomainMap).
+TEST_F(DdnsDomainTest, ddnsDomainParsing) {
+ // Create a valid domain configuration entry containing three valid
+ // servers.
+ std::string config =
+ "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" , "
+ " \"port\": 100 },"
+ " { \"ip_address\": \"127.0.0.2\" , "
+ " \"port\": 200 },"
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Add a TSIG key to the test key map, so key validation will pass.
+ addKey("d2_key.tmark.org", "md5", "0123456789");
+
+ // Verify that the domain configuration builds and commits without error.
+ ASSERT_NO_THROW(parser_->build(config_set_));
+ ASSERT_NO_THROW(parser_->commit());
+
+ // Verify that the domain storage contains the correct number of domains.
+ int count = domains_->size();
+ EXPECT_EQ(1, count);
+
+ // Verify that the expected domain exists and can be retrieved from
+ // the storage.
+ DdnsDomainMap::iterator gotit = domains_->find("tmark.org");
+ ASSERT_TRUE(gotit != domains_->end());
+ DdnsDomainPtr& domain = gotit->second;
+
+ // Verify the name and key_name values.
+ EXPECT_EQ("tmark.org", domain->getName());
+ EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+
+ // Verify that the server list exists and contains the correct number of
+ // servers.
+ const DnsServerInfoStoragePtr& servers = domain->getServers();
+ EXPECT_TRUE(servers);
+ count = servers->size();
+ EXPECT_EQ(3, count);
+
+ // Fetch each server and verify its contents.
+ DnsServerInfoPtr server = (*servers)[0];
+ EXPECT_TRUE(server);
+
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.1", 100));
+
+ server = (*servers)[1];
+ EXPECT_TRUE(server);
+
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.2", 200));
+
+ server = (*servers)[2];
+ EXPECT_TRUE(server);
+
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.3", 300));
+}
+
+/// @brief Tests the fundamentals of parsing DdnsDomain lists.
+/// This test verifies that given a valid domain list configuration
+/// it will accurately parse and populate each domain in the list.
+TEST_F(DdnsDomainTest, DdnsDomainListParsing) {
+ // Create a valid domain list configuration, with two domains
+ // that have three servers each.
+ std::string config =
+ "[ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" , "
+ " \"port\": 100 },"
+ " { \"ip_address\": \"127.0.0.2\" , "
+ " \"port\": 200 },"
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } "
+ ", "
+ "{ \"name\": \"billcat.net\" , "
+ " \"key_name\": \"d2_key.billcat.net\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.4\" , "
+ " \"port\": 400 },"
+ " { \"ip_address\": \"127.0.0.5\" , "
+ " \"port\": 500 },"
+ " { \"ip_address\": \"127.0.0.6\" , "
+ " \"port\": 600 } ] } "
+ "] ";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Add keys to key map so key validation passes.
+ addKey("d2_key.tmark.org", "algo1", "secret1");
+ addKey("d2_key.billcat.net", "algo2", "secret2");
+
+ // Create the list parser
+ isc::dhcp::ParserPtr list_parser;
+ ASSERT_NO_THROW(list_parser.reset(
+ new DdnsDomainListParser("test", domains_, keys_)));
+
+ // Verify that the domain configuration builds and commits without error.
+ ASSERT_NO_THROW(list_parser->build(config_set_));
+ ASSERT_NO_THROW(list_parser->commit());
+
+ // Verify that the domain storage contains the correct number of domains.
+ int count = domains_->size();
+ EXPECT_EQ(2, count);
+
+ // Verify that the first domain exists and can be retrieved.
+ DdnsDomainMap::iterator gotit = domains_->find("tmark.org");
+ ASSERT_TRUE(gotit != domains_->end());
+ DdnsDomainPtr& domain = gotit->second;
+
+ // Verify the name and key_name values of the first domain.
+ EXPECT_EQ("tmark.org", domain->getName());
+ EXPECT_EQ("d2_key.tmark.org", domain->getKeyName());
+
+ // Verify the each of the first domain's servers
+ DnsServerInfoStoragePtr servers = domain->getServers();
+ EXPECT_TRUE(servers);
+ count = servers->size();
+ EXPECT_EQ(3, count);
+
+ DnsServerInfoPtr server = (*servers)[0];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.1", 100));
+
+ server = (*servers)[1];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.2", 200));
+
+ server = (*servers)[2];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.3", 300));
+
+ // Verify second domain
+ gotit = domains_->find("billcat.net");
+ ASSERT_TRUE(gotit != domains_->end());
+ domain = gotit->second;
+
+ // Verify the name and key_name values of the second domain.
+ EXPECT_EQ("billcat.net", domain->getName());
+ EXPECT_EQ("d2_key.billcat.net", domain->getKeyName());
+
+ // Verify the each of second domain's servers
+ servers = domain->getServers();
+ EXPECT_TRUE(servers);
+ count = servers->size();
+ EXPECT_EQ(3, count);
+
+ server = (*servers)[0];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.4", 400));
+
+ server = (*servers)[1];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.5", 500));
+
+ server = (*servers)[2];
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(checkServer(server, "", "127.0.0.6", 600));
+}
+
+/// @brief Tests that a domain list configuration cannot contain duplicates.
+TEST_F(DdnsDomainTest, duplicateDomain) {
+ // Create a domain list configuration that contains two domains with
+ // the same name.
+ std::string config =
+ "[ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } "
+ ", "
+ "{ \"name\": \"tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.3\" , "
+ " \"port\": 300 } ] } "
+ "] ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Create the list parser
+ isc::dhcp::ParserPtr list_parser;
+ ASSERT_NO_THROW(list_parser.reset(
+ new DdnsDomainListParser("test", domains_, keys_)));
+
+ // Verify that the parse build succeeds but the commit fails.
+ ASSERT_NO_THROW(list_parser->build(config_set_));
+ ASSERT_THROW(list_parser->commit(), D2CfgError);
+}
+
+/// @brief Tests construction of D2CfgMgr
+/// This test verifies that a D2CfgMgr constructs properly.
+TEST(D2CfgMgr, construction) {
+ D2CfgMgr *cfg_mgr = NULL;
+
+ // Verify that configuration manager constructions without error.
+ ASSERT_NO_THROW(cfg_mgr = new D2CfgMgr());
+
+ // Verify that the context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr->getD2CfgContext());
+ EXPECT_TRUE(context);
+
+ // Verify that the forward manager can be retrieved and is not null.
+ EXPECT_TRUE(context->getForwardMgr());
+
+ // Verify that the reverse manager can be retrieved and is not null.
+ EXPECT_TRUE(context->getReverseMgr());
+
+ // Verify that the manager can be destructed without error.
+ EXPECT_NO_THROW(delete cfg_mgr);
+}
+
+/// @brief Tests the parsing of a complete, valid DHCP-DDNS configuration.
+/// This tests passes the configuration into an instance of D2CfgMgr just
+/// as it would be done by d2_process in response to a configuration update
+/// event.
+TEST_F(D2CfgMgrTest, fullConfig) {
+ // Create a configuration with all of application level parameters, plus
+ // both the forward and reverse ddns managers. Both managers have two
+ // domains with three servers per domain.
+ std::string config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": ["
+ "{"
+ " \"name\": \"d2_key.tmark.org\" , "
+ " \"algorithm\": \"md5\" , "
+ " \"secret\": \"ssh-dont-tell\" "
+ "},"
+ "{"
+ " \"name\": \"d2_key.billcat.net\" , "
+ " \"algorithm\": \"md5\" , "
+ " \"secret\": \"ollie-ollie-in-free\" "
+ "}"
+ "],"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"one.tmark\" } , "
+ " { \"hostname\": \"two.tmark\" } , "
+ " { \"hostname\": \"three.tmark\"} "
+ " ] } "
+ ", "
+ "{ \"name\": \"billcat.net\" , "
+ " \"key_name\": \"d2_key.billcat.net\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"four.billcat\" } , "
+ " { \"hostname\": \"five.billcat\" } , "
+ " { \"hostname\": \"six.billcat\" } "
+ " ] } "
+ "] },"
+ "\"reverse_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \" 0.168.192.in.addr.arpa.\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"one.rev\" } , "
+ " { \"hostname\": \"two.rev\" } , "
+ " { \"hostname\": \"three.rev\" } "
+ " ] } "
+ ", "
+ "{ \"name\": \" 0.247.106.in.addr.arpa.\" , "
+ " \"key_name\": \"d2_key.billcat.net\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"four.rev\" }, "
+ " { \"hostname\": \"five.rev\" } , "
+ " { \"hostname\": \"six.rev\" } "
+ " ] } "
+ "] } }";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that we can parse the configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the D2 context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
+
+ // Verify that the application level scalars have the proper values.
+ std::string interface;
+ EXPECT_NO_THROW (context->getParam("interface", interface));
+ EXPECT_EQ("eth1", interface);
+
+ std::string ip_address;
+ EXPECT_NO_THROW (context->getParam("ip_address", ip_address));
+ EXPECT_EQ("192.168.1.33", ip_address);
+
+ uint32_t port = 0;
+ EXPECT_NO_THROW (context->getParam("port", port));
+ EXPECT_EQ(88, port);
+
+ // Verify that the forward manager can be retrieved.
+ DdnsDomainListMgrPtr mgr = context->getForwardMgr();
+ ASSERT_TRUE(mgr);
+
+ // Verify that the forward manager has the correct number of domains.
+ DdnsDomainMapPtr domains = mgr->getDomains();
+ ASSERT_TRUE(domains);
+ int count = domains->size();
+ EXPECT_EQ(2, count);
+
+ // Verify that the server count in each of the forward manager domains.
+ // NOTE that since prior tests have validated server parsing, we are are
+ // assuming that the servers did in fact parse correctly if the correct
+ // number of them are there.
+ DdnsDomainMapPair domain_pair;
+ BOOST_FOREACH(domain_pair, (*domains)) {
+ DdnsDomainPtr domain = domain_pair.second;
+ DnsServerInfoStoragePtr servers = domain->getServers();
+ count = servers->size();
+ EXPECT_TRUE(servers);
+ EXPECT_EQ(3, count);
+ }
+
+ // Verify that the reverse manager can be retrieved.
+ mgr = context->getReverseMgr();
+ ASSERT_TRUE(mgr);
+
+ // Verify that the reverse manager has the correct number of domains.
+ domains = mgr->getDomains();
+ count = domains->size();
+ EXPECT_EQ(2, count);
+
+ // Verify that the server count in each of the reverse manager domains.
+ // NOTE that since prior tests have validated server parsing, we are are
+ // assuming that the servers did in fact parse correctly if the correct
+ // number of them are there.
+ BOOST_FOREACH(domain_pair, (*domains)) {
+ DdnsDomainPtr domain = domain_pair.second;
+ DnsServerInfoStoragePtr servers = domain->getServers();
+ count = servers->size();
+ EXPECT_TRUE(servers);
+ EXPECT_EQ(3, count);
+ }
+
+ // Verify that parsing the exact same configuration a second time
+ // does not cause a duplicate value errors.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+}
+
+/// @brief Tests the basics of the D2CfgMgr FQDN-domain matching
+/// This test uses a valid configuration to exercise the D2CfgMgr
+/// forward FQDN-to-domain matching.
+/// It verifies that:
+/// 1. Given an FQDN which exactly matches a domain's name, that domain is
+/// returned as match.
+/// 2. Given a FQDN for sub-domain in the list, returns the proper match.
+/// 3. Given a FQDN that matches no domain name, returns the wild card domain
+/// as a match.
+TEST_F(D2CfgMgrTest, forwardMatch) {
+ // Create configuration with one domain, one sub domain, and the wild
+ // card.
+ std::string config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": [] ,"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] } "
+ ", "
+ "{ \"name\": \"one.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.2\" } "
+ " ] } "
+ ", "
+ "{ \"name\": \"*\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"global.net\" } "
+ " ] } "
+ "] }, "
+ "\"reverse_ddns\" : {} "
+ "}";
+
+
+ ASSERT_TRUE(fromJSON(config));
+ // Verify that we can parse the configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the D2 context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
+
+ DdnsDomainPtr match;
+ // Verify that an exact match works.
+ EXPECT_TRUE(cfg_mgr_->matchForward("tmark.org", match));
+ EXPECT_EQ("tmark.org", match->getName());
+
+ // Verify that an exact match works.
+ EXPECT_TRUE(cfg_mgr_->matchForward("one.tmark.org", match));
+ EXPECT_EQ("one.tmark.org", match->getName());
+
+ // Verify that a FQDN for sub-domain matches.
+ EXPECT_TRUE(cfg_mgr_->matchForward("blue.tmark.org", match));
+ EXPECT_EQ("tmark.org", match->getName());
+
+ // Verify that a FQDN for sub-domain matches.
+ EXPECT_TRUE(cfg_mgr_->matchForward("red.one.tmark.org", match));
+ EXPECT_EQ("one.tmark.org", match->getName());
+
+ // Verify that an FQDN with no match, returns the wild card domain.
+ EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
+ EXPECT_EQ("*", match->getName());
+
+ // Verify that an attempt to match an empty FQDN throws.
+ ASSERT_THROW(cfg_mgr_->matchForward("", match), D2CfgError);
+}
+
+/// @brief Tests domain matching when there is no wild card domain.
+/// This test verifies that matches are found only for FQDNs that match
+/// some or all of a domain name. FQDNs without matches should not return
+/// a match.
+TEST_F(D2CfgMgrTest, matchNoWildcard) {
+ // Create a configuration with one domain, one sub-domain, and NO wild card.
+ std::string config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": [] ,"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] } "
+ ", "
+ "{ \"name\": \"one.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.2\" } "
+ " ] } "
+ "] }, "
+ "\"reverse_ddns\" : {} "
+ " }";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that we can parse the configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the D2 context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
+
+ DdnsDomainPtr match;
+ // Verify that full or partial matches, still match.
+ EXPECT_TRUE(cfg_mgr_->matchForward("tmark.org", match));
+ EXPECT_EQ("tmark.org", match->getName());
+
+ EXPECT_TRUE(cfg_mgr_->matchForward("blue.tmark.org", match));
+ EXPECT_EQ("tmark.org", match->getName());
+
+ EXPECT_TRUE(cfg_mgr_->matchForward("red.one.tmark.org", match));
+ EXPECT_EQ("one.tmark.org", match->getName());
+
+ // Verify that a FQDN with no match, fails to match.
+ EXPECT_FALSE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
+}
+
+/// @brief Tests domain matching when there is ONLY a wild card domain.
+/// This test verifies that any FQDN matches the wild card.
+TEST_F(D2CfgMgrTest, matchAll) {
+ std::string config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": [] ,"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"*\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] } "
+ "] }, "
+ "\"reverse_ddns\" : {} "
+ "}";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that we can parse the configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the D2 context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
+
+ // Verify that wild card domain is returned for any FQDN.
+ DdnsDomainPtr match;
+ EXPECT_TRUE(cfg_mgr_->matchForward("tmark.org", match));
+ EXPECT_EQ("*", match->getName());
+ EXPECT_TRUE(cfg_mgr_->matchForward("shouldbe.wildcard", match));
+ EXPECT_EQ("*", match->getName());
+
+ // Verify that an attempt to match an empty FQDN still throws.
+ ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError);
+
+}
+
+/// @brief Tests the basics of the D2CfgMgr reverse FQDN-domain matching
+/// This test uses a valid configuration to exercise the D2CfgMgr's
+/// reverse FQDN-to-domain matching.
+/// It verifies that:
+/// 1. Given an FQDN which exactly matches a domain's name, that domain is
+/// returned as match.
+/// 2. Given a FQDN for sub-domain in the list, returns the proper match.
+/// 3. Given a FQDN that matches no domain name, returns the wild card domain
+/// as a match.
+TEST_F(D2CfgMgrTest, matchReverse) {
+ std::string config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": [] ,"
+ "\"forward_ddns\" : {}, "
+ "\"reverse_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"5.100.168.192.in-addr.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] }, "
+ "{ \"name\": \"100.200.192.in-addr.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] }, "
+ "{ \"name\": \"170.192.in-addr.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] }, "
+ "{ \"name\": \"2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] },"
+ "{ \"name\": \"*\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] } "
+ "] } }";
+
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that we can parse the configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the D2 context can be retrieved and is not null.
+ D2CfgContextPtr context;
+ ASSERT_NO_THROW(context = cfg_mgr_->getD2CfgContext());
+
+ DdnsDomainPtr match;
+
+ // Verify an exact match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("192.168.100.5", match));
+ EXPECT_EQ("5.100.168.192.in-addr.arpa.", match->getName());
+
+ // Verify a sub-domain match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("192.200.100.27", match));
+ EXPECT_EQ("100.200.192.in-addr.arpa.", match->getName());
+
+ // Verify a sub-domain match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("192.170.50.30", match));
+ EXPECT_EQ("170.192.in-addr.arpa.", match->getName());
+
+ // Verify a wild card match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("1.1.1.1", match));
+ EXPECT_EQ("*", match->getName());
+
+ // Verify a IPv6 match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:302:99::",match));
+ EXPECT_EQ("2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.", match->getName());
+
+ // Verify a IPv6 wild card match.
+ EXPECT_TRUE(cfg_mgr_->matchReverse("2001:db8:99:302::",match));
+ EXPECT_EQ("*", match->getName());
+
+ // Verify that an attempt to match an invalid IP address throws.
+ ASSERT_THROW(cfg_mgr_->matchReverse("", match), D2CfgError);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/d2/tests/d2_controller_unittests.cc b/src/bin/d2/tests/d2_controller_unittests.cc
new file mode 100644
index 0000000..f4b4d41
--- /dev/null
+++ b/src/bin/d2/tests/d2_controller_unittests.cc
@@ -0,0 +1,225 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/ccsession.h>
+#include <d_test_stubs.h>
+#include <d2/d2_controller.h>
+#include <d2/spec_config.h>
+
+#include <boost/pointer_cast.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <gtest/gtest.h>
+
+#include <config.h>
+#include <sstream>
+
+using namespace boost::posix_time;
+
+namespace isc {
+namespace d2 {
+
+/// @brief Test fixture class for testing D2Controller class. This class
+/// derives from DControllerTest and wraps a D2Controller. Much of the
+/// underlying functionality is in the DControllerBase class which has an
+/// extensive set of unit tests that are independent of DHCP-DDNS.
+/// @TODO Currently These tests are relatively light and duplicate some of
+/// the testing done on the base class. These tests are sufficient to ensure
+/// that D2Controller properly derives from its base class and to test the
+/// logic that is unique to D2Controller. These tests will be augmented and
+/// or new tests added as additional functionality evolves.
+/// Unlike the stub testing, there is no use of SimFailure to induce error
+/// conditions as this is production code.
+class D2ControllerTest : public DControllerTest {
+public:
+ /// @brief Constructor
+ /// Note the constructor passes in the static D2Controller instance
+ /// method.
+ D2ControllerTest() : DControllerTest(D2Controller::instance) {
+ }
+
+ /// @brief Destructor
+ ~D2ControllerTest() {
+ }
+};
+
+/// @brief Basic Controller instantiation testing.
+/// Verifies that the controller singleton gets created and that the
+/// basic derivation from the base class is intact.
+TEST_F(D2ControllerTest, basicInstanceTesting) {
+ // Verify the we can the singleton instance can be fetched and that
+ // it is the correct type.
+ DControllerBasePtr& controller = DControllerTest::getController();
+ ASSERT_TRUE(controller);
+ ASSERT_NO_THROW(boost::dynamic_pointer_cast<D2Controller>(controller));
+
+ // Verify that controller's app name is correct.
+ EXPECT_TRUE(checkAppName(D2Controller::d2_app_name_));
+
+ // Verify that controller's bin name is correct.
+ EXPECT_TRUE(checkBinName(D2Controller::d2_bin_name_));
+
+ // Verify that controller's spec file name is correct.
+ EXPECT_TRUE(checkSpecFileName(D2_SPECFILE_LOCATION));
+
+ // Verify that controller's IOService exists.
+ EXPECT_TRUE(checkIOService());
+
+ // Verify that the Process does NOT exist.
+ EXPECT_FALSE(checkProcess());
+}
+
+/// @brief Tests basic command line processing.
+/// Verifies that:
+/// 1. Standard command line options are supported.
+/// 2. Invalid options are detected.
+TEST_F(D2ControllerTest, commandLineArgs) {
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+
+ // Verify that both flags are false initially.
+ EXPECT_TRUE(checkStandAlone(false));
+ EXPECT_TRUE(checkVerbose(false));
+
+ // Verify that standard options can be parsed without error.
+ EXPECT_NO_THROW(parseArgs(argc, argv));
+
+ // Verify that flags are now true.
+ EXPECT_TRUE(checkStandAlone(true));
+ EXPECT_TRUE(checkVerbose(true));
+
+ // Verify that an unknown option is detected.
+ char* argv2[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-x") };
+ argc = 2;
+ EXPECT_THROW(parseArgs(argc, argv2), InvalidUsage);
+}
+
+/// @brief Tests application process creation and initialization.
+/// Verifies that the process can be successfully created and initialized.
+TEST_F(D2ControllerTest, initProcessTesting) {
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+}
+
+/// @brief Tests launch and normal shutdown (stand alone mode).
+/// This creates an interval timer to generate a normal shutdown and then
+/// launches with a valid, stand-alone command line and no simulated errors.
+TEST_F(D2ControllerTest, launchNormalShutdown) {
+ // command line to run standalone
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+
+ // Use an asiolink IntervalTimer and callback to generate the
+ // shutdown invocation. (Note IntervalTimer setup is in milliseconds).
+ isc::asiolink::IntervalTimer timer(*getIOService());
+ timer.setup(genShutdownCallback, 2 * 1000);
+
+ // Record start time, and invoke launch().
+ ptime start = microsec_clock::universal_time();
+ EXPECT_NO_THROW(launch(argc, argv));
+
+ // Record stop time.
+ ptime stop = microsec_clock::universal_time();
+
+ // Verify that duration of the run invocation is the same as the
+ // timer duration. This demonstrates that the shutdown was driven
+ // by an io_service event and callback.
+ time_duration elapsed = stop - start;
+ EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
+ elapsed.total_milliseconds() <= 2100);
+}
+
+/// @brief Configuration update event testing.
+/// This really tests just the ability of the handlers to invoke the necessary
+/// chain of methods and handle error conditions. Configuration parsing and
+/// retrieval should be tested as part of the d2 configuration management
+/// implementation. Note that this testing calls the configuration update event
+/// callback, configHandler, directly.
+/// This test verifies that:
+/// 1. Configuration will be rejected in integrated mode when there is no
+/// session established. (This is a very contrived situation).
+/// 2. In stand-alone mode a configuration update results in successful
+/// status return.
+/// 3. That an application process error in configuration updating is handled
+/// properly.
+TEST_F(D2ControllerTest, configUpdateTests) {
+ int rcode = -1;
+ isc::data::ConstElementPtr answer;
+
+ // Initialize the application process.
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+
+ // Create a configuration set using a small, valid D2 configuration.
+ isc::data::ElementPtr config_set =
+ isc::data::Element::fromJSON(valid_d2_config);
+
+ // We are not stand-alone, so configuration should be rejected as there is
+ // no session. This is a pretty contrived situation that shouldn't be
+ // possible other than the handler being called directly (like this does).
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(1, rcode);
+
+ // Verify that in stand alone we get a successful update result.
+ setStandAlone(true);
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(0, rcode);
+
+ // Use an invalid configuration to verify parsing error return.
+ std::string config = "{ \"bogus\": 1000 } ";
+ config_set = isc::data::Element::fromJSON(config);
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(1, rcode);
+}
+
+/// @brief Command execution tests.
+/// This really tests just the ability of the handler to invoke the necessary
+/// chain of methods and to handle error conditions. Note that this testing
+/// calls the command callback, commandHandler, directly.
+/// This test verifies that:
+/// 1. That an unrecognized command is detected and returns a status of
+/// d2::COMMAND_INVALID.
+/// 2. Shutdown command is recognized and returns a d2::COMMAND_SUCCESS status.
+TEST_F(D2ControllerTest, executeCommandTests) {
+ int rcode = -1;
+ isc::data::ConstElementPtr answer;
+ isc::data::ElementPtr arg_set;
+
+ // Initialize the application process.
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+
+ // Verify that an unknown command returns an COMMAND_INVALID response.
+ std::string bogus_command("bogus");
+ answer = DControllerBase::commandHandler(bogus_command, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_INVALID, rcode);
+
+ // Verify that shutdown command returns COMMAND_SUCCESS response.
+ //answer = executeCommand(SHUT_DOWN_COMMAND, isc::data::ElementPtr());
+ answer = DControllerBase::commandHandler(SHUT_DOWN_COMMAND, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_SUCCESS, rcode);
+
+}
+
+}; // end of isc::d2 namespace
+}; // end of isc namespace
diff --git a/src/bin/d2/tests/d2_process_unittests.cc b/src/bin/d2/tests/d2_process_unittests.cc
new file mode 100644
index 0000000..cbbdd3f
--- /dev/null
+++ b/src/bin/d2/tests/d2_process_unittests.cc
@@ -0,0 +1,605 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+
+#include <config/ccsession.h>
+#include <d2/d2_process.h>
+#include <dhcp_ddns/ncr_io.h>
+#include <d_test_stubs.h>
+
+#include <boost/bind.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <gtest/gtest.h>
+
+#include <config.h>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::config;
+using namespace isc::d2;
+using namespace boost::posix_time;
+
+namespace {
+
+/// @brief Valid configuration containing an unavailable IP address.
+const char* bad_ip_d2_config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"1.1.1.1\" , "
+ "\"port\" : 5031, "
+ "\"tsig_keys\": ["
+ "{ \"name\": \"d2_key.tmark.org\" , "
+ " \"algorithm\": \"md5\" ,"
+ " \"secret\": \"0123456989\" "
+ "} ],"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"tmark.org\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"one.tmark\" } "
+ "] } ] }, "
+ "\"reverse_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \" 0.168.192.in.addr.arpa.\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.101\" , "
+ " \"port\": 100 } ] } "
+ "] } }";
+
+/// @brief D2Process test fixture class
+//class D2ProcessTest : public D2Process, public ::testing::Test {
+class D2ProcessTest : public D2Process, public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ D2ProcessTest() : D2Process("d2test",
+ IOServicePtr(new isc::asiolink::IOService())) {
+ }
+
+ /// @brief Destructor
+ virtual ~D2ProcessTest() {
+ }
+
+ /// @brief Callback that will invoke shutdown method.
+ void genShutdownCallback() {
+ shutdown(isc::data::ConstElementPtr());
+ }
+
+ /// @brief Callback that throws an exception.
+ void genFatalErrorCallback() {
+ isc_throw (DProcessBaseError, "simulated fatal error");
+ }
+
+ /// @brief Reconfigures and starts the queue manager given a configuration.
+ ///
+ /// This method emulates the reception of a new configuration and should
+ /// conclude with the Queue manager placed in the RUNNING state.
+ ///
+ /// @param config is the configuration to use
+ ///
+ /// @return Returns AssertionSuccess if the queue manager was successfully
+ /// reconfigured, AssertionFailure otherwise.
+ ::testing::AssertionResult runWithConfig(const char* config) {
+ int rcode = -1;
+ // Convert the string configuration into an Element set.
+ ::testing::AssertionResult res = fromJSON(config);
+ if (res != ::testing::AssertionSuccess()) {
+ return res;
+ }
+
+ isc::data::ConstElementPtr answer = configure(config_set_);
+ isc::data::ConstElementPtr comment;
+ comment = isc::config::parseAnswer(rcode, answer);
+
+ if (rcode) {
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "configure() failed:"
+ << comment));
+ }
+
+ // Must call checkQueueStatus, to cause queue manager to reconfigure
+ // and start.
+ checkQueueStatus();
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ // If queue manager isn't in the RUNNING state, return failure.
+ if (D2QueueMgr::RUNNING != queue_mgr->getMgrState()) {
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "queue manager did not start"));
+ }
+
+ // Good to go.
+ return (::testing::AssertionSuccess());
+ }
+
+ /// @brief Checks if shutdown criteria would be met given a shutdown type.
+ ///
+ /// This method sets the D2Process shutdown type to the given value, and
+ /// calls the canShutdown() method, returning its return value.
+ ///
+ /// @return Returns the boolean result canShutdown.
+ bool checkCanShutdown(ShutdownType shutdown_type) {
+ setShutdownType(shutdown_type);
+ return (canShutdown());
+ }
+};
+
+/// @brief Verifies D2Process construction behavior.
+/// 1. Verifies that constructor fails with an invalid IOService
+/// 2. Verifies that constructor succeeds with a valid IOService
+/// 3. Verifies that all managers are accessible
+TEST(D2Process, construction) {
+ // Verify that the constructor will fail if given an empty
+ // io service.
+ IOServicePtr lcl_io_service;
+ EXPECT_THROW (D2Process("TestProcess", lcl_io_service), DProcessBaseError);
+
+ // Verify that the constructor succeeds with a valid io_service
+ lcl_io_service.reset(new isc::asiolink::IOService());
+ ASSERT_NO_THROW (D2Process("TestProcess", lcl_io_service));
+
+ // Verify that the configuration, queue, and update managers
+ // are all accessible after construction.
+ D2Process d2process("TestProcess", lcl_io_service);
+
+ D2CfgMgrPtr cfg_mgr = d2process.getD2CfgMgr();
+ ASSERT_TRUE(cfg_mgr);
+
+ D2QueueMgrPtr queue_mgr = d2process.getD2QueueMgr();
+ ASSERT_TRUE(queue_mgr);
+
+ const D2UpdateMgrPtr& update_mgr = d2process.getD2UpdateMgr();
+ ASSERT_TRUE(update_mgr);
+}
+
+/// @brief Verifies basic configure method behavior.
+/// This test primarily verifies that upon receipt of a new configuration,
+/// D2Process will reconfigure the queue manager if the configuration is valid,
+/// or leave queue manager unaffected if not. Currently, the queue manager is
+/// only D2 component that must adapt to new configurations. Other components,
+/// such as Transactions will be unaffected as they are transient and use
+/// whatever configuration was in play at the time they were created.
+/// If other components need to provide "dynamic" configuration responses,
+/// those tests would need to be added.
+TEST_F(D2ProcessTest, configure) {
+ // Verify the queue manager is not yet initialized.
+ D2QueueMgrPtr queue_mgr = getD2QueueMgr();
+ ASSERT_TRUE(queue_mgr);
+ ASSERT_EQ(D2QueueMgr::NOT_INITTED, queue_mgr->getMgrState());
+
+ // Verify that reconfigure queue manager flag is false.
+ ASSERT_FALSE(getReconfQueueFlag());
+
+ // Create a valid configuration set from text config.
+ ASSERT_TRUE(fromJSON(valid_d2_config));
+
+ // Invoke configure() with a valid D2 configuration.
+ isc::data::ConstElementPtr answer = configure(config_set_);
+
+ // Verify that configure result is success and reconfigure queue manager
+ // flag is true.
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ ASSERT_TRUE(getReconfQueueFlag());
+
+ // Call checkQueueStatus, to cause queue manager to reconfigure and start.
+ checkQueueStatus();
+
+ // Verify that queue manager is now in the RUNNING state, and flag is false.
+ ASSERT_EQ(D2QueueMgr::RUNNING, queue_mgr->getMgrState());
+ ASSERT_FALSE(getReconfQueueFlag());
+
+ // Create an invalid configuration set from text config.
+ ASSERT_TRUE(fromJSON("{ \"bogus\": 1000 } "));
+
+ // Invoke configure() with the invalid configuration.
+ answer = configure(config_set_);
+
+ // Verify that configure result is failure, the reconfigure flag is
+ // false, and that the queue manager is still running.
+ ASSERT_TRUE(checkAnswer(answer, 1));
+ EXPECT_FALSE(getReconfQueueFlag());
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr->getMgrState());
+}
+
+/// @brief Tests checkQueueStatus() logic for stopping the queue on shutdown
+/// This test manually sets shutdown flag and verifies that queue manager
+/// stop is initiated.
+TEST_F(D2ProcessTest, queueStopOnShutdown) {
+ ASSERT_TRUE(runWithConfig(valid_d2_config));
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ setShutdownFlag(true);
+
+ // Calling checkQueueStatus restart queue manager
+ checkQueueStatus();
+
+ // Verify that the queue manager is stopping.
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Verify that a subsequent call with no events occurring in between,
+ // results in no change to queue manager
+ checkQueueStatus();
+
+ // Verify that the queue manager is still stopping.
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Call runIO so the IO cancel event occurs and verify that queue manager
+ // has stopped.
+ runIO();
+ ASSERT_EQ(D2QueueMgr::STOPPED, queue_mgr->getMgrState());
+}
+
+/// @brief Tests checkQueueStatus() logic for stopping the queue on reconfigure.
+/// This test manually sets queue reconfiguration flag and verifies that queue
+/// manager stop is initiated.
+TEST_F(D2ProcessTest, queueStopOnReconf) {
+ ASSERT_TRUE(runWithConfig(valid_d2_config));
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ // Manually set the reconfigure indicator.
+ setReconfQueueFlag(true);
+
+ // Calling checkQueueStatus should initiate stopping the queue manager.
+ checkQueueStatus();
+
+ // Verify that the queue manager is stopping.
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Call runIO so the IO cancel event occurs and verify that queue manager
+ // has stopped.
+ runIO();
+ ASSERT_EQ(D2QueueMgr::STOPPED, queue_mgr->getMgrState());
+}
+
+
+/// @brief Tests checkQueueStatus() logic for recovering from queue full
+/// This test manually creates a receive queue full condition and then
+/// "drains" the queue until the queue manager resumes listening. This
+/// verifies D2Process's ability to recover from a queue full condition.
+TEST_F(D2ProcessTest, queueFullRecovery) {
+ // Valid test message, contents are unimportant.
+ const char* test_msg =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ // Start queue manager with known good config.
+ ASSERT_TRUE(runWithConfig(valid_d2_config));
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ // Set the maximum queue size to manageable number.
+ size_t max_queue_size = 5;
+ queue_mgr->setMaxQueueSize(max_queue_size);
+
+ // Manually enqueue max requests.
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(test_msg));
+ for (int i = 0; i < max_queue_size; i++) {
+ // Verify that the request can be added to the queue and queue
+ // size increments accordingly.
+ ASSERT_NO_THROW(queue_mgr->enqueue(ncr));
+ ASSERT_EQ(i+1, queue_mgr->getQueueSize());
+ }
+
+ // Since we are not really receiving, we will simulate QUEUE FULL
+ // detection.
+ queue_mgr->stopListening(D2QueueMgr::STOPPED_QUEUE_FULL);
+ ASSERT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Call runIO so the IO cancel event occurs and verify that queue manager
+ // has stopped.
+ runIO();
+ ASSERT_EQ(D2QueueMgr::STOPPED_QUEUE_FULL, queue_mgr->getMgrState());
+
+ // Dequeue requests one at a time, calling checkQueueStatus after each
+ // dequeue, until we reach the resume threshold. This simulates update
+ // manager consuming jobs. Queue manager should remain stopped during
+ // this loop.
+ int resume_threshold = (max_queue_size * QUEUE_RESTART_PERCENT);
+ while (queue_mgr->getQueueSize() > resume_threshold) {
+ checkQueueStatus();
+ ASSERT_EQ(D2QueueMgr::STOPPED_QUEUE_FULL, queue_mgr->getMgrState());
+ ASSERT_NO_THROW(queue_mgr->dequeue());
+ }
+
+ // Dequeue one more, which brings us under the threshold and call
+ // checkQueueStatus.
+ // Verify that the queue manager is again running.
+ ASSERT_NO_THROW(queue_mgr->dequeue());
+ checkQueueStatus();
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr->getMgrState());
+}
+
+/// @brief Tests checkQueueStatus() logic for queue receive error recovery
+/// This test manually creates a queue receive error condition and tests
+/// verifies that checkQueueStatus reacts properly to recover.
+TEST_F(D2ProcessTest, queueErrorRecovery) {
+ ASSERT_TRUE(runWithConfig(valid_d2_config));
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ // Since we are not really receiving, we have to stage an error.
+ queue_mgr->stopListening(D2QueueMgr::STOPPED_RECV_ERROR);
+ ASSERT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Call runIO so the IO cancel event occurs and verify that queue manager
+ // has stopped.
+ runIO();
+ ASSERT_EQ(D2QueueMgr::STOPPED_RECV_ERROR, queue_mgr->getMgrState());
+
+ // Calling checkQueueStatus should restart queue manager
+ checkQueueStatus();
+
+ // Verify that queue manager is again running.
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr->getMgrState());
+}
+
+/// @brief Verifies queue manager recovery from unusable configuration
+/// This test checks D2Process's gracefully handle a configuration which
+/// while valid is not operationally usable (i.e. IP address is unavailable),
+/// and to subsequently recover given a usable configuration.
+TEST_F(D2ProcessTest, badConfigureRecovery) {
+ D2QueueMgrPtr queue_mgr = getD2QueueMgr();
+ ASSERT_TRUE(queue_mgr);
+
+ // Verify the queue manager is not initialized.
+ EXPECT_EQ(D2QueueMgr::NOT_INITTED, queue_mgr->getMgrState());
+
+ // Invoke configure() with a valid config that contains an unusable IP
+ ASSERT_TRUE(fromJSON(bad_ip_d2_config));
+ isc::data::ConstElementPtr answer = configure(config_set_);
+
+ // Verify that configure result is success and reconfigure queue manager
+ // flag is true.
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ ASSERT_TRUE(getReconfQueueFlag());
+
+ // Call checkQueueStatus to cause queue manager to attempt to reconfigure.
+ checkQueueStatus();
+
+ // Verify that queue manager failed to start, (i.e. is in INITTED state),
+ // and the the reconfigure flag is false.
+ ASSERT_EQ(D2QueueMgr::INITTED, queue_mgr->getMgrState());
+ ASSERT_FALSE(getReconfQueueFlag());
+
+ // Verify we can recover given a valid config with an usable IP address.
+ ASSERT_TRUE(fromJSON(valid_d2_config));
+ answer = configure(config_set_);
+
+ // Verify that configure result is success and reconfigure queue manager
+ // flag is true.
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ ASSERT_TRUE(getReconfQueueFlag());
+
+ // Call checkQueueStatus to cause queue manager to reconfigure and start.
+ checkQueueStatus();
+
+ // Verify that queue manager is now in the RUNNING state, and reconfigure
+ // flag is false.
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr->getMgrState());
+ EXPECT_FALSE(getReconfQueueFlag());
+}
+
+/// @brief Verifies basic command method behavior.
+/// @TODO IF the D2Process is extended to support extra commands this testing
+/// will need to augmented accordingly.
+TEST_F(D2ProcessTest, command) {
+ // Verify that the process will process unsupported command and
+ // return a failure response.
+ int rcode = -1;
+ string args = "{ \"arg1\": 77 } ";
+ isc::data::ElementPtr json = isc::data::Element::fromJSON(args);
+ isc::data::ConstElementPtr answer = command("bogus_command", json);
+ parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_INVALID, rcode);
+}
+
+/// @brief Tests shutdown command argument parsing
+/// The shutdown command supports an optional "type" argument. This test
+/// checks that for valid values, the shutdown() method: sets the shutdown
+/// type to correct value, set the shutdown flag to true, and returns a
+/// success response; and for invalid values: sets the shutdown flag to false
+/// and returns a failure response.
+TEST_F(D2ProcessTest, shutdownArgs) {
+ isc::data::ElementPtr args;
+ isc::data::ConstElementPtr answer;
+ const char* default_args = "{}";
+ const char* normal_args = "{ \"type\" : \"normal\" }";
+ const char* drain_args = "{ \"type\" : \"drain_first\" }";
+ const char* now_args = "{ \"type\" : \"now\" }";
+ const char* bogus_args = "{ \"type\" : \"bogus\" }";
+
+ // Verify defaulting to SD_NORMAL if no argument is given.
+ ASSERT_NO_THROW(args = isc::data::Element::fromJSON(default_args));
+ EXPECT_NO_THROW(answer = shutdown(args));
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ EXPECT_EQ(SD_NORMAL, getShutdownType());
+ EXPECT_TRUE(shouldShutdown());
+
+ // Verify argument value "normal".
+ ASSERT_NO_THROW(args = isc::data::Element::fromJSON(normal_args));
+ EXPECT_NO_THROW(answer = shutdown(args));
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ EXPECT_EQ(SD_NORMAL, getShutdownType());
+ EXPECT_TRUE(shouldShutdown());
+
+ // Verify argument value "drain_first".
+ ASSERT_NO_THROW(args = isc::data::Element::fromJSON(drain_args));
+ EXPECT_NO_THROW(answer = shutdown(args));
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ EXPECT_EQ(SD_DRAIN_FIRST, getShutdownType());
+ EXPECT_TRUE(shouldShutdown());
+
+ // Verify argument value "now".
+ ASSERT_NO_THROW(args = isc::data::Element::fromJSON(now_args));
+ EXPECT_NO_THROW(answer = shutdown(args));
+ ASSERT_TRUE(checkAnswer(answer, 0));
+ EXPECT_EQ(SD_NOW, getShutdownType());
+ EXPECT_TRUE(shouldShutdown());
+
+ // Verify correct handling of an invalid value.
+ ASSERT_NO_THROW(args = isc::data::Element::fromJSON(bogus_args));
+ EXPECT_NO_THROW(answer = shutdown(args));
+ ASSERT_TRUE(checkAnswer(answer, 1));
+ EXPECT_FALSE(shouldShutdown());
+}
+
+/// @brief Tests shutdown criteria logic
+/// D2Process using the method canShutdown() to determine if a shutdown
+/// can be performed given the value of the shutdown flag and the type of
+/// shutdown requested. For each shutdown type certain criteria must be met
+/// before the shutdown is permitted. This method is invoked once each pass
+/// through the main event loop. This test checks the operation of the
+/// canShutdown method. It uses a convenience method, checkCanShutdown(),
+/// which sets the shutdown type to the given value and invokes canShutdown(),
+/// returning its result.
+TEST_F(D2ProcessTest, canShutdown) {
+ ASSERT_TRUE(runWithConfig(valid_d2_config));
+ const D2QueueMgrPtr& queue_mgr = getD2QueueMgr();
+
+ // Shutdown flag is false. Method should return false for all types.
+ EXPECT_FALSE(checkCanShutdown(SD_NORMAL));
+ EXPECT_FALSE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_FALSE(checkCanShutdown(SD_NOW));
+
+ // Set shutdown flag to true.
+ setShutdownFlag(true);
+
+ // Queue Manager is running, queue is empty, no transactions.
+ // Only SD_NOW should return true.
+ EXPECT_FALSE(checkCanShutdown(SD_NORMAL));
+ EXPECT_FALSE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_TRUE(checkCanShutdown(SD_NOW));
+
+ // Tell queue manager to stop.
+ queue_mgr->stopListening();
+ // Verify that the queue manager is stopping.
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr->getMgrState());
+
+ // Queue Manager is stopping, queue is empty, no transactions.
+ // Only SD_NOW should return true.
+ EXPECT_FALSE(checkCanShutdown(SD_NORMAL));
+ EXPECT_FALSE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_TRUE(checkCanShutdown(SD_NOW));
+
+ // Allow cancel event to process.
+ ASSERT_NO_THROW(runIO());
+ // Verify that queue manager is stopped.
+ EXPECT_EQ(D2QueueMgr::STOPPED, queue_mgr->getMgrState());
+
+ // Queue Manager is stopped, queue is empty, no transactions.
+ // All types should return true.
+ EXPECT_TRUE(checkCanShutdown(SD_NORMAL));
+ EXPECT_TRUE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_TRUE(checkCanShutdown(SD_NOW));
+
+ const char* test_msg =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"fish.tmark.org\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ // Manually enqueue a request. This lets us test logic with queue
+ // not empty.
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(test_msg));
+ ASSERT_NO_THROW(queue_mgr->enqueue(ncr));
+ ASSERT_EQ(1, queue_mgr->getQueueSize());
+
+ // Queue Manager is stopped. Queue is not empty, no transactions.
+ // SD_DRAIN_FIRST should be false, SD_NORMAL and SD_NOW should be true.
+ EXPECT_TRUE(checkCanShutdown(SD_NORMAL));
+ EXPECT_FALSE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_TRUE(checkCanShutdown(SD_NOW));
+
+ // Now use update manager to dequeue the request and make a transaction.
+ // This lets us verify transaction list not empty logic.
+ const D2UpdateMgrPtr& update_mgr = getD2UpdateMgr();
+ ASSERT_TRUE(update_mgr);
+ ASSERT_NO_THROW(update_mgr->sweep());
+ ASSERT_EQ(0, queue_mgr->getQueueSize());
+ ASSERT_EQ(1, update_mgr->getTransactionCount());
+
+ // Queue Manager is stopped. Queue is empty, one transaction.
+ // Only SD_NOW should be true.
+ EXPECT_FALSE(checkCanShutdown(SD_NORMAL));
+ EXPECT_FALSE(checkCanShutdown(SD_DRAIN_FIRST));
+ EXPECT_TRUE(checkCanShutdown(SD_NOW));
+}
+
+
+/// @brief Verifies that an "external" call to shutdown causes the run method
+/// to exit gracefully.
+TEST_F(D2ProcessTest, normalShutdown) {
+ // Use an asiolink IntervalTimer and callback to generate the
+ // shutdown invocation. (Note IntervalTimer setup is in milliseconds).
+ isc::asiolink::IntervalTimer timer(*getIoService());
+ timer.setup(boost::bind(&D2ProcessTest::genShutdownCallback, this),
+ 2 * 1000);
+
+ // Record start time, and invoke run().
+ ptime start = microsec_clock::universal_time();
+ EXPECT_NO_THROW(run());
+
+ // Record stop time.
+ ptime stop = microsec_clock::universal_time();
+
+ // Verify that duration of the run invocation is the same as the
+ // timer duration. This demonstrates that the shutdown was driven
+ // by an io_service event and callback.
+ time_duration elapsed = stop - start;
+ EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
+ elapsed.total_milliseconds() <= 2100);
+}
+
+
+/// @brief Verifies that an "uncaught" exception thrown during event loop
+/// execution is treated as a fatal error.
+TEST_F(D2ProcessTest, fatalErrorShutdown) {
+ // Use an asiolink IntervalTimer and callback to generate the
+ // the exception. (Note IntervalTimer setup is in milliseconds).
+ isc::asiolink::IntervalTimer timer(*getIoService());
+ timer.setup(boost::bind(&D2ProcessTest::genFatalErrorCallback, this),
+ 2 * 1000);
+
+ // Record start time, and invoke run().
+ ptime start = microsec_clock::universal_time();
+ EXPECT_THROW(run(), DProcessBaseError);
+
+ // Record stop time.
+ ptime stop = microsec_clock::universal_time();
+
+ // Verify that duration of the run invocation is the same as the
+ // timer duration. This demonstrates that the anomaly occurred
+ // during io callback processing.
+ time_duration elapsed = stop - start;
+ EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
+ elapsed.total_milliseconds() <= 2100);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/d2/tests/d2_queue_mgr_unittests.cc b/src/bin/d2/tests/d2_queue_mgr_unittests.cc
new file mode 100644
index 0000000..a49ab65
--- /dev/null
+++ b/src/bin/d2/tests/d2_queue_mgr_unittests.cc
@@ -0,0 +1,445 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_asio.h>
+#include <d2/d2_queue_mgr.h>
+#include <dhcp_ddns/ncr_udp.h>
+#include <util/time_utilities.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <vector>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp_ddns;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Defines a list of valid JSON NameChangeRequest test messages.
+const char *valid_msgs[] =
+{
+ // Valid Add.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Remove.
+ "{"
+ " \"change_type\" : 1 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Add with IPv6 address
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"fe80::2acf:e9ff:fe12:e56f\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}"
+};
+
+static const int VALID_MSG_CNT = sizeof(valid_msgs)/sizeof(char*);
+
+const char* TEST_ADDRESS = "127.0.0.1";
+const uint32_t LISTENER_PORT = 5301;
+const uint32_t SENDER_PORT = LISTENER_PORT+1;
+const long TEST_TIMEOUT = 5 * 1000;
+
+/// @brief Tests that construction with max queue size of zero is not allowed.
+TEST(D2QueueMgrBasicTest, construction1) {
+ IOServicePtr io_service;
+
+ // Verify that constructing with null IOServicePtr is not allowed.
+ EXPECT_THROW((D2QueueMgr(io_service)), D2QueueMgrError);
+
+ io_service.reset(new isc::asiolink::IOService());
+ // Verify that constructing with max queue size of zero is not allowed.
+ EXPECT_THROW(D2QueueMgr(io_service, 0), D2QueueMgrError);
+}
+
+/// @brief Tests default construction works.
+TEST(D2QueueMgrBasicTest, construction2) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ // Verify that valid constructor works.
+ D2QueueMgrPtr queue_mgr;
+ ASSERT_NO_THROW(queue_mgr.reset(new D2QueueMgr(io_service)));
+ // Verify queue max is defaulted correctly.
+ EXPECT_EQ(D2QueueMgr::MAX_QUEUE_DEFAULT, queue_mgr->getMaxQueueSize());
+}
+
+/// @brief Tests construction with custom queue size works properly
+TEST(D2QueueMgrBasicTest, construction3) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ // Verify that custom queue size constructor works.
+ D2QueueMgrPtr queue_mgr;
+ ASSERT_NO_THROW(queue_mgr.reset(new D2QueueMgr(io_service, 100)));
+ // Verify queue max is the custom value.
+ EXPECT_EQ(100, queue_mgr->getMaxQueueSize());
+}
+
+/// @brief Tests QueueMgr's basic queue functions
+/// This test verifies that:
+/// 1. Following construction queue is empty
+/// 2. Attempting to peek at an empty queue is not allowed
+/// 3. Attempting to dequeue an empty queue is not allowed
+/// 4. Peek returns the first entry on the queue without altering queue content
+/// 5. Dequeue removes the first entry on the queue
+TEST(D2QueueMgrBasicTest, basicQueue) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ // Construct the manager with max queue size set to number of messages
+ // we'll use.
+ D2QueueMgrPtr queue_mgr;
+ ASSERT_NO_THROW(queue_mgr.reset(new D2QueueMgr(io_service, VALID_MSG_CNT)));
+ ASSERT_EQ(VALID_MSG_CNT, queue_mgr->getMaxQueueSize());
+
+ // Verify queue is empty after construction.
+ EXPECT_EQ(0, queue_mgr->getQueueSize());
+
+ // Verify that peek and dequeue both throw when queue is empty.
+ EXPECT_THROW(queue_mgr->peek(), D2QueueMgrQueueEmpty);
+ EXPECT_THROW(queue_mgr->dequeue(), D2QueueMgrQueueEmpty);
+
+ // Vector to keep track of the NCRs we que.
+ std::vector<NameChangeRequestPtr>ref_msgs;
+ NameChangeRequestPtr ncr;
+
+ // Iterate over the list of requests and add each to the queue.
+ for (int i = 0; i < VALID_MSG_CNT; i++) {
+ // Create the ncr and add to our reference list.
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ ref_msgs.push_back(ncr);
+
+ // Verify that the request can be added to the queue and queue
+ // size increments accordingly.
+ EXPECT_NO_THROW(queue_mgr->enqueue(ncr));
+ EXPECT_EQ(i+1, queue_mgr->getQueueSize());
+ }
+
+ // Loop through and verify that the queue contents match the
+ // reference list.
+ for (int i = 0; i < VALID_MSG_CNT; i++) {
+ // Verify that peek on a non-empty queue returns first entry
+ // without altering queue content.
+ EXPECT_NO_THROW(ncr = queue_mgr->peek());
+
+ // Verify the peeked entry is the one it should be.
+ ASSERT_TRUE(ncr);
+ EXPECT_TRUE (*(ref_msgs[i]) == *ncr);
+
+ // Verify that peek did not alter the queue size.
+ EXPECT_EQ(VALID_MSG_CNT - i, queue_mgr->getQueueSize());
+
+ // Verify the dequeueing from non-empty queue works
+ EXPECT_NO_THROW(queue_mgr->dequeue());
+
+ // Verify queue size decrements following dequeue.
+ EXPECT_EQ(VALID_MSG_CNT - (i + 1), queue_mgr->getQueueSize());
+ }
+
+ // Iterate over the list of requests and add each to the queue.
+ for (int i = 0; i < VALID_MSG_CNT; i++) {
+ // Create the ncr and add to our reference list.
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ ref_msgs.push_back(ncr);
+ EXPECT_NO_THROW(queue_mgr->enqueue(ncr));
+ }
+
+ // Verify queue count is correct.
+ EXPECT_EQ(VALID_MSG_CNT, queue_mgr->getQueueSize());
+
+ // Verfiy that peekAt returns the correct entry.
+ EXPECT_NO_THROW(ncr = queue_mgr->peekAt(1));
+ EXPECT_TRUE (*(ref_msgs[1]) == *ncr);
+
+ // Verfiy that dequeueAt removes the correct entry.
+ // Removing it, this should shift the queued entries forward by one.
+ EXPECT_NO_THROW(queue_mgr->dequeueAt(1));
+ EXPECT_NO_THROW(ncr = queue_mgr->peekAt(1));
+ EXPECT_TRUE (*(ref_msgs[2]) == *ncr);
+
+ // Verify the peekAt and dequeueAt throw when given indexes beyond the end.
+ EXPECT_THROW(queue_mgr->peekAt(VALID_MSG_CNT + 1), D2QueueMgrInvalidIndex);
+ EXPECT_THROW(queue_mgr->dequeueAt(VALID_MSG_CNT + 1),
+ D2QueueMgrInvalidIndex);
+}
+
+/// @brief Compares two NameChangeRequests for equality.
+bool checkSendVsReceived(NameChangeRequestPtr sent_ncr,
+ NameChangeRequestPtr received_ncr) {
+ return ((sent_ncr && received_ncr) &&
+ (*sent_ncr == *received_ncr));
+}
+
+/// @brief Text fixture that allows testing a listener and sender together
+/// It derives from both the receive and send handler classes and contains
+/// and instance of UDP listener and UDP sender.
+class QueueMgrUDPTest : public virtual ::testing::Test,
+ NameChangeSender::RequestSendHandler {
+public:
+ IOServicePtr io_service_;
+ NameChangeSenderPtr sender_;
+ isc::asiolink::IntervalTimer test_timer_;
+ D2QueueMgrPtr queue_mgr_;
+
+ NameChangeSender::Result send_result_;
+ std::vector<NameChangeRequestPtr> sent_ncrs_;
+ std::vector<NameChangeRequestPtr> received_ncrs_;
+
+ QueueMgrUDPTest() : io_service_(new isc::asiolink::IOService()),
+ test_timer_(*io_service_) {
+ isc::asiolink::IOAddress addr(TEST_ADDRESS);
+ // Create our sender instance. Note that reuse_address is true.
+ sender_.reset(new NameChangeUDPSender(addr, SENDER_PORT,
+ addr, LISTENER_PORT,
+ FMT_JSON, *this, 100, true));
+
+ // Set the test timeout to break any running tasks if they hang.
+ test_timer_.setup(boost::bind(&QueueMgrUDPTest::testTimeoutHandler,
+ this),
+ TEST_TIMEOUT);
+ }
+
+ void reset_results() {
+ sent_ncrs_.clear();
+ received_ncrs_.clear();
+ }
+
+ /// @brief Implements the send completion handler.
+ virtual void operator ()(const NameChangeSender::Result result,
+ NameChangeRequestPtr& ncr) {
+ // save the result and the NCR sent.
+ send_result_ = result;
+ sent_ncrs_.push_back(ncr);
+ }
+
+ /// @brief Handler invoked when test timeout is hit.
+ ///
+ /// This callback stops all running (hanging) tasks on IO service.
+ void testTimeoutHandler() {
+ io_service_->stop();
+ FAIL() << "Test timeout hit.";
+ }
+};
+
+/// @brief Tests D2QueueMgr's state model.
+/// This test verifies that:
+/// 1. Upon construction, initial state is NOT_INITTED.
+/// 2. Cannot start listening from while state is NOT_INITTED.
+/// 3. Successful listener initialization transitions from NOT_INITTED
+/// to INITTED.
+/// 4. Attempting to initialize the listener from INITTED state is not
+/// allowed.
+/// 5. Starting listener from INITTED transitions to RUNNING.
+/// 6. Stopping the listener transitions from RUNNING to STOPPED.
+/// 7. Starting listener from STOPPED transitions to RUNNING.
+TEST_F (QueueMgrUDPTest, stateModel) {
+ // Create the queue manager.
+ ASSERT_NO_THROW(queue_mgr_.reset(new D2QueueMgr(io_service_,
+ VALID_MSG_CNT)));
+
+ // Verify that the initial state is NOT_INITTED.
+ EXPECT_EQ(D2QueueMgr::NOT_INITTED, queue_mgr_->getMgrState());
+
+ // Verify that trying to listen before when not initialized fails.
+ EXPECT_THROW(queue_mgr_->startListening(), D2QueueMgrError);
+
+ // Verify that initializing the listener moves us to INITTED state.
+ isc::asiolink::IOAddress addr(TEST_ADDRESS);
+ EXPECT_NO_THROW(queue_mgr_->initUDPListener(addr, LISTENER_PORT,
+ FMT_JSON, true));
+ EXPECT_EQ(D2QueueMgr::INITTED, queue_mgr_->getMgrState());
+
+ // Verify that attempting to initialize the listener, from INITTED
+ // is not allowed.
+ EXPECT_THROW(queue_mgr_->initUDPListener(addr, LISTENER_PORT,
+ FMT_JSON, true),
+ D2QueueMgrError);
+
+ // Verify that we can enter the RUNNING from INITTED by starting the
+ // listener.
+ EXPECT_NO_THROW(queue_mgr_->startListening());
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr_->getMgrState());
+
+ // Verify that we can move from RUNNING to STOPPING by stopping the
+ // listener.
+ EXPECT_NO_THROW(queue_mgr_->stopListening());
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr_->getMgrState());
+
+ // Stopping requires IO cancel, which result in a callback.
+ // So process one event and verify we are STOPPED.
+ io_service_->run_one();
+ EXPECT_EQ(D2QueueMgr::STOPPED, queue_mgr_->getMgrState());
+
+ // Verify that we can re-enter the RUNNING from STOPPED by starting the
+ // listener.
+ EXPECT_NO_THROW(queue_mgr_->startListening());
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr_->getMgrState());
+
+ // Verify that we cannot remove the listener in the RUNNING state
+ EXPECT_THROW(queue_mgr_->removeListener(), D2QueueMgrError);
+
+ // Stop the listener.
+ EXPECT_NO_THROW(queue_mgr_->stopListening());
+ EXPECT_EQ(D2QueueMgr::STOPPING, queue_mgr_->getMgrState());
+
+ // Stopping requires IO cancel, which result in a callback.
+ // So process one event and verify we are STOPPED.
+ io_service_->run_one();
+ EXPECT_EQ(D2QueueMgr::STOPPED, queue_mgr_->getMgrState());
+
+ // Verify that we can remove the listener in the STOPPED state and
+ // end up back in NOT_INITTED.
+ EXPECT_NO_THROW(queue_mgr_->removeListener());
+ EXPECT_EQ(D2QueueMgr::NOT_INITTED, queue_mgr_->getMgrState());
+}
+
+/// @brief Tests D2QueueMgr's ability to manage received requests
+/// This test verifies that:
+/// 1. Requests can be received, queued, and dequeued
+/// 2. Once the queue is full, a subsequent request transitions
+/// manager to STOPPED_QUEUE_FULL state.
+/// 3. Starting listener returns manager to the RUNNING state.
+/// 4. Queue contents are preserved across state transitions.
+/// 5. Clearing the queue via the clearQueue() method works.
+/// 6. Requests can be received and queued normally after the queue
+/// has been emptied.
+/// 7. setQueueMax disallows values of 0 or less than current queue size.
+TEST_F (QueueMgrUDPTest, liveFeed) {
+ NameChangeRequestPtr send_ncr;
+ NameChangeRequestPtr received_ncr;
+
+ // Create the queue manager and start listening..
+ ASSERT_NO_THROW(queue_mgr_.reset(new D2QueueMgr(io_service_,
+ VALID_MSG_CNT)));
+ ASSERT_EQ(D2QueueMgr::NOT_INITTED, queue_mgr_->getMgrState());
+
+ // Verify that setting max queue size to 0 is not allowed.
+ EXPECT_THROW(queue_mgr_->setMaxQueueSize(0), D2QueueMgrError);
+ EXPECT_EQ(VALID_MSG_CNT, queue_mgr_->getMaxQueueSize());
+
+ isc::asiolink::IOAddress addr(TEST_ADDRESS);
+ ASSERT_NO_THROW(queue_mgr_->initUDPListener(addr, LISTENER_PORT,
+ FMT_JSON, true));
+ ASSERT_EQ(D2QueueMgr::INITTED, queue_mgr_->getMgrState());
+
+ ASSERT_NO_THROW(queue_mgr_->startListening());
+ ASSERT_EQ(D2QueueMgr::RUNNING, queue_mgr_->getMgrState());
+
+ // Place the sender into sending state.
+ ASSERT_NO_THROW(sender_->startSending(*io_service_));
+ ASSERT_TRUE(sender_->amSending());
+
+ // Iterate over the list of requests sending and receiving
+ // each one. Verify and dequeue as they arrive.
+ for (int i = 0; i < VALID_MSG_CNT; i++) {
+ // Create the ncr and add to our reference list.
+ ASSERT_NO_THROW(send_ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
+
+ // running two should do the send then the receive
+ io_service_->run_one();
+ io_service_->run_one();
+
+ // Verify that the request can be added to the queue and queue
+ // size increments accordingly.
+ EXPECT_EQ(1, queue_mgr_->getQueueSize());
+
+ // Verify that peek shows the NCR we just sent
+ EXPECT_NO_THROW(received_ncr = queue_mgr_->peek());
+ EXPECT_TRUE(checkSendVsReceived(send_ncr, received_ncr));
+
+ // Verify that we and dequeue the request.
+ EXPECT_NO_THROW(queue_mgr_->dequeue());
+ EXPECT_EQ(0, queue_mgr_->getQueueSize());
+ }
+
+ // Iterate over the list of requests, sending and receiving
+ // each one. Allow them to accumulate in the queue.
+ for (int i = 0; i < VALID_MSG_CNT; i++) {
+ // Create the ncr and add to our reference list.
+ ASSERT_NO_THROW(send_ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
+
+ // running two should do the send then the receive
+ EXPECT_NO_THROW(io_service_->run_one());
+ EXPECT_NO_THROW(io_service_->run_one());
+ EXPECT_EQ(i+1, queue_mgr_->getQueueSize());
+ }
+
+ // Verify that the queue is at max capacity.
+ EXPECT_EQ(queue_mgr_->getMaxQueueSize(), queue_mgr_->getQueueSize());
+
+ // Send another. The send should succeed.
+ ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
+ EXPECT_NO_THROW(io_service_->run_one());
+
+ // Now execute the receive which should not throw but should move us
+ // to STOPPED_QUEUE_FULL state.
+ EXPECT_NO_THROW(io_service_->run_one());
+ EXPECT_EQ(D2QueueMgr::STOPPED_QUEUE_FULL, queue_mgr_->getMgrState());
+
+ // Verify queue size did not increase beyond max.
+ EXPECT_EQ(VALID_MSG_CNT, queue_mgr_->getQueueSize());
+
+ // Verify that setting max queue size to a value less than current size of
+ // the queue is not allowed.
+ EXPECT_THROW(queue_mgr_->setMaxQueueSize(VALID_MSG_CNT-1), D2QueueMgrError);
+ EXPECT_EQ(VALID_MSG_CNT, queue_mgr_->getQueueSize());
+
+ // Verify that we can re-enter RUNNING from STOPPED_QUEUE_FULL.
+ EXPECT_NO_THROW(queue_mgr_->startListening());
+ EXPECT_EQ(D2QueueMgr::RUNNING, queue_mgr_->getMgrState());
+
+ // Verify that the queue contents were preserved.
+ EXPECT_EQ(queue_mgr_->getMaxQueueSize(), queue_mgr_->getQueueSize());
+
+ // Verify that clearQueue works.
+ EXPECT_NO_THROW(queue_mgr_->clearQueue());
+ EXPECT_EQ(0, queue_mgr_->getQueueSize());
+
+
+ // Verify that we can again receive requests.
+ // Send should be fine.
+ ASSERT_NO_THROW(sender_->sendRequest(send_ncr));
+ EXPECT_NO_THROW(io_service_->run_one());
+
+ // Receive should succeed.
+ EXPECT_NO_THROW(io_service_->run_one());
+ EXPECT_EQ(1, queue_mgr_->getQueueSize());
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/d2/tests/d2_test.py b/src/bin/d2/tests/d2_test.py
new file mode 100644
index 0000000..7548672
--- /dev/null
+++ b/src/bin/d2/tests/d2_test.py
@@ -0,0 +1,168 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from init import ProcessInfo, parse_args, dump_pid, unlink_pid_file, _BASETIME
+
+import unittest
+import sys
+import os
+import signal
+import socket
+from isc.net.addr import IPAddr
+import time
+import isc
+import fcntl
+
+class TestD2Daemon(unittest.TestCase):
+ def setUp(self):
+ # Don't redirect stdout/stderr here as we want to print out things
+ # during the test
+ #
+ # However, we do want to set the logging lock directory to somewhere
+ # to which we can write - use the current working directory. We then
+ # set the appropriate environment variable. os.putenv() may be not
+ # supported on some platforms as suggested in
+ # http://docs.python.org/release/3.2/library/os.html?highlight=putenv#os.environ:
+ # "If the platform supports the putenv() function...". It was checked
+ # that it does not work on Ubuntu. To overcome this problem we access
+ # os.environ directly.
+ lockdir_envvar = "B10_LOCKFILE_DIR_FROM_BUILD"
+ if lockdir_envvar not in os.environ:
+ os.environ[lockdir_envvar] = os.getcwd()
+
+ def tearDown(self):
+ pass
+
+ def readPipe(self, pipe_fd):
+ """
+ Reads bytes from a pipe and returns a character string. If nothing is
+ read, or if there is an error, an empty string is returned.
+
+ pipe_fd - Pipe file descriptor to read
+ """
+ try:
+ data = os.read(pipe_fd, 16384)
+ # Make sure we have a string
+ if (data is None):
+ data = ""
+ else:
+ data = str(data)
+ except OSError:
+ data = ""
+
+ return data
+
+ def runCommand(self, params, wait=1):
+ """
+ This method runs a command and returns a tuple: (returncode, stdout, stderr)
+ """
+ ## @todo: Convert this into generic method and reuse it in dhcp4 and dhcp6
+
+ print("Running command: %s" % (" ".join(params)))
+
+ # redirect stdout to a pipe so we can check that our
+ # process spawning is doing the right thing with stdout
+ self.stdout_old = os.dup(sys.stdout.fileno())
+ self.stdout_pipes = os.pipe()
+ os.dup2(self.stdout_pipes[1], sys.stdout.fileno())
+ os.close(self.stdout_pipes[1])
+
+ # do the same trick for stderr:
+ self.stderr_old = os.dup(sys.stderr.fileno())
+ self.stderr_pipes = os.pipe()
+ os.dup2(self.stderr_pipes[1], sys.stderr.fileno())
+ os.close(self.stderr_pipes[1])
+
+ # note that we use dup2() to restore the original stdout
+ # to the main program ASAP in each test... this prevents
+ # hangs reading from the child process (as the pipe is only
+ # open in the child), and also insures nice pretty output
+
+ pi = ProcessInfo('Test Process', params)
+ pi.spawn()
+ time.sleep(wait)
+ os.dup2(self.stdout_old, sys.stdout.fileno())
+ os.dup2(self.stderr_old, sys.stderr.fileno())
+ self.assertNotEqual(pi.process, None)
+ self.assertTrue(type(pi.pid) is int)
+
+ # Set non-blocking read on pipes. Process may not print anything
+ # on specific output and the we would hang without this.
+ fd = self.stdout_pipes[0]
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+ fd = self.stderr_pipes[0]
+ fl = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+
+ # As we don't know how long the subprocess will take to start and
+ # produce output, we'll loop and sleep for 250 ms between each
+ # iteration. To avoid an infinite loop, we'll loop for a maximum
+ # of five seconds: that should be enough.
+ for count in range(20):
+ # Read something from stderr and stdout (these reads don't block).
+ output = self.readPipe(self.stdout_pipes[0])
+ error = self.readPipe(self.stderr_pipes[0])
+
+ # If the process has already exited, or if it has output something,
+ # quit the loop now.
+ if pi.process.poll() is not None or len(error) > 0 or len(output) > 0:
+ break
+
+ # Process still running, try again in 250 ms.
+ time.sleep(0.25)
+
+ # Exited loop, kill the process if it is still running
+ if pi.process.poll() is None:
+ try:
+ pi.process.terminate()
+ except OSError:
+ print("Ignoring failed kill attempt. Process is dead already.")
+
+ # call this to get returncode, process should be dead by now
+ rc = pi.process.wait()
+
+ # Clean up our stdout/stderr munging.
+ os.dup2(self.stdout_old, sys.stdout.fileno())
+ os.close(self.stdout_old)
+ os.close(self.stdout_pipes[0])
+
+ os.dup2(self.stderr_old, sys.stderr.fileno())
+ os.close(self.stderr_old)
+ os.close(self.stderr_pipes[0])
+
+ # Free up resources (file descriptors) from the ProcessInfo object
+ # TODO: For some reason, this gives an error if the process has ended,
+ # although it does cause all descriptors still allocated to the
+ # object to be freed.
+ pi = None
+
+ print ("Process finished, return code=%d, stdout=%d bytes, stderr=%d bytes"
+ % (rc, len(output), len(error)) )
+
+ return (rc, output, error)
+
+ def test_alive(self):
+ print("Note: Simple test to verify that D2 server can be started.")
+ # note that "-s" for stand alone is necessary in order to flush the log output
+ # soon enough to catch it.
+ (returncode, output, error) = self.runCommand(["../b10-dhcp-ddns",
+ "-s", "-v"])
+ output_text = str(output) + str(error)
+ self.assertEqual(output_text.count("DCTL_STARTING"), 1)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/src/bin/d2/tests/d2_unittests.cc b/src/bin/d2/tests/d2_unittests.cc
new file mode 100644
index 0000000..bfba379
--- /dev/null
+++ b/src/bin/d2/tests/d2_unittests.cc
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/logger_support.h>
+#include <d2/d2_log.h>
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+
+ ::testing::InitGoogleTest(&argc, argv);
+
+ // See the documentation of the B10_* environment variables in
+ // src/lib/log/README for info on how to tweak logging
+ isc::log::initLogger();
+
+ int result = RUN_ALL_TESTS();
+
+ return (result);
+}
diff --git a/src/bin/d2/tests/d2_update_message_unittests.cc b/src/bin/d2/tests/d2_update_message_unittests.cc
new file mode 100644
index 0000000..28e01c7
--- /dev/null
+++ b/src/bin/d2/tests/d2_update_message_unittests.cc
@@ -0,0 +1,591 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <d2/d2_update_message.h>
+#include <d2/d2_zone.h>
+#include <dns/messagerenderer.h>
+#include <dns/rdataclass.h>
+#include <dns/rdata.h>
+#include <dns/rrttl.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+using namespace isc::dns;
+using namespace isc::dns::rdata;
+using namespace isc::util;
+
+namespace {
+
+// @brief Test fixture class for testing D2UpdateMessage object.
+class D2UpdateMessageTest : public ::testing::Test {
+public:
+ // @brief Constructor.
+ //
+ // Does nothing.
+ D2UpdateMessageTest() { }
+
+ // @brief Destructor.
+ //
+ // Does nothing.
+ ~D2UpdateMessageTest() { };
+
+ // @brief Return string representation of the name encoded in wire format.
+ //
+ // This function reads the number of bytes specified in the second
+ // argument from the buffer. It doesn't check if buffer has sufficient
+ // length for reading given number of bytes. Caller should verify it
+ // prior to calling this function.
+ //
+ // @param buf input buffer, its internal pointer will be moved to
+ // the position after a name being read from it.
+ // @param name_length length of the name stored in the buffer
+ // @param no_zero_byte if true it indicates that the given buffer does not
+ // comprise the zero byte, which signals end of the name. This is
+ // the case, when dealing with compressed messages which don't have
+ // this byte.
+ //
+ // @return string representation of the name.
+ std::string readNameFromWire(InputBuffer& buf, size_t name_length,
+ const bool no_zero_byte = false) {
+ std::vector<uint8_t> name_data;
+ // Create another InputBuffer which holds only the name in the wire
+ // format.
+ buf.readVector(name_data, name_length);
+ if (no_zero_byte) {
+ ++name_length;
+ name_data.push_back(0);
+ }
+ InputBuffer name_buf(&name_data[0], name_length);
+ // Parse the name and return its textual representation.
+ Name name(name_buf);
+ return (name.toText());
+ }
+};
+
+// This test verifies that DNS Update message ID can be set using
+// setId function.
+TEST_F(D2UpdateMessageTest, setId) {
+ // Message ID is initialized to 0.
+ D2UpdateMessage msg;
+ EXPECT_EQ(0, msg.getId());
+ // Override the default value and verify that it has been set.
+ msg.setId(0x1234);
+ EXPECT_EQ(0x1234, msg.getId());
+}
+
+// This test verifies that the DNS Update message RCODE can be set
+// using setRcode function.
+TEST_F(D2UpdateMessageTest, setRcode) {
+ D2UpdateMessage msg;
+ // Rcode must be explicitly set before it is accessed.
+ msg.setRcode(Rcode::NOERROR());
+ EXPECT_EQ(Rcode::NOERROR().getCode(), msg.getRcode().getCode());
+ // Let's override current value to make sure that getter does
+ // not return fixed value.
+ msg.setRcode(Rcode::NOTIMP());
+ EXPECT_EQ(Rcode::NOTIMP().getCode(), msg.getRcode().getCode());
+}
+
+// This test verifies that the Zone section in the DNS Update message
+// can be set.
+TEST_F(D2UpdateMessageTest, setZone) {
+ D2UpdateMessage msg;
+ // The zone pointer is initialized to NULL.
+ D2ZonePtr zone = msg.getZone();
+ EXPECT_FALSE(zone);
+ // Let's create a new Zone and check that it is returned
+ // via getter.
+ msg.setZone(Name("example.com"), RRClass::ANY());
+ zone = msg.getZone();
+ EXPECT_TRUE(zone);
+ EXPECT_EQ("example.com.", zone->getName().toText());
+ EXPECT_EQ(RRClass::ANY().getCode(), zone->getClass().getCode());
+
+ // Now, let's check that the existing Zone object can be
+ // overriden with a new one.
+ msg.setZone(Name("foo.example.com"), RRClass::NONE());
+ zone = msg.getZone();
+ EXPECT_TRUE(zone);
+ EXPECT_EQ("foo.example.com.", zone->getName().toText());
+ EXPECT_EQ(RRClass::NONE().getCode(), zone->getClass().getCode());
+}
+
+// This test verifies that the DNS message is properly decoded from the
+// wire format.
+TEST_F(D2UpdateMessageTest, fromWire) {
+ // The following table holds the DNS response in on-wire format.
+ // This message comprises the following sections:
+ // - HEADER
+ // - PREREQUISITE section with one RR
+ // - UPDATE section with 1 RR.
+ // Such a response may be generated by the DNS server as a result
+ // of copying the contents of the REQUEST message sent by DDNS client.
+ const uint8_t bin_msg[] = {
+ // HEADER section starts here (see RFC 2136, section 2).
+ 0x05, 0xAF, // ID=0x05AF
+ 0xA8, 0x6, // QR=1, Opcode=6, RCODE=YXDOMAIN
+ 0x0, 0x1, // ZOCOUNT=1
+ 0x0, 0x2, // PRCOUNT=2
+ 0x0, 0x1, // UPCOUNT=1
+ 0x0, 0x0, // ADCOUNT=0
+
+ // Zone section starts here. The The first field comprises
+ // the Zone name encoded as a set of labels, each preceded
+ // by a length of the following label. The whole Zone name is
+ // terminated with a NULL char.
+ // For Zone section format see (RFC 2136, section 2.3).
+ 0x7, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // example (7 is length)
+ 0x3, 0x63, 0x6F, 0x6D, //.com. (0x3 is a length)
+ 0x0, // NULL character terminates the Zone name.
+ 0x0, 0x6, // ZTYPE='SOA'
+ 0x0, 0x1, // ZCLASS='IN'
+
+ // Prerequisite section starts here. This section comprises two
+ // prerequisites:
+ // - 'Name is not in use'
+ // - 'Name is in use'
+ // See RFC 2136, section 2.4 for the format of Prerequisite section.
+ // Each prerequisite RR starts with its name. It is expressed in the
+ // compressed format as described in RFC 1035, section 4.1.4. The first
+ // label is expressed as in case of non-compressed name. It is preceded
+ // by the length value. The following two bytes are the pointer to the
+ // offset in the message where 'example.com' was used. That is, in the
+ // Zone name at offset 12. Pointer starts with two bits set - they
+ // mark start of the pointer.
+
+ // First prerequisite. NONE class indicates that the update requires
+ // that the name 'foo.example.com' is not use/
+ 0x03, 0x66, 0x6F, 0x6F, // foo.
+ 0xC0, 0x0C, // pointer to example.com.
+ 0x0, 0x1C, // TYPE=AAAA
+ 0x0, 0xFE, // CLASS=NONE
+ 0x0, 0x0, 0x0, 0x0, // TTL=0
+ 0x0, 0x0, // RDLENGTH=0
+
+ // Second prerequisite. ANY class indicates tha the update requires
+ // that the name 'bar.example.com' exists.
+ 0x03, 0x62, 0x61, 0x72, // bar.
+ 0xC0, 0x0C, // pointer to example.com.
+ 0x0, 0x1C, // TYPE=AAAA
+ 0x0, 0xFF, // CLASS=ANY
+ 0x0, 0x0, 0x0, 0x0, // TTL=0
+ 0x0, 0x0, // RDLENGTH=0
+
+ // Update section starts here. The format of this section conforms to
+ // RFC 2136, section 2.5. The name of the RR is again expressed in
+ // compressed format. The two pointer bytes point to the offset in the
+ // message where 'foo.example.com' was used already - 29.
+ 0xC0, 0x1D, // pointer to foo.example.com.
+ 0x0, 0x1C, // TYPE=AAAA
+ 0x0, 0x1, // CLASS=IN
+ 0xAA, 0xBB, 0xCC, 0xDD, // TTL=0xAABBCCDD
+ 0x0, 0x10, // RDLENGTH=16
+ // The following 16 bytes of RDATA hold IPv6 address: 2001:db8:1::1.
+ 0x20, 0x01, 0x0D, 0xB8, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
+ };
+ InputBuffer buf(bin_msg, sizeof(bin_msg));
+
+ // Create an object to be used to decode the message from the wire format.
+ D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+ // Decode the message.
+ ASSERT_NO_THROW(msg.fromWire(buf));
+
+ // Check that the message header is valid.
+ EXPECT_EQ(0x05AF, msg.getId());
+ EXPECT_EQ(D2UpdateMessage::RESPONSE, msg.getQRFlag());
+ EXPECT_EQ(Rcode::YXDOMAIN_CODE, msg.getRcode().getCode());
+
+ // The ZOCOUNT must contain exactly one zone. If it does, we should get
+ // the name, class and type of the zone and verify they are valid.
+ ASSERT_EQ(1, msg.getRRCount(D2UpdateMessage::SECTION_ZONE));
+ D2ZonePtr zone = msg.getZone();
+ ASSERT_TRUE(zone);
+ EXPECT_EQ("example.com.", zone->getName().toText());
+ EXPECT_EQ(RRClass::IN().getCode(), zone->getClass().getCode());
+
+ // Check the Prerequisite section. It should contain two records.
+ ASSERT_EQ(2, msg.getRRCount(D2UpdateMessage::SECTION_PREREQUISITE));
+
+ // Proceed to the first prerequisite.
+ RRsetIterator rrset_it =
+ msg.beginSection(D2UpdateMessage::SECTION_PREREQUISITE);
+ RRsetPtr prereq1 = *rrset_it;
+ ASSERT_TRUE(prereq1);
+ // Check record fields.
+ EXPECT_EQ("foo.example.com.", prereq1->getName().toText()); // NAME
+ EXPECT_EQ(RRType::AAAA().getCode(), prereq1->getType().getCode()); // TYPE
+ EXPECT_EQ(RRClass::NONE().getCode(),
+ prereq1->getClass().getCode()); // CLASS
+ EXPECT_EQ(0, prereq1->getTTL().getValue()); // TTL
+ EXPECT_EQ(0, prereq1->getRdataCount()); // RDLENGTH
+
+ // Move to next prerequisite section.
+ ++rrset_it;
+ RRsetPtr prereq2 = *rrset_it;
+ ASSERT_TRUE(prereq2);
+ // Check record fields.
+ EXPECT_EQ("bar.example.com.", prereq2->getName().toText()); // NAME
+ EXPECT_EQ(RRType::AAAA().getCode(), prereq2->getType().getCode()); // TYPE
+ EXPECT_EQ(RRClass::ANY().getCode(), prereq2->getClass().getCode()); // CLASS
+ EXPECT_EQ(0, prereq2->getTTL().getValue()); // TTL
+ EXPECT_EQ(0, prereq2->getRdataCount()); // RDLENGTH
+
+ // Check the Update section. There is only one record, so beginSection()
+ // should return the pointer to this sole record.
+ ASSERT_EQ(1, msg.getRRCount(D2UpdateMessage::SECTION_UPDATE));
+ rrset_it = msg.beginSection(D2UpdateMessage::SECTION_UPDATE);
+ RRsetPtr update = *rrset_it;
+ ASSERT_TRUE(update);
+ // Check the record fields.
+ EXPECT_EQ("foo.example.com.", update->getName().toText()); // NAME
+ EXPECT_EQ(RRType::AAAA().getCode(), update->getType().getCode()); // TYPE
+ EXPECT_EQ(RRClass::IN().getCode(), update->getClass().getCode()); // CLASS
+ EXPECT_EQ(0xAABBCCDD, update->getTTL().getValue()); // TTL
+ // There should be exactly one record holding the IPv6 address.
+ // This record can be accessed using RdataIterator. This record
+ // can be compared with the reference record, holding expected IPv6
+ // address using compare function.
+ ASSERT_EQ(1, update->getRdataCount());
+ RdataIteratorPtr rdata_it = update->getRdataIterator();
+ ASSERT_TRUE(rdata_it);
+ in::AAAA rdata_ref("2001:db8:1::1");
+ EXPECT_EQ(0, rdata_ref.compare(rdata_it->getCurrent()));
+
+ // @todo: at this point we don't test Additional Data records. We may
+ // consider implementing tests for it in the future.
+}
+
+// This test verifies that the fromWire function throws appropriate exception
+// if the message being parsed comprises invalid Opcode (is not a DNS Update).
+TEST_F(D2UpdateMessageTest, fromWireInvalidOpcode) {
+ // This is a binary representation of the DNS message.
+ // It comprises invalid Opcode=3, expected value is 6
+ // (Update).
+ const uint8_t bin_msg[] = {
+ 0x05, 0xAF, // ID=0x05AF
+ 0x98, 0x6, // QR=1, Opcode=3, RCODE=YXDOMAIN
+ 0x0, 0x0, // ZOCOUNT=0
+ 0x0, 0x0, // PRCOUNT=0
+ 0x0, 0x0, // UPCOUNT=0
+ 0x0, 0x0 // ADCOUNT=0
+ };
+ InputBuffer buf(bin_msg, sizeof(bin_msg));
+ // The 'true' argument passed to the constructor turns the
+ // message into the parse mode in which the fromWire function
+ // can be used to decode the binary mesasage data.
+ D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+ // When using invalid Opcode, the fromWire function should
+ // throw NotUpdateMessage exception.
+ EXPECT_THROW(msg.fromWire(buf), isc::d2::NotUpdateMessage);
+}
+
+// This test verifies that the fromWire function throws appropriate exception
+// if the message being parsed comprises invalid QR flag. The QR bit is
+// expected to be set to indicate that the message is a RESPONSE.
+TEST_F(D2UpdateMessageTest, fromWireInvalidQRFlag) {
+ // This is a binary representation of the DNS message.
+ // It comprises invalid QR flag = 0.
+ const uint8_t bin_msg[] = {
+ 0x05, 0xAF, // ID=0x05AF
+ 0x28, 0x6, // QR=0, Opcode=6, RCODE=YXDOMAIN
+ 0x0, 0x0, // ZOCOUNT=0
+ 0x0, 0x0, // PRCOUNT=0
+ 0x0, 0x0, // UPCOUNT=0
+ 0x0, 0x0 // ADCOUNT=0
+ };
+ InputBuffer buf(bin_msg, sizeof(bin_msg));
+ // The 'true' argument passed to the constructor turns the
+ // message into the parse mode in which the fromWire function
+ // can be used to decode the binary mesasage data.
+ D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+ // When using invalid QR flag, the fromWire function should
+ // throw InvalidQRFlag exception.
+ EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidQRFlag);
+}
+
+// This test verifies that the fromWire function throws appropriate exception
+// if the message being parsed comprises more than one (two in this case)
+// Zone records.
+TEST_F(D2UpdateMessageTest, fromWireTooManyZones) {
+ // This is a binary representation of the DNS message. This message
+ // comprises two Zone records.
+ const uint8_t bin_msg[] = {
+ 0x05, 0xAF, // ID=0x05AF
+ 0xA8, 0x6, // QR=1, Opcode=6, RCODE=YXDOMAIN
+ 0x0, 0x2, // ZOCOUNT=2
+ 0x0, 0x0, // PRCOUNT=0
+ 0x0, 0x0, // UPCOUNT=0
+ 0x0, 0x0, // ADCOUNT=0
+
+ // Start first Zone record.
+ 0x7, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // example (7 is length)
+ 0x3, 0x63, 0x6F, 0x6D, //.com. (0x3 is a length)
+ 0x0, // NULL character terminates the Zone name.
+ 0x0, 0x6, // ZTYPE='SOA'
+ 0x0, 0x1, // ZCLASS='IN'
+
+ // Start second Zone record. Presence of this record should result
+ // in error when parsing this message.
+ 0x3, 0x63, 0x6F, 0x6D, // com. (0x3 is a length)
+ 0x0, // NULL character terminates the Zone name.
+ 0x0, 0x6, // ZTYPE='SOA'
+ 0x0, 0x1 // ZCLASS='IN'
+ };
+ InputBuffer buf(bin_msg, sizeof(bin_msg));
+
+ // The 'true' argument passed to the constructor turns the
+ // message into the parse mode in which the fromWire function
+ // can be used to decode the binary mesasage data.
+ D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+ // When parsing a message with more than one Zone record,
+ // exception should be thrown.
+ EXPECT_THROW(msg.fromWire(buf), isc::d2::InvalidZoneSection);
+}
+
+// This test verifies that the wire format of the message is produced
+// in the render mode.
+TEST_F(D2UpdateMessageTest, toWire) {
+ D2UpdateMessage msg;
+ // Set message ID.
+ msg.setId(0x1234);
+ // Rcode to NOERROR.
+ msg.setRcode(Rcode(Rcode::NOERROR_CODE));
+
+ // Set Zone section. This section must comprise exactly
+ // one Zone. toWire function would fail if Zone is not set.
+ msg.setZone(Name("example.com"), RRClass::IN());
+
+ // Set prerequisities.
+
+ // 'Name Is Not In Use' prerequisite (RFC 2136, section 2.4.5)
+ RRsetPtr prereq1(new RRset(Name("foo.example.com"), RRClass::NONE(),
+ RRType::ANY(), RRTTL(0)));
+ msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq1);
+
+ // 'Name is In Use' prerequisite (RFC 2136, section 2.4.4)
+ RRsetPtr prereq2(new RRset(Name("bar.example.com"), RRClass::ANY(),
+ RRType::ANY(), RRTTL(0)));
+ msg.addRRset(D2UpdateMessage::SECTION_PREREQUISITE, prereq2);
+
+ // Set Update Section.
+
+ // Create RR holding a name being added. This RR is constructed
+ // in conformance to RFC 2136, section 2.5.1.
+ RRsetPtr updaterr1(new RRset(Name("foo.example.com"), RRClass::IN(),
+ RRType::A(), RRTTL(10)));
+ // RR record is of the type A, thus RDATA holds 4 octet Internet
+ // address. This address is 10.10.1.1.
+ char rdata1[] = {
+ 0xA, 0xA , 0x1, 0x1
+ };
+ InputBuffer buf_rdata1(rdata1, 4);
+ updaterr1->addRdata(createRdata(RRType::A(), RRClass::IN(), buf_rdata1,
+ buf_rdata1.getLength()));
+ // Add the RR to the message.
+ msg.addRRset(D2UpdateMessage::SECTION_UPDATE, updaterr1);
+
+ // Render message into the wire format.
+ MessageRenderer renderer;
+ ASSERT_NO_THROW(msg.toWire(renderer));
+
+ // Make sure that created packet is not truncated.
+ ASSERT_EQ(77, renderer.getLength());
+
+ // Create input buffer from the rendered data. InputBuffer
+ // is handy to validate the byte contents of the rendered
+ // message.
+ InputBuffer buf(renderer.getData(), renderer.getLength());
+
+ // Start validating the message header.
+
+ // Verify message ID.
+ EXPECT_EQ(0x1234, buf.readUint16());
+ // The 2-bytes following message ID comprise the following fields:
+ // - QR - 1 bit indicating that it is REQUEST. Should be 0.
+ // - Opcode - 4 bits which should hold value of 5 indicating this is
+ // an Update message. Binary form is "0101".
+ // - Z - These bits are unused for Update Message and should be 0.
+ // - RCODE - Response code, set to NOERROR for REQUEST. It is 0.
+ //8706391835
+ // The binary value is:
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // | QR| Opcode | Z | RCODE |
+ // +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+ // | 0 | 0 1 0 1 | 0 0 0 0 0 0 0 | 0 0 0 0 |
+ // +---+---+---+-------+---+---+---+---+---+---+---+---+---+---+---+
+ // and the hexadecimal representation is 0x2800.
+ EXPECT_EQ(0x2800, buf.readUint16());
+
+ // ZOCOUNT - holds the number of zones for the update. For Request
+ // message it must be exactly one record (RFC2136, section 2.3).
+ EXPECT_EQ(1, buf.readUint16());
+
+ // PRCOUNT - holds the number of prerequisites. Earlier we have added
+ // two prerequisites. Thus, expect that this conter is 2.
+ EXPECT_EQ(2, buf.readUint16());
+
+ // UPCOUNT - holds the number of RRs in the Update Section. We have
+ // added 1 RR, which adds the name foo.example.com to the Zone.
+ EXPECT_EQ(1, buf.readUint16());
+
+ // ADCOUNT - holds the number of RRs in the Additional Data Section.
+ EXPECT_EQ(0, buf.readUint16());
+
+ // Start validating the Zone section. This section comprises the
+ // following data:
+ // - ZNAME
+ // - ZTYPE
+ // - ZCLASS
+
+ // ZNAME holds 'example.com.' encoded as set of labels. Each label
+ // is preceded by its length. The name is ended with the byte holding
+ // zero value. This yields the total size of the name in wire format
+ // of 13 bytes.
+
+ // The simplest way to convert the name from wire format to a string
+ // is to use dns::Name class. It should be ok to rely on the Name class
+ // to decode the name, because it is unit tested elswhere.
+ std::string zone_name = readNameFromWire(buf, 13);
+ EXPECT_EQ("example.com.", zone_name);
+
+ // ZTYPE of the Zone section must be SOA according to RFC 2136,
+ // section 2.3.
+ EXPECT_EQ(RRType::SOA().getCode(), buf.readUint16());
+
+ // ZCLASS of the Zone section is IN.
+ EXPECT_EQ(RRClass::IN().getCode(), buf.readUint16());
+
+ // Start checks on Prerequisite section. Each prerequisite comprises
+ // the following fields:
+ // - NAME - name of the RR in wire format
+ // - TYPE - two octets with one of the RR TYPE codes
+ // - CLASS - two octets with one of the RR CLASS codes
+ // - TTL - a 32-bit signed integer specifying Time-To-Live
+ // - RDLENGTH - length of the RDATA field
+ // - RDATA - a variable length string of octets containing
+ // resource data.
+ // In case of this message, we expect to have two prerequisite RRs.
+ // Their structure is checked below.
+
+ // First prerequisite should comprise the 'Name is not in use prerequisite'
+ // for 'foo.example.com'.
+
+ // Check the name first. Message renderer is using compression for domain
+ // names as described in RFC 1035, section 4.1.4. The name in this RR is
+ // foo.example.com. The name of the zone is example.com and it has occured
+ // in this message already at offset 12 (the size of the header is 12).
+ // Therefore, name of this RR is encoded as 'foo', followed by a pointer
+ // to offset in this message where the remainder of this name was used.
+ // This pointer has the following format:
+ // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ // | 1 1| OFFSET |
+ // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ // | 1 1| 0 0 0 0 0 0 0 0 0 0 1 1 0 0|
+ // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ // which has a following hexadecimal representation: 0xC00C
+
+ // Let's read the non-compressed part first - 'foo.'
+ std::string name_prereq1 = readNameFromWire(buf, 4, true);
+ EXPECT_EQ("foo.", name_prereq1);
+ // The remaining two bytes hold the pointer to 'example.com'.
+ EXPECT_EQ(0xC00C, buf.readUint16());
+ // TYPE is ANY
+ EXPECT_EQ(RRType::ANY().getCode(), buf.readUint16());
+ // CLASS is NONE
+ EXPECT_EQ(RRClass::NONE().getCode(), buf.readUint16());
+ // TTL is a 32-but value, expecting 0
+ EXPECT_EQ(0, buf.readUint32());
+ // There is no RDATA, so RDLENGTH is 0
+ EXPECT_EQ(0, buf.readUint16());
+
+ // Start checking second prerequisite.
+
+ std::string name_prereq2 = readNameFromWire(buf, 4, true);
+ EXPECT_EQ("bar.", name_prereq2);
+ // The remaining two bytes hold the pointer to 'example.com'.
+ EXPECT_EQ(0xC00C, buf.readUint16());
+ // TYPE is ANY
+ EXPECT_EQ(RRType::ANY().getCode(), buf.readUint16());
+ // CLASS is ANY
+ EXPECT_EQ(RRClass::ANY().getCode(), buf.readUint16());
+ // TTL is a 32-but value, expecting 0
+ EXPECT_EQ(0, buf.readUint32());
+ // There is no RDATA, so RDLENGTH is 0
+ EXPECT_EQ(0, buf.readUint16());
+
+ // Start checking Update section. This section contains RRset with
+ // one A RR.
+
+ // The name of the RR is 'foo.example.com'. It is encoded in the
+ // compressed format - as a pointer to the name of prerequisite 1.
+ // This name is in offset 0x1D in this message.
+ EXPECT_EQ(0xC01D, buf.readUint16());
+ // TYPE is A
+ EXPECT_EQ(RRType::A().getCode(), buf.readUint16());
+ // CLASS is IN (same as zone class)
+ EXPECT_EQ(RRClass::IN().getCode(), buf.readUint16());
+ // TTL is a 32-but value, set here to 10.
+ EXPECT_EQ(10, buf.readUint32());
+ // For A records, the RDATA comprises the 4-byte Internet address.
+ // So, RDLENGTH is 4.
+ EXPECT_EQ(4, buf.readUint16());
+ // We have stored the following address in RDATA field: 10.10.1.1
+ // (which is 0A 0A 01 01) in hexadecimal format.
+ EXPECT_EQ(0x0A0A0101, buf.readUint32());
+
+ // @todo: consider extending this test to verify Additional Data
+ // section.
+}
+
+// This test verifies that an attempt to call toWire function on the
+// received message will result in an exception.
+TEST_F(D2UpdateMessageTest, toWireInvalidQRFlag) {
+ // This is a binary representation of the DNS message.
+ // This message is valid and should be parsed with no
+ // error.
+ const uint8_t bin_msg[] = {
+ 0x05, 0xAF, // ID=0x05AF
+ 0xA8, 0x6, // QR=1, Opcode=6, RCODE=YXDOMAIN
+ 0x0, 0x0, // ZOCOUNT=0
+ 0x0, 0x0, // PRCOUNT=0
+ 0x0, 0x0, // UPCOUNT=0
+ 0x0, 0x0 // ADCOUNT=0
+ };
+
+ InputBuffer buf(bin_msg, sizeof(bin_msg));
+ // The 'true' argument passed to the constructor turns the
+ // message into the parse mode in which the fromWire function
+ // can be used to decode the binary mesasage data.
+ D2UpdateMessage msg(D2UpdateMessage::INBOUND);
+ ASSERT_NO_THROW(msg.fromWire(buf));
+
+ // The message is parsed. The QR Flag should now indicate that
+ // it is a Response message.
+ ASSERT_EQ(D2UpdateMessage::RESPONSE, msg.getQRFlag());
+
+ // An attempt to call toWire on the Response message should
+ // result in the InvalidQRFlag exception.
+ MessageRenderer renderer;
+ EXPECT_THROW(msg.toWire(renderer), isc::d2::InvalidQRFlag);
+}
+
+} // End of anonymous namespace
diff --git a/src/bin/d2/tests/d2_update_mgr_unittests.cc b/src/bin/d2/tests/d2_update_mgr_unittests.cc
new file mode 100644
index 0000000..2711a71
--- /dev/null
+++ b/src/bin/d2/tests/d2_update_mgr_unittests.cc
@@ -0,0 +1,772 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_asio.h>
+#include <d2/d2_update_mgr.h>
+#include <util/time_utilities.h>
+#include <d_test_stubs.h>
+#include <nc_test_utils.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <vector>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp_ddns;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Wrapper class for D2UpdateMgr providing access to non-public methods.
+///
+/// This class facilitates testing by making non-public methods accessible so
+/// they can be invoked directly in test routines.
+class D2UpdateMgrWrapper : public D2UpdateMgr {
+public:
+ /// @brief Constructor
+ ///
+ /// Parameters match those needed by D2UpdateMgr.
+ D2UpdateMgrWrapper(D2QueueMgrPtr& queue_mgr, D2CfgMgrPtr& cfg_mgr,
+ IOServicePtr& io_service,
+ const size_t max_transactions = MAX_TRANSACTIONS_DEFAULT)
+ : D2UpdateMgr(queue_mgr, cfg_mgr, io_service, max_transactions) {
+ }
+
+ /// @brief Destructor
+ virtual ~D2UpdateMgrWrapper() {
+ }
+
+ // Expose the protected methods to be tested.
+ using D2UpdateMgr::checkFinishedTransactions;
+ using D2UpdateMgr::pickNextJob;
+ using D2UpdateMgr::makeTransaction;
+};
+
+/// @brief Defines a pointer to a D2UpdateMgr instance.
+typedef boost::shared_ptr<D2UpdateMgrWrapper> D2UpdateMgrWrapperPtr;
+
+/// @brief Test fixture for testing D2UpdateMgr.
+///
+/// Note this class uses D2UpdateMgrWrapper class to exercise non-public
+/// aspects of D2UpdateMgr. D2UpdateMgr depends on both D2QueueMgr and
+/// D2CfgMgr. This fixture provides an instance of each, plus a canned,
+/// valid DHCP_DDNS configuration sufficient to test D2UpdateMgr's basic
+/// functions.
+class D2UpdateMgrTest : public TimedIO, public ConfigParseTest {
+public:
+ D2QueueMgrPtr queue_mgr_;
+ D2CfgMgrPtr cfg_mgr_;
+ D2UpdateMgrWrapperPtr update_mgr_;
+ std::vector<NameChangeRequestPtr> canned_ncrs_;
+ size_t canned_count_;
+
+ D2UpdateMgrTest() {
+ queue_mgr_.reset(new D2QueueMgr(io_service_));
+ cfg_mgr_.reset(new D2CfgMgr());
+ update_mgr_.reset(new D2UpdateMgrWrapper(queue_mgr_, cfg_mgr_,
+ io_service_));
+ makeCannedNcrs();
+ makeCannedConfig();
+ }
+
+ ~D2UpdateMgrTest() {
+ }
+
+ /// @brief Creates a list of valid NameChangeRequest.
+ ///
+ /// This method builds a list of NameChangeRequests from a single
+ /// JSON string request. Each request is assigned a unique DHCID.
+ void makeCannedNcrs() {
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"my.example.com.\" , "
+ " \"ip_address\" : \"192.168.1.2\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ const char* dhcids[] = { "111111", "222222", "333333", "444444"};
+ canned_count_ = 4;
+ for (int i = 0; i < canned_count_; i++) {
+ dhcp_ddns::NameChangeRequestPtr ncr = NameChangeRequest::
+ fromJSON(msg_str);
+ ncr->setDhcid(dhcids[i]);
+ ncr->setChangeType(i % 2 == 0 ?
+ dhcp_ddns::CHG_ADD : dhcp_ddns::CHG_REMOVE);
+ canned_ncrs_.push_back(ncr);
+ }
+ }
+
+ /// @brief Seeds configuration manager with a valid DHCP_DDNS configuration.
+ void makeCannedConfig() {
+ std::string canned_config_ =
+ "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"192.168.1.33\" , "
+ "\"port\" : 88 , "
+ "\"tsig_keys\": [] ,"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"example.com.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\", \"port\" : 5301 } "
+ " ] },"
+ "{ \"name\": \"org.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] }, "
+ "] }, "
+ "\"reverse_ddns\" : { "
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"1.168.192.in-addr.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\", \"port\" : 5301 } "
+ " ] }, "
+ "{ \"name\": \"2.0.3.0.8.B.D.0.1.0.0.2.ip6.arpa.\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.1\" } "
+ " ] } "
+ "] } }";
+
+ // If this configuration fails to parse most tests will fail.
+ ASSERT_TRUE(fromJSON(canned_config_));
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+ }
+
+ /// @brief Fakes the completion of a given transaction.
+ ///
+ /// @param index index of the request from which the transaction was formed.
+ /// @param status completion status to assign to the request
+ void completeTransaction(const size_t index,
+ const dhcp_ddns::NameChangeStatus& status) {
+ // add test on index
+ if (index >= canned_count_) {
+ ADD_FAILURE() << "request index is out of range: " << index;
+ }
+
+ const dhcp_ddns::D2Dhcid key = canned_ncrs_[index]->getDhcid();
+
+ // locate the transaction based on the request DHCID
+ TransactionList::iterator pos = update_mgr_->findTransaction(key);
+ if (pos == update_mgr_->transactionListEnd()) {
+ ADD_FAILURE() << "cannot find transaction for key: " << key.toStr();
+ }
+
+ NameChangeTransactionPtr trans = (*pos).second;
+ // Update the status of the request
+ trans->getNcr()->setStatus(status);
+ // End the model.
+ trans->endModel();
+ }
+
+ /// @brief Determines if any transactions are waiting for IO completion.
+ ///
+ /// @returns True if isModelWaiting() is true for at least one of the current
+ /// transactions.
+ bool anyoneWaiting() {
+ TransactionList::iterator it = update_mgr_->transactionListBegin();
+ while (it != update_mgr_->transactionListEnd()) {
+ if (((*it).second)->isModelWaiting()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// @brief Process events until all requests have been completed.
+ ///
+ /// This method iteratively calls D2UpdateMgr::sweep and executes
+ /// IOService calls until both the request queue and transaction list
+ /// are empty or a timeout occurs. Note that in addition to the safety
+ /// timer, the number of passes through the loop is also limited to
+ /// a given number. This is a failsafe to guard against an infinite loop
+ /// in the test.
+ ///
+ /// @param timeout_millisec Maximum amount of time to allow IOService to
+ /// run before failing the test. Note, If the intent of a test is to
+ /// verify proper handling of DNSClient timeouts, the value must be
+ /// slightly larger than that being used for DNSClient timeout value.
+ /// @param max_passes Maximum number of times through the loop before
+ /// failing the test. The default value of twenty is likely large enough
+ /// for most tests. The number of passes required for a given test can
+ /// vary.
+ void processAll(unsigned int timeout_millisec =
+ NameChangeTransaction::DNS_UPDATE_DEFAULT_TIMEOUT + 100,
+ size_t max_passes = 100) {
+ // Loop until all the transactions have been dequeued and run through to
+ // completion.
+ size_t passes = 0;
+ size_t handlers = 0;
+ while (update_mgr_->getQueueCount() ||
+ update_mgr_->getTransactionCount()) {
+ ++passes;
+ update_mgr_->sweep();
+ // If any transactions are waiting on IO, run the service.
+ if (anyoneWaiting()) {
+ int cnt = runTimedIO(timeout_millisec);
+
+ // If cnt is zero then the service stopped unexpectedly.
+ if (cnt == 0) {
+ ADD_FAILURE()
+ << "processALL: IO service stopped unexpectedly,"
+ << " passes: " << passes << ", handlers executed: "
+ << handlers;
+ }
+
+ handlers += cnt;
+ }
+
+ // This is a last resort fail safe to ensure we don't go around
+ // forever. We cut it off the number of passes at 100 (default
+ // value). This is roughly ten times the number for the longest
+ // test (currently, multiTransactionTimeout).
+ if (passes > max_passes) {
+ ADD_FAILURE() << "processALL failed, too many passes: "
+ << passes << ", total handlers executed: " << handlers;
+ }
+ }
+ }
+
+};
+
+/// @brief Tests the D2UpdateMgr construction.
+/// This test verifies that:
+/// 1. Construction with invalid queue manager is not allowed
+/// 2. Construction with invalid configuration manager is not allowed
+/// 3. Construction with max transactions of zero is not allowed
+/// 4. Default construction works and max transactions is defaulted properly
+/// 5. Construction with custom max transactions works properly
+TEST(D2UpdateMgr, construction) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+ D2QueueMgrPtr queue_mgr;
+ D2CfgMgrPtr cfg_mgr;
+ D2UpdateMgrPtr update_mgr;
+
+ // Verify that constructor fails if given an invalid queue manager.
+ ASSERT_NO_THROW(cfg_mgr.reset(new D2CfgMgr()));
+ EXPECT_THROW(D2UpdateMgr(queue_mgr, cfg_mgr, io_service),
+ D2UpdateMgrError);
+
+ // Verify that constructor fails if given an invalid config manager.
+ ASSERT_NO_THROW(queue_mgr.reset(new D2QueueMgr(io_service)));
+ ASSERT_NO_THROW(cfg_mgr.reset());
+ EXPECT_THROW(D2UpdateMgr(queue_mgr, cfg_mgr, io_service),
+ D2UpdateMgrError);
+
+ ASSERT_NO_THROW(cfg_mgr.reset(new D2CfgMgr()));
+
+ // Verify that constructor fails with invalid io_service.
+ io_service.reset();
+ EXPECT_THROW(D2UpdateMgr(queue_mgr, cfg_mgr, io_service),
+ D2UpdateMgrError);
+ io_service.reset(new isc::asiolink::IOService());
+
+ // Verify that max transactions cannot be zero.
+ EXPECT_THROW(D2UpdateMgr(queue_mgr, cfg_mgr, io_service, 0),
+ D2UpdateMgrError);
+
+ // Verify that given valid values, constructor works.
+ ASSERT_NO_THROW(update_mgr.reset(new D2UpdateMgr(queue_mgr, cfg_mgr,
+ io_service)));
+
+ // Verify that max transactions defaults properly.
+ EXPECT_EQ(D2UpdateMgr::MAX_TRANSACTIONS_DEFAULT,
+ update_mgr->getMaxTransactions());
+
+
+ // Verify that constructor permits custom max transactions.
+ ASSERT_NO_THROW(update_mgr.reset(new D2UpdateMgr(queue_mgr, cfg_mgr,
+ io_service, 100)));
+
+ // Verify that max transactions is correct.
+ EXPECT_EQ(100, update_mgr->getMaxTransactions());
+}
+
+/// @brief Tests the D2UpdateManager's transaction list services
+/// This test verifies that:
+/// 1. A transaction can be added to the list.
+/// 2. Finding a transaction in the list by key works correctly.
+/// 3. Looking for a non-existent transaction works properly.
+/// 4. Attempting to add a transaction for a DHCID already in the list fails.
+/// 5. Removing a transaction by key works properly.
+/// 6. Attempting to remove an non-existent transaction does no harm.
+TEST_F(D2UpdateMgrTest, transactionList) {
+ // Grab a canned request for test purposes.
+ NameChangeRequestPtr& ncr = canned_ncrs_[0];
+ TransactionList::iterator pos;
+
+ // Verify that we can add a transaction.
+ EXPECT_NO_THROW(update_mgr_->makeTransaction(ncr));
+ EXPECT_EQ(1, update_mgr_->getTransactionCount());
+
+ // Verify that we can find a transaction by key.
+ EXPECT_NO_THROW(pos = update_mgr_->findTransaction(ncr->getDhcid()));
+ EXPECT_TRUE(pos != update_mgr_->transactionListEnd());
+
+ // Verify that convenience method has same result.
+ EXPECT_TRUE(update_mgr_->hasTransaction(ncr->getDhcid()));
+
+ // Verify that we will not find a transaction that isn't there.
+ dhcp_ddns::D2Dhcid bogus_id("FFFF");
+ EXPECT_NO_THROW(pos = update_mgr_->findTransaction(bogus_id));
+ EXPECT_TRUE(pos == update_mgr_->transactionListEnd());
+
+ // Verify that convenience method has same result.
+ EXPECT_FALSE(update_mgr_->hasTransaction(bogus_id));
+
+ // Verify that adding a transaction for the same key fails.
+ EXPECT_THROW(update_mgr_->makeTransaction(ncr), D2UpdateMgrError);
+ EXPECT_EQ(1, update_mgr_->getTransactionCount());
+
+ // Verify the we can remove a transaction by key.
+ EXPECT_NO_THROW(update_mgr_->removeTransaction(ncr->getDhcid()));
+ EXPECT_EQ(0, update_mgr_->getTransactionCount());
+
+ // Verify the we can try to remove a non-existent transaction without harm.
+ EXPECT_NO_THROW(update_mgr_->removeTransaction(ncr->getDhcid()));
+}
+
+/// @brief Tests D2UpdateManager's checkFinishedTransactions method.
+/// This test verifies that:
+/// 1. Completed transactions are removed from the transaction list.
+/// 2. Failed transactions are removed from the transaction list.
+/// @todo This test will need to expand if and when checkFinishedTransactions
+/// method expands to do more than remove them from the list.
+TEST_F(D2UpdateMgrTest, checkFinishedTransaction) {
+ // Ensure we have at least 4 canned requests with which to work.
+ ASSERT_TRUE(canned_count_ >= 4);
+
+ // Create a transaction for each canned request.
+ for (int i = 0; i < canned_count_; i++) {
+ EXPECT_NO_THROW(update_mgr_->makeTransaction(canned_ncrs_[i]));
+ }
+ // Verify we have that the transaction count is correct.
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+
+ // Verify that all four transactions have been started.
+ TransactionList::iterator pos;
+ EXPECT_NO_THROW(pos = update_mgr_->transactionListBegin());
+ while (pos != update_mgr_->transactionListEnd()) {
+ NameChangeTransactionPtr trans = (*pos).second;
+ ASSERT_EQ(dhcp_ddns::ST_PENDING, trans->getNcrStatus());
+ ASSERT_TRUE(trans->isModelRunning());
+ ++pos;
+ }
+
+ // Verify that invoking checkFinishedTransactions does not throw.
+ EXPECT_NO_THROW(update_mgr_->checkFinishedTransactions());
+
+ // Since nothing is running IOService, the all four transactions should
+ // still be in the list.
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+
+ // Now "complete" two of the four.
+ // Simulate a successful completion.
+ completeTransaction(1, dhcp_ddns::ST_COMPLETED);
+
+ // Simulate a failed completion.
+ completeTransaction(3, dhcp_ddns::ST_FAILED);
+
+ // Verify that invoking checkFinishedTransactions does not throw.
+ EXPECT_NO_THROW(update_mgr_->checkFinishedTransactions());
+
+ // Verify that the list of transactions has decreased by two.
+ EXPECT_EQ(canned_count_ - 2, update_mgr_->getTransactionCount());
+
+ // Vefity that the transaction list is correct.
+ EXPECT_TRUE(update_mgr_->hasTransaction(canned_ncrs_[0]->getDhcid()));
+ EXPECT_FALSE(update_mgr_->hasTransaction(canned_ncrs_[1]->getDhcid()));
+ EXPECT_TRUE(update_mgr_->hasTransaction(canned_ncrs_[2]->getDhcid()));
+ EXPECT_FALSE(update_mgr_->hasTransaction(canned_ncrs_[3]->getDhcid()));
+}
+
+/// @brief Tests D2UpdateManager's pickNextJob method.
+/// This test verifies that:
+/// 1. pickNextJob will select and make transactions from NCR queue.
+/// 2. Requests are removed from the queue once selected
+/// 3. Requests for DHCIDs with transactions already in progress are not
+/// selected.
+/// 4. Requests with no matching servers are removed from the queue and
+/// discarded.
+TEST_F(D2UpdateMgrTest, pickNextJob) {
+ // Ensure we have at least 4 canned requests with which to work.
+ ASSERT_TRUE(canned_count_ >= 4);
+
+ // Put each transaction on the queue.
+ for (int i = 0; i < canned_count_; i++) {
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[i]));
+ }
+
+ // Invoke pickNextJob canned_count_ times which should create a
+ // transaction for each canned ncr.
+ for (int i = 0; i < canned_count_; i++) {
+ EXPECT_NO_THROW(update_mgr_->pickNextJob());
+ EXPECT_EQ(i + 1, update_mgr_->getTransactionCount());
+ EXPECT_TRUE(update_mgr_->hasTransaction(canned_ncrs_[i]->getDhcid()));
+ }
+
+ // Verify that the queue has been drained.
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+
+ // Now verify that a subsequent request for a DCHID for which a
+ // transaction is in progress, is not dequeued.
+ // First add the "subsequent" request.
+ dhcp_ddns::NameChangeRequestPtr
+ subsequent_ncr(new dhcp_ddns::NameChangeRequest(*(canned_ncrs_[2])));
+ EXPECT_NO_THROW(queue_mgr_->enqueue(subsequent_ncr));
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Verify that invoking pickNextJob:
+ // 1. Does not throw
+ // 2. Does not make a new transaction
+ // 3. Does not dequeue the entry
+ EXPECT_NO_THROW(update_mgr_->pickNextJob());
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Clear out the queue and transaction list.
+ queue_mgr_->clearQueue();
+ update_mgr_->clearTransactionList();
+
+ // Make a forward change NCR with an FQDN that has no forward match.
+ dhcp_ddns::NameChangeRequestPtr
+ bogus_ncr(new dhcp_ddns::NameChangeRequest(*(canned_ncrs_[0])));
+ bogus_ncr->setForwardChange(true);
+ bogus_ncr->setReverseChange(false);
+ bogus_ncr->setFqdn("bogus.forward.domain.com");
+
+ // Put it on the queue up
+ ASSERT_NO_THROW(queue_mgr_->enqueue(bogus_ncr));
+
+ // Verify that invoking pickNextJob:
+ // 1. Does not throw
+ // 2. Does not make a new transaction
+ // 3. Does dequeue the entry
+ EXPECT_NO_THROW(update_mgr_->pickNextJob());
+ EXPECT_EQ(0, update_mgr_->getTransactionCount());
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+
+ // Make a reverse change NCR with an FQDN that has no reverse match.
+ bogus_ncr.reset(new dhcp_ddns::NameChangeRequest(*(canned_ncrs_[0])));
+ bogus_ncr->setForwardChange(false);
+ bogus_ncr->setReverseChange(true);
+ bogus_ncr->setIpAddress("77.77.77.77");
+
+ // Verify that invoking pickNextJob:
+ // 1. does not throw
+ // 2. Does not make a new transaction
+ // 3. Does dequeue the entry
+ EXPECT_NO_THROW(update_mgr_->pickNextJob());
+ EXPECT_EQ(0, update_mgr_->getTransactionCount());
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+}
+
+/// @brief Tests D2UpdateManager's sweep method.
+/// Since sweep is primarily a wrapper around checkFinishedTransactions and
+/// pickNextJob, along with checks on maximum transaction limits, it mostly
+/// verifies that these three pieces work together to move process jobs.
+/// Most of what is tested here is tested above.
+TEST_F(D2UpdateMgrTest, sweep) {
+ // Ensure we have at least 4 canned requests with which to work.
+ ASSERT_TRUE(canned_count_ >= 4);
+
+ // Set max transactions to same as current transaction count.
+ EXPECT_NO_THROW(update_mgr_->setMaxTransactions(canned_count_));
+ EXPECT_EQ(canned_count_, update_mgr_->getMaxTransactions());
+
+ // Put each transaction on the queue.
+ for (int i = 0; i < canned_count_; i++) {
+ EXPECT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[i]));
+ }
+
+ // Invoke sweep canned_count_ times which should create a
+ // transaction for each canned ncr.
+ for (int i = 0; i < canned_count_; i++) {
+ EXPECT_NO_THROW(update_mgr_->sweep());
+ EXPECT_EQ(i + 1, update_mgr_->getTransactionCount());
+ EXPECT_TRUE(update_mgr_->hasTransaction(canned_ncrs_[i]->getDhcid()));
+ }
+
+ // Verify that the queue has been drained.
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+
+ // Verify max transactions can't be less than current transaction count.
+ EXPECT_THROW(update_mgr_->setMaxTransactions(1), D2UpdateMgrError);
+
+ // Queue up a request for a DHCID which has a transaction in progress.
+ dhcp_ddns::NameChangeRequestPtr
+ subsequent_ncr(new dhcp_ddns::NameChangeRequest(*(canned_ncrs_[2])));
+ EXPECT_NO_THROW(queue_mgr_->enqueue(subsequent_ncr));
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Verify that invoking sweep, does not dequeue the job nor make a
+ // transaction for it.
+ EXPECT_NO_THROW(update_mgr_->sweep());
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Mark the transaction complete.
+ completeTransaction(2, dhcp_ddns::ST_COMPLETED);
+
+ // Verify that invoking sweep, cleans up the completed transaction,
+ // dequeues the queued job and adds its transaction to the list.
+ EXPECT_NO_THROW(update_mgr_->sweep());
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+
+ // Queue up a request from a new DHCID.
+ dhcp_ddns::NameChangeRequestPtr
+ another_ncr(new dhcp_ddns::NameChangeRequest(*(canned_ncrs_[0])));
+ another_ncr->setDhcid("AABBCCDDEEFF");
+ EXPECT_NO_THROW(queue_mgr_->enqueue(another_ncr));
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Verify that sweep does not dequeue the new request as we are at
+ // maximum transaction count.
+ EXPECT_NO_THROW(update_mgr_->sweep());
+ EXPECT_EQ(canned_count_, update_mgr_->getTransactionCount());
+ EXPECT_EQ(1, update_mgr_->getQueueCount());
+
+ // Set max transactions to same as current transaction count.
+ EXPECT_NO_THROW(update_mgr_->setMaxTransactions(canned_count_ + 1));
+
+ // Verify that invoking sweep, dequeues the request and creates
+ // a transaction for it.
+ EXPECT_NO_THROW(update_mgr_->sweep());
+ EXPECT_EQ(canned_count_ + 1, update_mgr_->getTransactionCount());
+ EXPECT_EQ(0, update_mgr_->getQueueCount());
+
+ // Verify that clearing transaction list works.
+ EXPECT_NO_THROW(update_mgr_->clearTransactionList());
+ EXPECT_EQ(0, update_mgr_->getTransactionCount());
+}
+
+/// @brief Tests integration of NameAddTransaction
+/// This test verifies that update manager can create and manage a
+/// NameAddTransaction from start to finish. It utilizes a fake server
+/// which responds to all requests sent with NOERROR, simulating a
+/// successful addition. The transaction processes both forward and
+/// reverse changes.
+TEST_F(D2UpdateMgrTest, addTransaction) {
+ // Put each transaction on the queue.
+ canned_ncrs_[0]->setChangeType(dhcp_ddns::CHG_ADD);
+ canned_ncrs_[0]->setReverseChange(true);
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[0]));
+
+ // Call sweep once, this should:
+ // 1. Dequeue the request
+ // 2. Create the transaction
+ // 3. Start the transaction
+ ASSERT_NO_THROW(update_mgr_->sweep());
+
+ // Get a copy of the transaction.
+ TransactionList::iterator pos = update_mgr_->transactionListBegin();
+ ASSERT_TRUE (pos != update_mgr_->transactionListEnd());
+ NameChangeTransactionPtr trans = (*pos).second;
+ ASSERT_TRUE(trans);
+
+ // At this point the transaction should have constructed
+ // and sent the DNS request.
+ ASSERT_TRUE(trans->getCurrentServer());
+ ASSERT_TRUE(trans->isModelRunning());
+ ASSERT_EQ(1, trans->getUpdateAttempts());
+ ASSERT_EQ(StateModel::NOP_EVT, trans->getNextEvent());
+
+ // Create a server based on the transaction's current server, and
+ // start it listening.
+ FauxServer server(*io_service_, *(trans->getCurrentServer()));
+ server.receive(FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+ // Run sweep and IO until everything is done.
+ processAll();
+
+ // Verify that model succeeded.
+ EXPECT_FALSE(trans->didModelFail());
+
+ // Both completion flags should be true.
+ EXPECT_TRUE(trans->getForwardChangeCompleted());
+ EXPECT_TRUE(trans->getReverseChangeCompleted());
+
+ // Verify that we went through success state.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ trans->getPrevState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ trans->getLastEvent());
+}
+
+/// @brief Tests integration of NameRemoveTransaction
+/// This test verifies that update manager can create and manage a
+/// NameRemoveTransaction from start to finish. It utilizes a fake server
+/// which responds to all requests sent with NOERROR, simulating a
+/// successful addition. The transaction processes both forward and
+/// reverse changes.
+TEST_F(D2UpdateMgrTest, removeTransaction) {
+ // Put each transaction on the queue.
+ canned_ncrs_[0]->setChangeType(dhcp_ddns::CHG_REMOVE);
+ canned_ncrs_[0]->setReverseChange(true);
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[0]));
+
+ // Call sweep once, this should:
+ // 1. Dequeue the request
+ // 2. Create the transaction
+ // 3. Start the transaction
+ ASSERT_NO_THROW(update_mgr_->sweep());
+
+ // Get a copy of the transaction.
+ TransactionList::iterator pos = update_mgr_->transactionListBegin();
+ ASSERT_TRUE (pos != update_mgr_->transactionListEnd());
+ NameChangeTransactionPtr trans = (*pos).second;
+ ASSERT_TRUE(trans);
+
+ // At this point the transaction should have constructed
+ // and sent the DNS request.
+ ASSERT_TRUE(trans->getCurrentServer());
+ ASSERT_TRUE(trans->isModelRunning());
+ ASSERT_EQ(1, trans->getUpdateAttempts());
+ ASSERT_EQ(StateModel::NOP_EVT, trans->getNextEvent());
+
+ // Create a server based on the transaction's current server,
+ // and start it listening.
+ FauxServer server(*io_service_, *(trans->getCurrentServer()));
+ server.receive(FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+ // Run sweep and IO until everything is done.
+ processAll();
+
+ // Verify that model succeeded.
+ EXPECT_FALSE(trans->didModelFail());
+
+ // Both completion flags should be true.
+ EXPECT_TRUE(trans->getForwardChangeCompleted());
+ EXPECT_TRUE(trans->getReverseChangeCompleted());
+
+ // Verify that we went through success state.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ trans->getPrevState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ trans->getLastEvent());
+}
+
+
+/// @brief Tests handling of a transaction which fails.
+/// This test verifies that update manager correctly concludes a transaction
+/// which fails to complete successfully. The failure simulated is repeated
+/// corrupt responses from the server, which causes an exhaustion of the
+/// available servers.
+TEST_F(D2UpdateMgrTest, errorTransaction) {
+ // Put each transaction on the queue.
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[0]));
+
+ // Call sweep once, this should:
+ // 1. Dequeue the request
+ // 2. Create the transaction
+ // 3. Start the transaction
+ ASSERT_NO_THROW(update_mgr_->sweep());
+
+ // Get a copy of the transaction.
+ TransactionList::iterator pos = update_mgr_->transactionListBegin();
+ ASSERT_TRUE (pos != update_mgr_->transactionListEnd());
+ NameChangeTransactionPtr trans = (*pos).second;
+ ASSERT_TRUE(trans);
+
+ ASSERT_TRUE(trans->isModelRunning());
+ ASSERT_EQ(1, trans->getUpdateAttempts());
+ ASSERT_EQ(StateModel::NOP_EVT, trans->getNextEvent());
+ ASSERT_TRUE(trans->getCurrentServer());
+
+ // Create a server and start it listening.
+ FauxServer server(*io_service_, *(trans->getCurrentServer()));
+ server.receive(FauxServer::CORRUPT_RESP);
+
+ // Run sweep and IO until everything is done.
+ processAll();
+
+ // Verify that model succeeded.
+ EXPECT_FALSE(trans->didModelFail());
+
+ // Both completion flags should be false.
+ EXPECT_FALSE(trans->getForwardChangeCompleted());
+ EXPECT_FALSE(trans->getReverseChangeCompleted());
+
+ // Verify that we went through success state.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ trans->getPrevState());
+ EXPECT_EQ(NameChangeTransaction::NO_MORE_SERVERS_EVT,
+ trans->getLastEvent());
+
+
+}
+
+/// @brief Tests processing of multiple transactions.
+/// This test verifies that update manager can create and manage a multiple
+/// transactions, concurrently. It uses a fake server that responds to all
+/// requests sent with NOERROR, simulating successful DNS updates. The
+/// transactions are a mix of both adds and removes.
+TEST_F(D2UpdateMgrTest, multiTransaction) {
+ // Queue up all the requests.
+ int test_count = canned_count_;
+ for (int i = test_count; i > 0; i--) {
+ canned_ncrs_[i-1]->setReverseChange(true);
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[i-1]));
+ }
+
+ // Create a server and start it listening. Note this relies on the fact
+ // that all of configured servers have the same address.
+ // and start it listening.
+ asiolink::IOAddress server_ip("127.0.0.1");
+ FauxServer server(*io_service_, server_ip, 5301);
+ server.receive(FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+ // Run sweep and IO until everything is done.
+ processAll();
+
+ for (int i = 0; i < test_count; i++) {
+ EXPECT_EQ(dhcp_ddns::ST_COMPLETED, canned_ncrs_[i]->getStatus());
+ }
+}
+
+/// @brief Tests processing of multiple transactions.
+/// This test verifies that update manager can create and manage a multiple
+/// transactions, concurrently. It uses a fake server that responds to all
+/// requests sent with NOERROR, simulating successful DNS updates. The
+/// transactions are a mix of both adds and removes.
+TEST_F(D2UpdateMgrTest, multiTransactionTimeout) {
+ // Queue up all the requests.
+ int test_count = canned_count_;
+ for (int i = test_count; i > 0; i--) {
+ canned_ncrs_[i-1]->setReverseChange(true);
+ ASSERT_NO_THROW(queue_mgr_->enqueue(canned_ncrs_[i-1]));
+ }
+
+ // No server is running, so everything will time out.
+
+ // Run sweep and IO until everything is done.
+ processAll();
+
+ for (int i = 0; i < test_count; i++) {
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, canned_ncrs_[i]->getStatus());
+ }
+}
+
+}
diff --git a/src/bin/d2/tests/d2_zone_unittests.cc b/src/bin/d2/tests/d2_zone_unittests.cc
new file mode 100644
index 0000000..853cdbe
--- /dev/null
+++ b/src/bin/d2/tests/d2_zone_unittests.cc
@@ -0,0 +1,75 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <d2/d2_zone.h>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+using namespace isc::dns;
+
+namespace {
+
+// This test verifies that Zone object is created and its constructor sets
+// appropriate values for its members.
+TEST(D2ZoneTest, constructor) {
+ // Create first object.
+ D2Zone zone1(Name("example.com"), RRClass::ANY());
+ EXPECT_EQ("example.com.", zone1.getName().toText());
+ EXPECT_EQ(RRClass::ANY().getCode(), zone1.getClass().getCode());
+ // Create another object to make sure that constructor doesn't assign
+ // fixed values, but they change when constructor's parameters change.
+ D2Zone zone2(Name("foo.example.com"), RRClass::IN());
+ EXPECT_EQ("foo.example.com.", zone2.getName().toText());
+ EXPECT_EQ(RRClass::IN().getCode(), zone2.getClass().getCode());
+}
+
+// This test verifies that toText() function returns text representation of
+// of the zone in expected format.
+TEST(D2ZoneTest, toText) {
+ // Create first object.
+ D2Zone zone1(Name("example.com"), RRClass::ANY());
+ EXPECT_EQ("example.com. ANY SOA\n", zone1.toText());
+ // Create another object with different parameters to make sure that the
+ // function's output changes accordingly.
+ D2Zone zone2(Name("foo.example.com"), RRClass::IN());
+ EXPECT_EQ("foo.example.com. IN SOA\n", zone2.toText());
+}
+
+// This test verifies that the equality and inequality operators behave as
+// expected.
+TEST(D2ZoneTest, compare) {
+ const Name a("a"), b("b");
+ const RRClass in(RRClass::IN()), any(RRClass::ANY());
+
+ // Equality check
+ EXPECT_TRUE(D2Zone(a, any) == D2Zone(a, any));
+ EXPECT_FALSE(D2Zone(a, any) != D2Zone(a, any));
+
+ // Inequality check, objects differ by class.
+ EXPECT_FALSE(D2Zone(a, any) == D2Zone(a, in));
+ EXPECT_TRUE(D2Zone(a, any) != D2Zone(a, in));
+
+ // Inequality check, objects differ by name.
+ EXPECT_FALSE(D2Zone(a, any) == D2Zone(b, any));
+ EXPECT_TRUE(D2Zone(a, any) != D2Zone(b, any));
+
+ // Inequality check, objects differ by name and class.
+ EXPECT_FALSE(D2Zone(a, any) == D2Zone(b, in));
+ EXPECT_TRUE(D2Zone(a, any) != D2Zone(b, in));
+}
+
+} // End of anonymous namespace
diff --git a/src/bin/d2/tests/d_cfg_mgr_unittests.cc b/src/bin/d2/tests/d_cfg_mgr_unittests.cc
new file mode 100644
index 0000000..512b896
--- /dev/null
+++ b/src/bin/d2/tests/d_cfg_mgr_unittests.cc
@@ -0,0 +1,386 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/ccsession.h>
+#include <config/module_spec.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <d2/d_cfg_mgr.h>
+#include <d_test_stubs.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <gtest/gtest.h>
+
+#include <config.h>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::config;
+using namespace isc::d2;
+using namespace boost::posix_time;
+
+namespace {
+
+/// @brief Test Class for verifying that configuration context cannot be null
+/// during construction.
+class DCtorTestCfgMgr : public DCfgMgrBase {
+public:
+ /// @brief Constructor - Note that is passes in an empty configuration
+ /// pointer to the base class constructor.
+ DCtorTestCfgMgr() : DCfgMgrBase(DCfgContextBasePtr()) {
+ }
+
+ /// @brief Destructor
+ virtual ~DCtorTestCfgMgr() {
+ }
+
+ /// @brief Dummy implementation as this method is abstract.
+ virtual isc::dhcp::ParserPtr
+ createConfigParser(const std::string& /* element_id */) {
+ return (isc::dhcp::ParserPtr());
+ }
+};
+
+/// @brief Test fixture class for testing DCfgMgrBase class.
+/// It maintains an member instance of DStubCfgMgr and derives from
+/// ConfigParseTest fixture, thus providing methods for converting JSON
+/// strings to configuration element sets, checking parse results, and
+/// accessing the configuration context.
+class DStubCfgMgrTest : public ConfigParseTest {
+public:
+
+ /// @brief Constructor
+ DStubCfgMgrTest():cfg_mgr_(new DStubCfgMgr) {
+ }
+
+ /// @brief Destructor
+ ~DStubCfgMgrTest() {
+ }
+
+ /// @brief Convenience method which returns a DStubContextPtr to the
+ /// configuration context.
+ ///
+ /// @return returns a DStubContextPtr.
+ DStubContextPtr getStubContext() {
+ return (boost::dynamic_pointer_cast<DStubContext>
+ (cfg_mgr_->getContext()));
+ }
+
+ /// @brief Configuration manager instance.
+ DStubCfgMgrPtr cfg_mgr_;
+};
+
+///@brief Tests basic construction/destruction of configuration manager.
+/// Verifies that:
+/// 1. Proper construction succeeds.
+/// 2. Configuration context is initialized by construction.
+/// 3. Destruction works properly.
+/// 4. Construction with a null context is not allowed.
+TEST(DCfgMgrBase, construction) {
+ DCfgMgrBasePtr cfg_mgr;
+
+ // Verify that configuration manager constructions without error.
+ ASSERT_NO_THROW(cfg_mgr.reset(new DStubCfgMgr()));
+
+ // Verify that the context can be retrieved and is not null.
+ DCfgContextBasePtr context = cfg_mgr->getContext();
+ EXPECT_TRUE(context);
+
+ // Verify that the manager can be destructed without error.
+ EXPECT_NO_THROW(cfg_mgr.reset());
+
+ // Verify that an attempt to construct a manger with a null context fails.
+ ASSERT_THROW(DCtorTestCfgMgr(), DCfgMgrBaseError);
+}
+
+///@brief Tests fundamental aspects of configuration parsing.
+/// Verifies that:
+/// 1. A correctly formed simple configuration parses without error.
+/// 2. An error building the element is handled.
+/// 3. An error committing the element is handled.
+/// 4. An unknown element error is handled.
+TEST_F(DStubCfgMgrTest, basicParseTest) {
+ // Create a simple configuration.
+ string config = "{ \"test-value\": 1000 } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that we can parse a simple configuration.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(0));
+
+ // Verify that an error building the element is caught and returns a
+ // failed parse result.
+ SimFailure::set(SimFailure::ftElementBuild);
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+
+ // Verify that an error committing the element is caught and returns a
+ // failed parse result.
+ SimFailure::set(SimFailure::ftElementCommit);
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+
+ // Verify that an unknown element error is caught and returns a failed
+ // parse result.
+ SimFailure::set(SimFailure::ftElementUnknown);
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+}
+
+///@brief Tests ordered and non-ordered element parsing
+/// This test verifies that:
+/// 1. Non-ordered parsing parses elements in the order they are presented
+/// by the configuration set (as-they-come).
+/// 2. A parse order list with too few elements is detected.
+/// 3. Ordered parsing parses the elements in the order specified by the
+/// configuration manager's parse order list.
+/// 4. A parse order list with too many elements is detected.
+TEST_F(DStubCfgMgrTest, parseOrderTest) {
+ // Element ids used for test.
+ std::string charlie("charlie");
+ std::string bravo("bravo");
+ std::string alpha("alpha");
+
+ // Create the test configuration with the elements in "random" order.
+
+ // NOTE that element sets produced by isc::data::Element::fromJSON(),
+ // are in lexical order by element_id. This means that iterating over
+ // such an element set, will present the elements in lexical order. Should
+ // this change, this test will need to be modified accordingly.
+ string config = "{ \"bravo\": 2, "
+ " \"alpha\": 1, "
+ " \"charlie\": 3 } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that non-ordered parsing, results in an as-they-come parse order.
+ // Create an expected parse order.
+ // (NOTE that iterating over Element sets produced by fromJSON() will
+ // present the elements in lexical order. Should this change, the expected
+ // order list below would need to be changed accordingly).
+ ElementIdList order_expected;
+ order_expected.push_back(alpha);
+ order_expected.push_back(bravo);
+ order_expected.push_back(charlie);
+
+ // Verify that the manager has an EMPTY parse order list. (Empty list
+ // instructs the manager to parse them as-they-come.)
+ EXPECT_EQ(0, cfg_mgr_->getParseOrder().size());
+
+ // Parse the configuration, verify it parses without error.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(0));
+
+ // Verify that the parsed order matches what we expected.
+ EXPECT_TRUE(cfg_mgr_->parsed_order_ == order_expected);
+
+ // Clear the manager's parse order "memory".
+ cfg_mgr_->parsed_order_.clear();
+
+ // Create a parse order list that has too few entries. Verify that
+ // when parsing the test config, it fails.
+ cfg_mgr_->addToParseOrder(charlie);
+ // Verify the parse order list is the size we expect.
+ EXPECT_EQ(1, cfg_mgr_->getParseOrder().size());
+
+ // Verify the configuration fails.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+
+ // Verify that the configuration parses correctly, when the parse order
+ // is correct. Add the needed entries to the parse order
+ cfg_mgr_->addToParseOrder(bravo);
+ cfg_mgr_->addToParseOrder(alpha);
+
+ // Verify the parse order list is the size we expect.
+ EXPECT_EQ(3, cfg_mgr_->getParseOrder().size());
+
+ // Clear the manager's parse order "memory".
+ cfg_mgr_->parsed_order_.clear();
+
+ // Verify the configuration parses without error.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(0));
+
+ // Verify that the parsed order is the order we configured.
+ EXPECT_TRUE(cfg_mgr_->getParseOrder() == cfg_mgr_->parsed_order_);
+
+ // Create a parse order list that has too many entries. Verify that
+ // when parsing the test config, it fails.
+ cfg_mgr_->addToParseOrder("delta");
+
+ // Verify the parse order list is the size we expect.
+ EXPECT_EQ(4, cfg_mgr_->getParseOrder().size());
+
+ // Verify the configuration fails.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+}
+
+/// @brief Tests that element ids supported by the base class as well as those
+/// added by the derived class function properly.
+/// This test verifies that:
+/// 1. Boolean parameters can be parsed and retrieved.
+/// 2. Uint32 parameters can be parsed and retrieved.
+/// 3. String parameters can be parsed and retrieved.
+/// 4. Derivation-specific parameters can be parsed and retrieved.
+/// 5. Parsing a second configuration, updates the existing context values
+/// correctly.
+TEST_F(DStubCfgMgrTest, simpleTypesTest) {
+ // Fetch a derivation specific pointer to the context.
+ DStubContextPtr context = getStubContext();
+ ASSERT_TRUE(context);
+
+ // Create a configuration with all of the parameters.
+ string config = "{ \"bool_test\": true , "
+ " \"uint32_test\": 77 , "
+ " \"string_test\": \"hmmm chewy\" , "
+ " \"extra_test\": 430 } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the configuration parses without error.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ ASSERT_TRUE(checkAnswer(0));
+
+ // Verify that the boolean parameter was parsed correctly by retrieving
+ // its value from the context.
+ bool actual_bool = false;
+ EXPECT_NO_THROW(context->getParam("bool_test", actual_bool));
+ EXPECT_EQ(true, actual_bool);
+
+ // Verify that the uint32 parameter was parsed correctly by retrieving
+ // its value from the context.
+ uint32_t actual_uint32 = 0;
+ EXPECT_NO_THROW(context->getParam("uint32_test", actual_uint32));
+ EXPECT_EQ(77, actual_uint32);
+
+ // Verify that the string parameter was parsed correctly by retrieving
+ // its value from the context.
+ std::string actual_string = "";
+ EXPECT_NO_THROW(context->getParam("string_test", actual_string));
+ EXPECT_EQ("hmmm chewy", actual_string);
+
+ // Verify that the "extra" parameter was parsed correctly by retrieving
+ // its value from the context.
+ uint32_t actual_extra = 0;
+ EXPECT_NO_THROW(context->getExtraParam("extra_test", actual_extra));
+ EXPECT_EQ(430, actual_extra);
+
+ // Create a configuration which "updates" all of the parameter values.
+ string config2 = "{ \"bool_test\": false , "
+ " \"uint32_test\": 88 , "
+ " \"string_test\": \"ewww yuk!\" , "
+ " \"extra_test\": 11 } ";
+ ASSERT_TRUE(fromJSON(config2));
+
+ // Verify that the configuration parses without error.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(0));
+
+ // Verify that the boolean parameter was updated correctly by retrieving
+ // its value from the context.
+ actual_bool = true;
+ EXPECT_NO_THROW(context->getParam("bool_test", actual_bool));
+ EXPECT_FALSE(actual_bool);
+
+ // Verify that the uint32 parameter was updated correctly by retrieving
+ // its value from the context.
+ actual_uint32 = 0;
+ EXPECT_NO_THROW(context->getParam("uint32_test", actual_uint32));
+ EXPECT_EQ(88, actual_uint32);
+
+ // Verify that the string parameter was updated correctly by retrieving
+ // its value from the context.
+ actual_string = "";
+ EXPECT_NO_THROW(context->getParam("string_test", actual_string));
+ EXPECT_EQ("ewww yuk!", actual_string);
+
+ // Verify that the "extra" parameter was updated correctly by retrieving
+ // its value from the context.
+ actual_extra = 0;
+ EXPECT_NO_THROW(context->getExtraParam("extra_test", actual_extra));
+ EXPECT_EQ(11, actual_extra);
+}
+
+/// @brief Tests that the configuration context is preserved after failure
+/// during parsing causes a rollback.
+/// 1. Verifies configuration context rollback.
+TEST_F(DStubCfgMgrTest, rollBackTest) {
+ // Fetch a derivation specific pointer to the context.
+ DStubContextPtr context = getStubContext();
+ ASSERT_TRUE(context);
+
+ // Create a configuration with all of the parameters.
+ string config = "{ \"bool_test\": true , "
+ " \"uint32_test\": 77 , "
+ " \"string_test\": \"hmmm chewy\" , "
+ " \"extra_test\": 430 } ";
+ ASSERT_TRUE(fromJSON(config));
+
+ // Verify that the configuration parses without error.
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(0));
+
+ // Verify that all of parameters have the expected values.
+ bool actual_bool = false;
+ EXPECT_NO_THROW(context->getParam("bool_test", actual_bool));
+ EXPECT_EQ(true, actual_bool);
+
+ uint32_t actual_uint32 = 0;
+ EXPECT_NO_THROW(context->getParam("uint32_test", actual_uint32));
+ EXPECT_EQ(77, actual_uint32);
+
+ std::string actual_string = "";
+ EXPECT_NO_THROW(context->getParam("string_test", actual_string));
+ EXPECT_EQ("hmmm chewy", actual_string);
+
+ uint32_t actual_extra = 0;
+ EXPECT_NO_THROW(context->getExtraParam("extra_test", actual_extra));
+ EXPECT_EQ(430, actual_extra);
+
+ // Create a configuration which "updates" all of the parameter values
+ // plus one unknown at the end.
+ string config2 = "{ \"bool_test\": false , "
+ " \"uint32_test\": 88 , "
+ " \"string_test\": \"ewww yuk!\" , "
+ " \"extra_test\": 11 , "
+ " \"zeta_unknown\": 33 } ";
+ ASSERT_TRUE(fromJSON(config2));
+
+ // Force a failure on the last element
+ SimFailure::set(SimFailure::ftElementUnknown);
+ answer_ = cfg_mgr_->parseConfig(config_set_);
+ EXPECT_TRUE(checkAnswer(1));
+
+ // Refresh our local pointer.
+ context = getStubContext();
+
+ // Verify that all of parameters have the original values.
+ actual_bool = false;
+ EXPECT_NO_THROW(context->getParam("bool_test", actual_bool));
+ EXPECT_EQ(true, actual_bool);
+
+ actual_uint32 = 0;
+ EXPECT_NO_THROW(context->getParam("uint32_test", actual_uint32));
+ EXPECT_EQ(77, actual_uint32);
+
+ actual_string = "";
+ EXPECT_NO_THROW(context->getParam("string_test", actual_string));
+ EXPECT_EQ("hmmm chewy", actual_string);
+
+ actual_extra = 0;
+ EXPECT_NO_THROW(context->getExtraParam("extra_test", actual_extra));
+ EXPECT_EQ(430, actual_extra);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/d2/tests/d_controller_unittests.cc b/src/bin/d2/tests/d_controller_unittests.cc
new file mode 100644
index 0000000..26b0e0e
--- /dev/null
+++ b/src/bin/d2/tests/d_controller_unittests.cc
@@ -0,0 +1,364 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config/ccsession.h>
+#include <d_test_stubs.h>
+#include <d2/spec_config.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <gtest/gtest.h>
+
+#include <config.h>
+#include <sstream>
+
+using namespace boost::posix_time;
+
+namespace isc {
+namespace d2 {
+
+/// @brief Test fixture class for testing DControllerBase class. This class
+/// derives from DControllerTest and wraps a DStubController. DStubController
+/// has been constructed to exercise DControllerBase.
+class DStubControllerTest : public DControllerTest {
+public:
+
+ /// @brief Constructor.
+ /// Note the constructor passes in the static DStubController instance
+ /// method.
+ DStubControllerTest() : DControllerTest (DStubController::instance) {
+ }
+
+ virtual ~DStubControllerTest() {
+ }
+};
+
+/// @brief Basic Controller instantiation testing.
+/// Verfies that the controller singleton gets created and that the
+/// basic derivation from the base class is intact.
+TEST_F(DStubControllerTest, basicInstanceTesting) {
+ // Verify that the singleton exists and it is the correct type.
+ DControllerBasePtr& controller = DControllerTest::getController();
+ ASSERT_TRUE(controller);
+ ASSERT_NO_THROW(boost::dynamic_pointer_cast<DStubController>(controller));
+
+ // Verify that controller's app name is correct.
+ EXPECT_TRUE(checkAppName(DStubController::stub_app_name_));
+
+ // Verify that controller's bin name is correct.
+ EXPECT_TRUE(checkBinName(DStubController::stub_bin_name_));
+
+ // Verify that controller's spec file name is correct.
+ EXPECT_TRUE(checkSpecFileName(D2_SPECFILE_LOCATION));
+
+ // Verify that controller's IOService exists.
+ EXPECT_TRUE(checkIOService());
+
+ // Verify that the Process does NOT exist.
+ EXPECT_FALSE(checkProcess());
+}
+
+/// @brief Tests basic command line processing.
+/// Verifies that:
+/// 1. Standard command line options are supported.
+/// 2. Custom command line options are supported.
+/// 3. Invalid options are detected.
+/// 4. Extraneous command line information is detected.
+TEST_F(DStubControllerTest, commandLineArgs) {
+
+ // Verify that both flags are false initially.
+ EXPECT_TRUE(checkStandAlone(false));
+ EXPECT_TRUE(checkVerbose(false));
+
+ // Verify that standard options can be parsed without error.
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+ EXPECT_NO_THROW(parseArgs(argc, argv));
+
+ // Verify that flags are now true.
+ EXPECT_TRUE(checkStandAlone(true));
+ EXPECT_TRUE(checkVerbose(true));
+
+ // Verify that the custom command line option is parsed without error.
+ char xopt[3] = "- ";
+ xopt[1] = *DStubController::stub_option_x_;
+ char* argv1[] = { const_cast<char*>("progName"), xopt};
+ argc = 2;
+ EXPECT_NO_THROW (parseArgs(argc, argv1));
+
+ // Verify that an unknown option is detected.
+ char* argv2[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-bs") };
+ argc = 2;
+ EXPECT_THROW (parseArgs(argc, argv2), InvalidUsage);
+
+ // Verify that extraneous information is detected.
+ char* argv3[] = { const_cast<char*>("progName"),
+ const_cast<char*>("extra"),
+ const_cast<char*>("information") };
+ argc = 3;
+ EXPECT_THROW (parseArgs(argc, argv3), InvalidUsage);
+}
+
+/// @brief Tests application process creation and initialization.
+/// Verifies that:
+/// 1. An error during process creation is handled.
+/// 2. A NULL returned by process creation is handled.
+/// 3. An error during process initialization is handled.
+/// 4. Process can be successfully created and initialized.
+TEST_F(DStubControllerTest, initProcessTesting) {
+ // Verify that a failure during process creation is caught.
+ SimFailure::set(SimFailure::ftCreateProcessException);
+ EXPECT_THROW(initProcess(), DControllerBaseError);
+ EXPECT_FALSE(checkProcess());
+
+ // Verify that a NULL returned by process creation is handled.
+ SimFailure::set(SimFailure::ftCreateProcessNull);
+ EXPECT_THROW(initProcess(), DControllerBaseError);
+ EXPECT_FALSE(checkProcess());
+
+ // Re-create controller, verify that we are starting clean
+ resetController();
+ EXPECT_FALSE(checkProcess());
+
+ // Verify that an error during process initialization is handled.
+ SimFailure::set(SimFailure::ftProcessInit);
+ EXPECT_THROW(initProcess(), DProcessBaseError);
+
+ // Re-create controller, verify that we are starting clean
+ resetController();
+ EXPECT_FALSE(checkProcess());
+
+ // Verify that the application process can created and initialized.
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+}
+
+/// @brief Tests launch handling of invalid command line.
+/// This test launches with an invalid command line which should throw
+/// an InvalidUsage.
+TEST_F(DStubControllerTest, launchInvalidUsage) {
+ // Command line to run integrated
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-z") };
+ int argc = 2;
+
+ // Launch the controller in integrated mode.
+ EXPECT_THROW(launch(argc, argv), InvalidUsage);
+}
+
+/// @brief Tests launch handling of failure in application process
+/// initialization. This test launches with a valid command line but with
+/// SimFailure set to fail during process creation. Launch should throw
+/// ProcessInitError.
+TEST_F(DStubControllerTest, launchProcessInitError) {
+ // Command line to run integrated
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+
+ // Launch the controller in stand alone mode.
+ SimFailure::set(SimFailure::ftCreateProcessException);
+ EXPECT_THROW(launch(argc, argv), ProcessInitError);
+}
+
+/// @brief Tests launch and normal shutdown (stand alone mode).
+/// This creates an interval timer to generate a normal shutdown and then
+/// launches with a valid, stand-alone command line and no simulated errors.
+TEST_F(DStubControllerTest, launchNormalShutdown) {
+ // command line to run standalone
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+
+ // Use an asiolink IntervalTimer and callback to generate the
+ // shutdown invocation. (Note IntervalTimer setup is in milliseconds).
+ isc::asiolink::IntervalTimer timer(*getIOService());
+ timer.setup(genShutdownCallback, 2 * 1000);
+
+ // Record start time, and invoke launch().
+ ptime start = microsec_clock::universal_time();
+ EXPECT_NO_THROW(launch(argc, argv));
+
+ // Record stop time.
+ ptime stop = microsec_clock::universal_time();
+
+ // Verify that duration of the run invocation is the same as the
+ // timer duration. This demonstrates that the shutdown was driven
+ // by an io_service event and callback.
+ time_duration elapsed = stop - start;
+ EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
+ elapsed.total_milliseconds() <= 2100);
+}
+
+/// @brief Tests launch with an operational error during application execution.
+/// This test creates an interval timer to generate a runtime exception during
+/// the process event loop. It launches wih a valid, stand-alone command line
+/// and no simulated errors. Launch should throw ProcessRunError.
+TEST_F(DStubControllerTest, launchRuntimeError) {
+ // command line to run standalone
+ char* argv[] = { const_cast<char*>("progName"),
+ const_cast<char*>("-s"),
+ const_cast<char*>("-v") };
+ int argc = 3;
+
+ // Use an asiolink IntervalTimer and callback to generate the
+ // shutdown invocation. (Note IntervalTimer setup is in milliseconds).
+ isc::asiolink::IntervalTimer timer(*getIOService());
+ timer.setup(genFatalErrorCallback, 2 * 1000);
+
+ // Record start time, and invoke launch().
+ ptime start = microsec_clock::universal_time();
+ EXPECT_THROW(launch(argc, argv), ProcessRunError);
+
+ // Record stop time.
+ ptime stop = microsec_clock::universal_time();
+
+ // Verify that duration of the run invocation is the same as the
+ // timer duration. This demonstrates that the shutdown was driven
+ // by an io_service event and callback.
+ time_duration elapsed = stop - start;
+ EXPECT_TRUE(elapsed.total_milliseconds() >= 1900 &&
+ elapsed.total_milliseconds() <= 2100);
+}
+
+/// @brief Tests launch with a session establishment failure.
+/// This test launches with a valid command line for integrated mode and no.
+/// Attempting to connect to BIND10 should fail, even if BIND10 is running
+/// UNLESS the test is run as root. Launch should throw SessionStartError.
+TEST_F(DStubControllerTest, launchSessionFailure) {
+ // Command line to run integrated
+ char* argv[] = { (char*)"progName" };
+ int argc = 1;
+
+ // Launch the controller in integrated mode.
+ EXPECT_THROW(launch(argc, argv), SessionStartError);
+}
+
+/// @brief Configuration update event testing.
+/// This really tests just the ability of the handlers to invoke the necessary
+/// chain of methods and handle error conditions. Configuration parsing and
+/// retrieval should be tested as part of the d2 configuration management
+/// implementation. Note that this testing calls the configuration update event
+/// callback, configHandler, directly.
+/// This test verifies that:
+/// 1. Configuration will be rejected in integrated mode when there is no
+/// session established. (This is a very contrived situation).
+/// 2. In stand-alone mode a configuration update results in successful
+/// status return.
+/// 3. That an application process error in configuration updating is handled
+/// properly.
+TEST_F(DStubControllerTest, configUpdateTests) {
+ int rcode = -1;
+ isc::data::ConstElementPtr answer;
+
+ // Initialize the application process.
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+
+ // Create a configuration set. Content is arbitrary, just needs to be
+ // valid JSON.
+ std::string config = "{ \"test-value\": 1000 } ";
+ isc::data::ElementPtr config_set = isc::data::Element::fromJSON(config);
+
+ // We are not stand-alone, so configuration should be rejected as there is
+ // no session. This is a pretty contrived situation that shouldn't be
+ // possible other than the handler being called directly (like this does).
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(1, rcode);
+
+ // Verify that in stand alone we get a successful update result.
+ setStandAlone(true);
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(0, rcode);
+
+ // Verify that an error in process configure method is handled.
+ SimFailure::set(SimFailure::ftProcessConfigure);
+ answer = DControllerBase::configHandler(config_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(1, rcode);
+}
+
+/// @brief Command execution tests.
+/// This really tests just the ability of the handler to invoke the necessary
+/// chain of methods and to handle error conditions. Note that this testing
+/// calls the command callback, commandHandler, directly.
+/// This test verifies that:
+/// 1. That an unrecognized command is detected and returns a status of
+/// d2::COMMAND_INVALID.
+/// 2. Shutdown command is recognized and returns a d2::COMMAND_SUCCESS status.
+/// 3. A valid, custom controller command is recognized a d2::COMMAND_SUCCESS
+/// status.
+/// 4. A valid, custom process command is recognized a d2::COMMAND_SUCCESS
+/// status.
+/// 5. That a valid controller command that fails returns a d2::COMMAND_ERROR.
+/// 6. That a valid process command that fails returns a d2::COMMAND_ERROR.
+TEST_F(DStubControllerTest, executeCommandTests) {
+ int rcode = -1;
+ isc::data::ConstElementPtr answer;
+ isc::data::ElementPtr arg_set;
+
+ // Initialize the application process.
+ ASSERT_NO_THROW(initProcess());
+ EXPECT_TRUE(checkProcess());
+
+ // Verify that an unknown command returns an d2::COMMAND_INVALID response.
+ std::string bogus_command("bogus");
+ answer = DControllerBase::commandHandler(bogus_command, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_INVALID, rcode);
+
+ // Verify that shutdown command returns d2::COMMAND_SUCCESS response.
+ answer = DControllerBase::commandHandler(SHUT_DOWN_COMMAND, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_SUCCESS, rcode);
+
+ // Verify that a valid custom controller command returns
+ // d2::COMMAND_SUCCESS response.
+ answer = DControllerBase::commandHandler(DStubController::
+ stub_ctl_command_, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_SUCCESS, rcode);
+
+ // Verify that a valid custom process command returns d2::COMMAND_SUCCESS
+ // response.
+ answer = DControllerBase::commandHandler(DStubProcess::
+ stub_proc_command_, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_SUCCESS, rcode);
+
+ // Verify that a valid custom controller command that fails returns
+ // a d2::COMMAND_ERROR.
+ SimFailure::set(SimFailure::ftControllerCommand);
+ answer = DControllerBase::commandHandler(DStubController::
+ stub_ctl_command_, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_ERROR, rcode);
+
+ // Verify that a valid custom process command that fails returns
+ // a d2::COMMAND_ERROR.
+ SimFailure::set(SimFailure::ftProcessCommand);
+ answer = DControllerBase::commandHandler(DStubProcess::
+ stub_proc_command_, arg_set);
+ isc::config::parseAnswer(rcode, answer);
+ EXPECT_EQ(COMMAND_ERROR, rcode);
+}
+
+}; // end of isc::d2 namespace
+}; // end of isc namespace
diff --git a/src/bin/d2/tests/d_test_stubs.cc b/src/bin/d2/tests/d_test_stubs.cc
new file mode 100644
index 0000000..319dcd7
--- /dev/null
+++ b/src/bin/d2/tests/d_test_stubs.cc
@@ -0,0 +1,319 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_log.h>
+#include <d2/spec_config.h>
+#include <d2/tests/d_test_stubs.h>
+
+using namespace asio;
+
+namespace isc {
+namespace d2 {
+
+const char* valid_d2_config = "{ "
+ "\"interface\" : \"eth1\" , "
+ "\"ip_address\" : \"127.0.0.1\" , "
+ "\"port\" : 5031, "
+ "\"tsig_keys\": ["
+ "{ \"name\": \"d2_key.tmark.org\" , "
+ " \"algorithm\": \"md5\" ,"
+ " \"secret\": \"0123456989\" "
+ "} ],"
+ "\"forward_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \"tmark.org.\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"hostname\": \"one.tmark\" } "
+ "] } ] }, "
+ "\"reverse_ddns\" : {"
+ "\"ddns_domains\": [ "
+ "{ \"name\": \" 0.168.192.in.addr.arpa.\" , "
+ " \"key_name\": \"d2_key.tmark.org\" , "
+ " \"dns_servers\" : [ "
+ " { \"ip_address\": \"127.0.0.101\" , "
+ " \"port\": 100 } ] } "
+ "] } }";
+
+// Initialize the static failure flag.
+SimFailure::FailureType SimFailure::failure_type_ = SimFailure::ftNoFailure;
+
+// Define custom process command supported by DStubProcess.
+const char* DStubProcess::stub_proc_command_("cool_proc_cmd");
+
+DStubProcess::DStubProcess(const char* name, IOServicePtr io_service)
+ : DProcessBase(name, io_service, DCfgMgrBasePtr(new DStubCfgMgr())) {
+};
+
+void
+DStubProcess::init() {
+ if (SimFailure::shouldFailOn(SimFailure::ftProcessInit)) {
+ // Simulates a failure to instantiate the process.
+ isc_throw(DProcessBaseError, "DStubProcess simulated init() failure");
+ }
+};
+
+void
+DStubProcess::run() {
+ // Until shut down or an fatal error occurs, wait for and
+ // execute a single callback. This is a preliminary implementation
+ // that is likely to evolve as development progresses.
+ // To use run(), the "managing" layer must issue an io_service::stop
+ // or the call to run will continue to block, and shutdown will not
+ // occur.
+ IOServicePtr& io_service = getIoService();
+ while (!shouldShutdown()) {
+ try {
+ io_service->run_one();
+ } catch (const std::exception& ex) {
+ isc_throw (DProcessBaseError,
+ std::string("Process run method failed:") + ex.what());
+ }
+ }
+};
+
+isc::data::ConstElementPtr
+DStubProcess::shutdown(isc::data::ConstElementPtr /* args */) {
+ if (SimFailure::shouldFailOn(SimFailure::ftProcessShutdown)) {
+ // Simulates a failure during shutdown process.
+ isc_throw(DProcessBaseError, "DStubProcess simulated shutdown failure");
+ }
+
+ setShutdownFlag(true);
+ stopIOService();
+ return (isc::config::createAnswer(0, "Shutdown inititiated."));
+}
+
+isc::data::ConstElementPtr
+DStubProcess::configure(isc::data::ConstElementPtr /*config_set*/) {
+ if (SimFailure::shouldFailOn(SimFailure::ftProcessConfigure)) {
+ // Simulates a process configure failure.
+ return (isc::config::createAnswer(1,
+ "Simulated process configuration error."));
+ }
+
+ return (isc::config::createAnswer(0, "Configuration accepted."));
+}
+
+isc::data::ConstElementPtr
+DStubProcess::command(const std::string& command,
+ isc::data::ConstElementPtr /* args */) {
+ isc::data::ConstElementPtr answer;
+ if (SimFailure::shouldFailOn(SimFailure::ftProcessCommand)) {
+ // Simulates a process command execution failure.
+ answer = isc::config::createAnswer(COMMAND_ERROR,
+ "SimFailure::ftProcessCommand");
+ } else if (command.compare(stub_proc_command_) == 0) {
+ answer = isc::config::createAnswer(COMMAND_SUCCESS, "Command accepted");
+ } else {
+ answer = isc::config::createAnswer(COMMAND_INVALID,
+ "Unrecognized command:" + command);
+ }
+
+ return (answer);
+}
+
+DStubProcess::~DStubProcess() {
+};
+
+//************************** DStubController *************************
+
+// Define custom controller command supported by DStubController.
+const char* DStubController::stub_ctl_command_("spiffy");
+
+// Define custom command line option command supported by DStubController.
+const char* DStubController::stub_option_x_ = "x";
+
+/// @brief Defines the app name used to construct the controller
+const char* DStubController::stub_app_name_ = "TestService";
+
+/// @brief Defines the bin name used to construct the controller
+const char* DStubController::stub_bin_name_ = "TestBin";
+
+DControllerBasePtr&
+DStubController::instance() {
+ // If the singleton hasn't been created, do it now.
+ if (!getController()) {
+ DControllerBasePtr p(new DStubController());
+ setController(p);
+ }
+
+ return (getController());
+}
+
+DStubController::DStubController()
+ : DControllerBase(stub_app_name_, stub_bin_name_) {
+
+ if (getenv("B10_FROM_BUILD")) {
+ setSpecFileName(std::string(getenv("B10_FROM_BUILD")) +
+ "/src/bin/d2/dhcp-ddns.spec");
+ } else {
+ setSpecFileName(D2_SPECFILE_LOCATION);
+ }
+}
+
+bool
+DStubController::customOption(int option, char* /* optarg */)
+{
+ // Check for the custom option supported by DStubController.
+ if (static_cast<char>(option) == *stub_option_x_) {
+ return (true);
+ }
+
+ return (false);
+}
+
+DProcessBase* DStubController::createProcess() {
+ if (SimFailure::shouldFailOn(SimFailure::ftCreateProcessException)) {
+ // Simulates a failure to instantiate the process due to exception.
+ throw std::runtime_error("SimFailure::ftCreateProcess");
+ }
+
+ if (SimFailure::shouldFailOn(SimFailure::ftCreateProcessNull)) {
+ // Simulates a failure to instantiate the process.
+ return (NULL);
+ }
+
+ // This should be a successful instantiation.
+ return (new DStubProcess(getAppName().c_str(), getIOService()));
+}
+
+isc::data::ConstElementPtr
+DStubController::customControllerCommand(const std::string& command,
+ isc::data::ConstElementPtr /* args */) {
+ isc::data::ConstElementPtr answer;
+ if (SimFailure::shouldFailOn(SimFailure::ftControllerCommand)) {
+ // Simulates command failing to execute.
+ answer = isc::config::createAnswer(COMMAND_ERROR,
+ "SimFailure::ftControllerCommand");
+ } else if (command.compare(stub_ctl_command_) == 0) {
+ answer = isc::config::createAnswer(COMMAND_SUCCESS, "Command accepted");
+ } else {
+ answer = isc::config::createAnswer(COMMAND_INVALID,
+ "Unrecognized command:" + command);
+ }
+
+ return (answer);
+}
+
+const std::string DStubController::getCustomOpts() const {
+ // Return the "list" of custom options supported by DStubController.
+ return (std::string(stub_option_x_));
+}
+
+DStubController::~DStubController() {
+}
+
+// Initialize controller wrapper's static instance getter member.
+DControllerTest::InstanceGetter DControllerTest::instanceGetter_ = NULL;
+
+//************************** TestParser *************************
+
+TestParser::TestParser(const std::string& param_name):param_name_(param_name) {
+}
+
+TestParser::~TestParser(){
+}
+
+void
+TestParser::build(isc::data::ConstElementPtr new_config) {
+ if (SimFailure::shouldFailOn(SimFailure::ftElementBuild)) {
+ // Simulates an error during element data parsing.
+ isc_throw (DCfgMgrBaseError, "Simulated build exception");
+ }
+
+ value_ = new_config;
+}
+
+void
+TestParser::commit() {
+ if (SimFailure::shouldFailOn(SimFailure::ftElementCommit)) {
+ // Simulates an error while committing the parsed element data.
+ throw std::runtime_error("Simulated commit exception");
+ }
+}
+
+//************************** DStubContext *************************
+
+DStubContext::DStubContext(): extra_values_(new isc::dhcp::Uint32Storage()) {
+}
+
+DStubContext::~DStubContext() {
+}
+
+void
+DStubContext::getExtraParam(const std::string& name, uint32_t& value) {
+ value = extra_values_->getParam(name);
+}
+
+isc::dhcp::Uint32StoragePtr
+DStubContext::getExtraStorage() {
+ return (extra_values_);
+}
+
+DCfgContextBasePtr
+DStubContext::clone() {
+ return (DCfgContextBasePtr(new DStubContext(*this)));
+}
+
+DStubContext::DStubContext(const DStubContext& rhs): DCfgContextBase(rhs),
+ extra_values_(new isc::dhcp::Uint32Storage(*(rhs.extra_values_))) {
+}
+
+//************************** DStubCfgMgr *************************
+
+DStubCfgMgr::DStubCfgMgr()
+ : DCfgMgrBase(DCfgContextBasePtr(new DStubContext())) {
+}
+
+DStubCfgMgr::~DStubCfgMgr() {
+}
+
+isc::dhcp::ParserPtr
+DStubCfgMgr::createConfigParser(const std::string& element_id) {
+ isc::dhcp::DhcpConfigParser* parser = NULL;
+ DStubContextPtr context =
+ boost::dynamic_pointer_cast<DStubContext>(getContext());
+
+ if (element_id == "bool_test") {
+ parser = new isc::dhcp::BooleanParser(element_id,
+ context->getBooleanStorage());
+ } else if (element_id == "uint32_test") {
+ parser = new isc::dhcp::Uint32Parser(element_id,
+ context->getUint32Storage());
+ } else if (element_id == "string_test") {
+ parser = new isc::dhcp::StringParser(element_id,
+ context->getStringStorage());
+ } else if (element_id == "extra_test") {
+ parser = new isc::dhcp::Uint32Parser(element_id,
+ context->getExtraStorage());
+ } else {
+ // Fail only if SimFailure dictates we should. This makes it easier
+ // to test parse ordering, by permitting a wide range of element ids
+ // to "succeed" without specifically supporting them.
+ if (SimFailure::shouldFailOn(SimFailure::ftElementUnknown)) {
+ isc_throw(DCfgMgrBaseError, "Configuration parameter not supported: "
+ << element_id);
+ }
+
+ parsed_order_.push_back(element_id);
+ parser = new TestParser(element_id);
+ }
+
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+
+}; // namespace isc::d2
+}; // namespace isc
diff --git a/src/bin/d2/tests/d_test_stubs.h b/src/bin/d2/tests/d_test_stubs.h
new file mode 100644
index 0000000..7680a98
--- /dev/null
+++ b/src/bin/d2/tests/d_test_stubs.h
@@ -0,0 +1,667 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D_TEST_STUBS_H
+#define D_TEST_STUBS_H
+
+#include <cc/data.h>
+#include <cc/session.h>
+#include <config/ccsession.h>
+
+#include <d2/d2_asio.h>
+#include <d2/d_controller.h>
+#include <d2/d_cfg_mgr.h>
+
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace d2 {
+
+/// @brief Provides a valid DHCP-DDNS configuration for testing basic
+/// parsing fundamentals.
+extern const char* valid_d2_config;
+
+
+/// @brief Class is used to set a globally accessible value that indicates
+/// a specific type of failure to simulate. Test derivations of base classes
+/// can exercise error handling code paths by testing for specific SimFailure
+/// values at the appropriate places and then causing the error to "occur".
+/// The class consists of an enumerated set of failures, and static methods
+/// for getting, setting, and testing the current value.
+class SimFailure {
+public:
+ enum FailureType {
+ ftUnknown = -1,
+ ftNoFailure = 0,
+ ftCreateProcessException,
+ ftCreateProcessNull,
+ ftProcessInit,
+ ftProcessConfigure,
+ ftControllerCommand,
+ ftProcessCommand,
+ ftProcessShutdown,
+ ftElementBuild,
+ ftElementCommit,
+ ftElementUnknown
+ };
+
+ /// @brief Sets the SimFailure value to the given value.
+ ///
+ /// @param value is the new value to assign to the global value.
+ static void set(enum FailureType value) {
+ failure_type_ = value;
+ }
+
+ /// @brief Gets the current global SimFailure value
+ ///
+ /// @return returns the current SimFailure value
+ static enum FailureType get() {
+ return (failure_type_);
+ }
+
+ /// @brief One-shot test of the SimFailure value. If the global
+ /// SimFailure value is equal to the given value, clear the global
+ /// value and return true. This makes it convenient for code to
+ /// test and react without having to explicitly clear the global
+ /// value.
+ ///
+ /// @param value is the value against which the global value is
+ /// to be compared.
+ ///
+ /// @return returns true if current SimFailure value matches the
+ /// given value.
+ static bool shouldFailOn(enum FailureType value) {
+ if (failure_type_ == value) {
+ clear();
+ return (true);
+ }
+
+ return (false);
+ }
+
+ /// @brief Resets the failure type to none.
+ static void clear() {
+ failure_type_ = ftNoFailure;
+ }
+
+ /// @brief Static value for holding the failure type to simulate.
+ static enum FailureType failure_type_;
+};
+
+/// @brief Test Derivation of the DProcessBase class.
+///
+/// This class is used primarily to server as a test process class for testing
+/// DControllerBase. It provides minimal, but sufficient implementation to
+/// test the majority of DControllerBase functionality.
+class DStubProcess : public DProcessBase {
+public:
+
+ /// @brief Static constant that defines a custom process command string.
+ static const char* stub_proc_command_;
+
+ /// @brief Constructor
+ ///
+ /// @param name name is a text label for the process. Generally used
+ /// in log statements, but otherwise arbitrary.
+ /// @param io_service is the io_service used by the caller for
+ /// asynchronous event handling.
+ ///
+ /// @throw DProcessBaseError is io_service is NULL.
+ DStubProcess(const char* name, IOServicePtr io_service);
+
+ /// @brief Invoked after process instantiation to perform initialization.
+ /// This implementation supports simulating an error initializing the
+ /// process by throwing a DProcessBaseError if SimFailure is set to
+ /// ftProcessInit.
+ virtual void init();
+
+ /// @brief Implements the process's event loop.
+ /// This implementation is quite basic, surrounding calls to
+ /// io_service->runOne() with a test of the shutdown flag. Once invoked,
+ /// the method will continue until the process itself is exiting due to a
+ /// request to shutdown or some anomaly forces an exit.
+ /// @return returns 0 upon a successful, "normal" termination, non-zero to
+ /// indicate an abnormal termination.
+ virtual void run();
+
+ /// @brief Implements the process shutdown procedure.
+ ///
+ /// This sets the instance shutdown flag monitored by run() and stops
+ /// the IO service.
+ virtual isc::data::ConstElementPtr shutdown(isc::data::ConstElementPtr);
+
+ /// @brief Processes the given configuration.
+ ///
+ /// This implementation fails if SimFailure is set to ftProcessConfigure.
+ /// Otherwise it will complete successfully. It does not check the content
+ /// of the inbound configuration.
+ ///
+ /// @param config_set a new configuration (JSON) for the process
+ /// @return an Element that contains the results of configuration composed
+ /// of an integer status value (0 means successful, non-zero means failure),
+ /// and a string explanation of the outcome.
+ virtual isc::data::ConstElementPtr configure(isc::data::ConstElementPtr
+ config_set);
+
+ /// @brief Executes the given command.
+ ///
+ /// This implementation will recognizes one "custom" process command,
+ /// stub_proc_command_. It will fail if SimFailure is set to
+ /// ftProcessCommand.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command.
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value and a string explanation of the outcome.
+ /// The status value is:
+ /// COMMAND_SUCCESS if the command is recognized and executes successfully.
+ /// COMMAND_ERROR if the command is recognized but fails to execute.
+ /// COMMAND_INVALID if the command is not recognized.
+ virtual isc::data::ConstElementPtr command(const std::string& command,
+ isc::data::ConstElementPtr args);
+
+ // @brief Destructor
+ virtual ~DStubProcess();
+};
+
+
+/// @brief Test Derivation of the DControllerBase class.
+///
+/// DControllerBase is an abstract class and therefore requires a derivation
+/// for testing. It allows testing the majority of the base class code
+/// without polluting production derivations (e.g. D2Process). It uses
+/// DStubProcess as its application process class. It is a full enough
+/// implementation to support running both stand alone and integrated.
+/// Obviously BIND10 connectivity is not available under unit tests, so
+/// testing here is limited to "failures" to communicate with BIND10.
+class DStubController : public DControllerBase {
+public:
+ /// @brief Static singleton instance method. This method returns the
+ /// base class singleton instance member. It instantiates the singleton
+ /// and sets the base class instance member upon first invocation.
+ ///
+ /// @return returns a pointer reference to the singleton instance.
+ static DControllerBasePtr& instance();
+
+ /// @brief Defines a custom controller command string. This is a
+ /// custom command supported by DStubController.
+ static const char* stub_ctl_command_;
+
+ /// @brief Defines a custom command line option supported by
+ /// DStubController.
+ static const char* stub_option_x_;
+
+ /// @brief Defines the app name used to construct the controller
+ static const char* stub_app_name_;
+
+ /// @brief Defines the executable name used to construct the controller
+ static const char* stub_bin_name_;
+
+protected:
+ /// @brief Handles additional command line options that are supported
+ /// by DStubController. This implementation supports an option "-x".
+ ///
+ /// @param option is the option "character" from the command line, without
+ /// any prefixing hyphen(s)
+ /// @optarg optarg is the argument value (if one) associated with the option
+ ///
+ /// @return returns true if the option is "x", otherwise ti returns false.
+ virtual bool customOption(int option, char *optarg);
+
+ /// @brief Instantiates an instance of DStubProcess.
+ ///
+ /// This implementation will fail if SimFailure is set to
+ /// ftCreateProcessException OR ftCreateProcessNull.
+ ///
+ /// @return returns a pointer to the new process instance (DProcessBase*)
+ /// or NULL if SimFailure is set to ftCreateProcessNull.
+ /// @throw throws std::runtime_error if SimFailure is set to
+ /// ftCreateProcessException.
+ virtual DProcessBase* createProcess();
+
+ /// @brief Executes custom controller commands are supported by
+ /// DStubController. This implementation supports one custom controller
+ /// command, stub_ctl_command_. It will fail if SimFailure is set
+ /// to ftControllerCommand.
+ ///
+ /// @param command is a string label representing the command to execute.
+ /// @param args is a set of arguments (if any) required for the given
+ /// command.
+ /// @return an Element that contains the results of command composed
+ /// of an integer status value and a string explanation of the outcome.
+ /// The status value is:
+ /// COMMAND_SUCCESS if the command is recognized and executes successfully.
+ /// COMMAND_ERROR if the command is recognized but fails to execute.
+ /// COMMAND_INVALID if the command is not recognized.
+ virtual isc::data::ConstElementPtr customControllerCommand(
+ const std::string& command, isc::data::ConstElementPtr args);
+
+ /// @brief Provides a string of the additional command line options
+ /// supported by DStubController. DStubController supports one
+ /// addition option, stub_option_x_.
+ ///
+ /// @return returns a string containing the option letters.
+ virtual const std::string getCustomOpts() const;
+
+private:
+ /// @brief Constructor is private to protect singleton integrity.
+ DStubController();
+
+public:
+ virtual ~DStubController();
+};
+
+/// @brief Abstract Test fixture class that wraps a DControllerBase. This class
+/// is a friend class of DControllerBase which allows it access to class
+/// content to facilitate testing. It provides numerous wrapper methods for
+/// the protected and private methods and member of the base class.
+class DControllerTest : public ::testing::Test {
+public:
+
+ /// @brief Defines a function pointer for controller singleton fetchers.
+ typedef DControllerBasePtr& (*InstanceGetter)();
+
+ /// @brief Static storage of the controller class's singleton fetcher.
+ /// We need this this statically available for callbacks.
+ static InstanceGetter instanceGetter_;
+
+ /// @brief Constructor
+ ///
+ /// @param instance_getter is a function pointer to the static instance
+ /// method of the DControllerBase derivation under test.
+ DControllerTest(InstanceGetter instance_getter) {
+ // Set the static fetcher member, then invoke it via getController.
+ // This ensures the singleton is instantiated.
+ instanceGetter_ = instance_getter;
+ getController();
+ }
+
+ /// @brief Destructor
+ /// Note the controller singleton is destroyed. This is essential to ensure
+ /// a clean start between tests.
+ virtual ~DControllerTest() {
+ getController().reset();
+ }
+
+ /// @brief Convenience method that destructs and then recreates the
+ /// controller singleton under test. This is handy for tests within
+ /// tests.
+ void resetController() {
+ getController().reset();
+ getController();
+ }
+
+ /// @brief Static method which returns the instance of the controller
+ /// under test.
+ /// @return returns a reference to the controller instance.
+ static DControllerBasePtr& getController() {
+ return ((*instanceGetter_)());
+ }
+
+ /// @brief Returns true if the Controller's app name matches the
+ /// given value.
+ ///
+ /// @param should_be is the value to compare against.
+ ///
+ /// @return returns true if the values are equal.
+ bool checkAppName(const std::string& should_be) {
+ return (getController()->getAppName().compare(should_be) == 0);
+ }
+
+ /// @brief Returns true if the Controller's service name matches the
+ /// given value.
+ ///
+ /// @param should_be is the value to compare against.
+ ///
+ /// @return returns true if the values are equal.
+ bool checkBinName(const std::string& should_be) {
+ return (getController()->getBinName().compare(should_be) == 0);
+ }
+
+ /// @brief Returns true if the Controller's spec file name matches the
+ /// given value.
+ ///
+ /// @param should_be is the value to compare against.
+ ///
+ /// @return returns true if the values are equal.
+ bool checkSpecFileName(const std::string& should_be) {
+ return (getController()->getSpecFileName().compare(should_be) == 0);
+ }
+
+ /// @brief Tests the existence of the Controller's application process.
+ ///
+ /// @return returns true if the process instance exists.
+ bool checkProcess() {
+ return (getController()->process_);
+ }
+
+ /// @brief Tests the existence of the Controller's IOService.
+ ///
+ /// @return returns true if the IOService exists.
+ bool checkIOService() {
+ return (getController()->io_service_);
+ }
+
+ /// @brief Gets the Controller's IOService.
+ ///
+ /// @return returns a reference to the IOService
+ IOServicePtr& getIOService() {
+ return (getController()->io_service_);
+ }
+
+ /// @brief Compares stand alone flag with the given value.
+ ///
+ /// @param value
+ ///
+ /// @return returns true if the stand alone flag is equal to the given
+ /// value.
+ bool checkStandAlone(bool value) {
+ return (getController()->isStandAlone() == value);
+ }
+
+ /// @brief Sets the controller's stand alone flag to the given value.
+ ///
+ /// @param value is the new value to assign.
+ ///
+ void setStandAlone(bool value) {
+ getController()->setStandAlone(value);
+ }
+
+ /// @brief Compares verbose flag with the given value.
+ ///
+ /// @param value
+ ///
+ /// @return returns true if the verbose flag is equal to the given value.
+ bool checkVerbose(bool value) {
+ return (getController()->isVerbose() == value);
+ }
+
+ /// @Wrapper to invoke the Controller's parseArgs method. Please refer to
+ /// DControllerBase::parseArgs for details.
+ void parseArgs(int argc, char* argv[]) {
+ getController()->parseArgs(argc, argv);
+ }
+
+ /// @Wrapper to invoke the Controller's init method. Please refer to
+ /// DControllerBase::init for details.
+ void initProcess() {
+ getController()->initProcess();
+ }
+
+ /// @Wrapper to invoke the Controller's establishSession method. Please
+ /// refer to DControllerBase::establishSession for details.
+ void establishSession() {
+ getController()->establishSession();
+ }
+
+ /// @Wrapper to invoke the Controller's launch method. Please refer to
+ /// DControllerBase::launch for details.
+ void launch(int argc, char* argv[]) {
+ optind = 1;
+ getController()->launch(argc, argv, true);
+ }
+
+ /// @Wrapper to invoke the Controller's disconnectSession method. Please
+ /// refer to DControllerBase::disconnectSession for details.
+ void disconnectSession() {
+ getController()->disconnectSession();
+ }
+
+ /// @Wrapper to invoke the Controller's updateConfig method. Please
+ /// refer to DControllerBase::updateConfig for details.
+ isc::data::ConstElementPtr updateConfig(isc::data::ConstElementPtr
+ new_config) {
+ return (getController()->updateConfig(new_config));
+ }
+
+ /// @Wrapper to invoke the Controller's executeCommand method. Please
+ /// refer to DControllerBase::executeCommand for details.
+ isc::data::ConstElementPtr executeCommand(const std::string& command,
+ isc::data::ConstElementPtr args){
+ return (getController()->executeCommand(command, args));
+ }
+
+ /// @brief Callback that will generate shutdown command via the
+ /// command callback function.
+ static void genShutdownCallback() {
+ isc::data::ElementPtr arg_set;
+ DControllerBase::commandHandler(SHUT_DOWN_COMMAND, arg_set);
+ }
+
+ /// @brief Callback that throws an exception.
+ static void genFatalErrorCallback() {
+ isc_throw (DProcessBaseError, "simulated fatal error");
+ }
+};
+
+/// @brief Simple parser derivation for testing the basics of configuration
+/// parsing.
+class TestParser : public isc::dhcp::DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ ///
+ /// @param param_name name of the parsed parameter
+ TestParser(const std::string& param_name);
+
+ /// @brief Destructor
+ virtual ~TestParser();
+
+ /// @brief Builds parameter value.
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ ///
+ /// @param new_config pointer to the new configuration
+ /// @throw throws DCfgMgrBaseError if the SimFailure is set to
+ /// ftElementBuild. This allows for the simulation of an
+ /// exception during the build portion of parsing an element.
+ virtual void build(isc::data::ConstElementPtr new_config);
+
+ /// @brief Commits the parsed value to storage.
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ ///
+ /// @throw throws DCfgMgrBaseError if SimFailure is set to ftElementCommit.
+ /// This allows for the simulation of an exception during the commit
+ /// portion of parsing an element.
+ virtual void commit();
+
+private:
+ /// name of the parsed parameter
+ std::string param_name_;
+
+ /// pointer to the parsed value of the parameter
+ isc::data::ConstElementPtr value_;
+};
+
+/// @brief Test Derivation of the DCfgContextBase class.
+///
+/// This class is used to test basic functionality of configuration context.
+/// It adds an additional storage container "extra values" to mimic an
+/// application extension of configuration storage. This permits testing that
+/// both the base class content as well as the application content is
+/// correctly copied during cloning. This is vital to configuration backup
+/// and rollback during configuration parsing.
+class DStubContext : public DCfgContextBase {
+public:
+
+ /// @brief Constructor
+ DStubContext();
+
+ /// @brief Destructor
+ virtual ~DStubContext();
+
+ /// @brief Fetches the value for a given "extra" configuration parameter
+ /// from the context.
+ ///
+ /// @param name is the name of the parameter to retrieve.
+ /// @param value is an output parameter in which to return the retrieved
+ /// value.
+ /// @throw throws DhcpConfigError if the context does not contain the
+ /// parameter.
+ void getExtraParam(const std::string& name, uint32_t& value);
+
+ /// @brief Fetches the extra storage.
+ ///
+ /// @return returns a pointer to the extra storage.
+ isc::dhcp::Uint32StoragePtr getExtraStorage();
+
+ /// @brief Creates a clone of a DStubContext.
+ ///
+ /// @return returns a pointer to the new clone.
+ virtual DCfgContextBasePtr clone();
+
+protected:
+ /// @brief Copy constructor
+ DStubContext(const DStubContext& rhs);
+
+private:
+ /// @brief Private assignment operator, not implemented.
+ DStubContext& operator=(const DStubContext& rhs);
+
+ /// @brief Extra storage for uint32 parameters.
+ isc::dhcp::Uint32StoragePtr extra_values_;
+};
+
+/// @brief Defines a pointer to DStubContext.
+typedef boost::shared_ptr<DStubContext> DStubContextPtr;
+
+/// @brief Test Derivation of the DCfgMgrBase class.
+///
+/// This class is used to test basic functionality of configuration management.
+/// It supports the following configuration elements:
+///
+/// "bool_test" - Boolean element, tests parsing and committing a boolean
+/// configuration parameter.
+/// "uint32_test" - Uint32 element, tests parsing and committing a uint32_t
+/// configuration parameter.
+/// "string_test" - String element, tests parsing and committing a string
+/// configuration parameter.
+/// "extra_test" - "Extra" element, tests parsing and committing an extra
+/// configuration parameter. (This is used to demonstrate
+/// derivation's addition of storage to configuration context.
+///
+/// It also keeps track of the element ids that are parsed in the order they
+/// are parsed. This is used to test ordered and non-ordered parsing.
+class DStubCfgMgr : public DCfgMgrBase {
+public:
+ /// @brief Constructor
+ DStubCfgMgr();
+
+ /// @brief Destructor
+ virtual ~DStubCfgMgr();
+
+ /// @brief Given an element_id returns an instance of the appropriate
+ /// parser. It supports the element ids as described in the class brief.
+ ///
+ /// @param element_id is the string name of the element as it will appear
+ /// in the configuration set.
+ ///
+ /// @return returns a ParserPtr to the parser instance.
+ /// @throw throws DCfgMgrBaseError if SimFailure is ftElementUnknown.
+ virtual isc::dhcp::ParserPtr
+ createConfigParser(const std::string& element_id);
+
+ /// @brief A list for remembering the element ids in the order they were
+ /// parsed.
+ ElementIdList parsed_order_;
+};
+
+/// @brief Defines a pointer to DStubCfgMgr.
+typedef boost::shared_ptr<DStubCfgMgr> DStubCfgMgrPtr;
+
+/// @brief Test fixture base class for any fixtures which test parsing.
+/// It provides methods for converting JSON strings to configuration element
+/// sets and checking parse results
+class ConfigParseTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ ConfigParseTest(){
+ }
+
+ /// @brief Destructor
+ ~ConfigParseTest() {
+ }
+
+ /// @brief Converts a given JSON string into an Element set and stores the
+ /// result the member variable, config_set_.
+ ///
+ /// @param json_text contains the configuration text in JSON format to
+ /// convert.
+ /// @return returns AssertionSuccess if there were no parsing errors,
+ /// AssertionFailure otherwise.
+ ::testing::AssertionResult fromJSON(const std::string& json_text) {
+ try {
+ config_set_ = isc::data::Element::fromJSON(json_text);
+ } catch (const isc::Exception &ex) {
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "JSON text failed to parse:"
+ << ex.what()));
+ }
+
+ return (::testing::AssertionSuccess());
+ }
+
+ /// @brief Compares the status in the parse result stored in member
+ /// variable answer_ to a given value.
+ ///
+ /// @param should_be is an integer against which to compare the status.
+ ///
+ /// @return returns AssertionSuccess if there were no parsing errors,
+ /// AssertionFailure otherwise.
+ ::testing::AssertionResult checkAnswer(int should_be) {
+ return (checkAnswer(answer_, should_be));
+ }
+
+ /// @brief Compares the status in the given parse result to a given value.
+ ///
+ /// @param answer Element set containing an integer response and string
+ /// comment.
+ /// @param should_be is an integer against which to compare the status.
+ ///
+ /// @return returns AssertionSuccess if there were no parsing errors,
+ /// AssertionFailure otherwise.
+ ::testing::AssertionResult checkAnswer(isc::data::ConstElementPtr answer,
+ int should_be) {
+ int rcode = 0;
+ isc::data::ConstElementPtr comment;
+ comment = isc::config::parseAnswer(rcode, answer);
+ if (rcode == should_be) {
+ return (testing::AssertionSuccess());
+ }
+
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "checkAnswer rcode:" << rcode
+ << " comment: " << *comment));
+ }
+
+ /// @brief Configuration set being tested.
+ isc::data::ElementPtr config_set_;
+
+ /// @brief Results of most recent element parsing.
+ isc::data::ConstElementPtr answer_;
+};
+
+/// @brief Defines a small but valid DHCP-DDNS compliant configuration for
+/// testing configuration parsing fundamentals.
+extern const char* valid_d2_config;
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/tests/dns_client_unittests.cc b/src/bin/d2/tests/dns_client_unittests.cc
new file mode 100644
index 0000000..1b4c2bd
--- /dev/null
+++ b/src/bin/d2/tests/dns_client_unittests.cc
@@ -0,0 +1,425 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <d2/dns_client.h>
+#include <asiodns/io_fetch.h>
+#include <asiodns/logger.h>
+#include <asiolink/interval_timer.h>
+#include <dns/rcode.h>
+#include <dns/rrclass.h>
+#include <dns/tsig.h>
+#include <asio/ip/udp.hpp>
+#include <asio/socket_base.hpp>
+#include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <nc_test_utils.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::asiodns;
+using namespace isc::d2;
+
+using namespace isc;
+using namespace isc::dns;
+using namespace isc::util;
+using namespace asio;
+using namespace asio::ip;
+
+namespace {
+
+const char* TEST_ADDRESS = "127.0.0.1";
+const uint16_t TEST_PORT = 5301;
+const size_t MAX_SIZE = 1024;
+const long TEST_TIMEOUT = 5 * 1000;
+
+// @brief Test Fixture class.
+//
+// This test fixture class implements DNSClient::Callback so as it can be
+// installed as a completion callback for tests it implements. This callback
+// is called when a DDNS transaction (send and receive) completes. This allows
+// for the callback function to directly access class members. In particular,
+// the callback function can access IOService on which run() was called and
+// call stop() on it.
+//
+// Many of the tests defined here schedule execution of certain tasks and block
+// until tasks are completed or a timeout is hit. However, if timeout is not
+// properly handled a task may be hanging for a long time. In order to prevent
+// it, the asiolink::IntervalTimer is used to break a running test if test
+// timeout is hit. This will result in test failure.
+class DNSClientTest : public virtual ::testing::Test, DNSClient::Callback {
+public:
+ IOService service_;
+ D2UpdateMessagePtr response_;
+ DNSClient::Status status_;
+ uint8_t receive_buffer_[MAX_SIZE];
+ DNSClientPtr dns_client_;
+ bool corrupt_response_;
+ bool expect_response_;
+ asiolink::IntervalTimer test_timer_;
+ int received_;
+ int expected_;
+
+ // @brief Constructor.
+ //
+ // This constructor overrides the default logging level of asiodns logger to
+ // prevent it from emitting debug messages from IOFetch class. Such an error
+ // message can be emitted if timeout occurs when DNSClient class is
+ // waiting for a response. Some of the tests are checking DNSClient behavior
+ // in case when response from the server is not received. Tests output would
+ // become messy if such errors were logged.
+ DNSClientTest()
+ : service_(),
+ status_(DNSClient::SUCCESS),
+ corrupt_response_(false),
+ expect_response_(true),
+ test_timer_(service_),
+ received_(0), expected_(0) {
+ asiodns::logger.setSeverity(isc::log::INFO);
+ response_.reset();
+ dns_client_.reset(new DNSClient(response_, this));
+
+ // Set the test timeout to break any running tasks if they hang.
+ test_timer_.setup(boost::bind(&DNSClientTest::testTimeoutHandler, this),
+ TEST_TIMEOUT);
+ }
+
+ // @brief Destructor.
+ //
+ // Sets the asiodns logging level back to DEBUG.
+ virtual ~DNSClientTest() {
+ asiodns::logger.setSeverity(isc::log::DEBUG);
+ };
+
+ // @brief Exchange completion callback.
+ //
+ // This callback is called when the exchange with the DNS server is
+ // complete or an error occurred. This includes the occurrence of a timeout.
+ //
+ // @param status A status code returned by DNSClient.
+ virtual void operator()(DNSClient::Status status) {
+ status_ = status;
+ if (!expected_ || (expected_ == ++received_))
+ {
+ service_.stop();
+ }
+
+ if (expect_response_) {
+ if (!corrupt_response_) {
+ // We should have received a response.
+ EXPECT_EQ(DNSClient::SUCCESS, status_);
+
+ ASSERT_TRUE(response_);
+ EXPECT_EQ(D2UpdateMessage::RESPONSE, response_->getQRFlag());
+ ASSERT_EQ(1,
+ response_->getRRCount(D2UpdateMessage::SECTION_ZONE));
+ D2ZonePtr zone = response_->getZone();
+ ASSERT_TRUE(zone);
+ EXPECT_EQ("example.com.", zone->getName().toText());
+ EXPECT_EQ(RRClass::IN().getCode(), zone->getClass().getCode());
+
+ } else {
+ EXPECT_EQ(DNSClient::INVALID_RESPONSE, status_);
+
+ }
+ // If we don't expect a response, the status should indicate a timeout.
+ } else {
+ EXPECT_EQ(DNSClient::TIMEOUT, status_);
+
+ }
+ }
+
+ // @brief Handler invoked when test timeout is hit.
+ //
+ // This callback stops all running (hanging) tasks on IO service.
+ void testTimeoutHandler() {
+ service_.stop();
+ FAIL() << "Test timeout hit.";
+ }
+
+ // @brief Handler invoked when test request is received.
+ //
+ // This callback handler is installed when performing async read on a
+ // socket to emulate reception of the DNS Update request by a server.
+ // As a result, this handler will send an appropriate DNS Update response
+ // message back to the address from which the request has come.
+ //
+ // @param socket A pointer to a socket used to receive a query and send a
+ // response.
+ // @param remote A pointer to an object which specifies the host (address
+ // and port) from which a request has come.
+ // @param receive_length A length (in bytes) of the received data.
+ // @param corrupt_response A bool value which indicates that the server's
+ // response should be invalid (true) or valid (false)
+ void udpReceiveHandler(udp::socket* socket, udp::endpoint* remote,
+ size_t receive_length, const bool corrupt_response) {
+ // The easiest way to create a response message is to copy the entire
+ // request.
+ OutputBuffer response_buf(receive_length);
+ response_buf.writeData(receive_buffer_, receive_length);
+ // If a response is to be valid, we have to modify it slightly. If not,
+ // we leave it as is.
+ if (!corrupt_response) {
+ // For a valid response the QR bit must be set. This bit
+ // differentiates both types of messages. Note that the 3rd byte of
+ // the message header comprises this bit in the front followed by
+ // the message code and reserved zeros. Therefore, this byte
+ // has the following value:
+ // 10101000,
+ // where a leading bit is a QR flag. The hexadecimal value is 0xA8.
+ // Write it at message offset 2.
+ response_buf.writeUint8At(0xA8, 2);
+ }
+ // A response message is now ready to send. Send it!
+ socket->send_to(asio::buffer(response_buf.getData(),
+ response_buf.getLength()),
+ *remote);
+ }
+
+ // This test verifies that when invalid response placeholder object is
+ // passed to a constructor, constructor throws the appropriate exception.
+ // It also verifies that the constructor will not throw if the supplied
+ // callback object is NULL.
+ void runConstructorTest() {
+ EXPECT_NO_THROW(DNSClient(response_, NULL, DNSClient::UDP));
+
+ // The TCP Transport is not supported right now. So, we return exception
+ // if caller specified TCP as a preferred protocol. This test will be
+ // removed once TCP is supported.
+ EXPECT_THROW(DNSClient(response_, NULL, DNSClient::TCP),
+ isc::NotImplemented);
+ }
+
+ // This test verifies that it accepted timeout values belong to the range of
+ // <0, DNSClient::getMaxTimeout()>.
+ void runInvalidTimeoutTest() {
+
+ expect_response_ = false;
+
+ // Create outgoing message. Simply set the required message fields:
+ // error code and Zone section. This is enough to create on-wire format
+ // of this message and send it.
+ D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+ ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+ ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+ // Start with a valid timeout equal to maximal allowed. This way we will
+ // ensure that doUpdate doesn't throw an exception for valid timeouts.
+ unsigned int timeout = DNSClient::getMaxTimeout();
+ EXPECT_NO_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+ TEST_PORT, message, timeout));
+
+ // Cross the limit and expect that exception is thrown this time.
+ timeout = DNSClient::getMaxTimeout() + 1;
+ EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+ TEST_PORT, message, timeout),
+ isc::BadValue);
+ }
+
+ // This test verifies that isc::NotImplemented exception is thrown when
+ // attempt to send DNS Update message with TSIG is attempted.
+ void runTSIGTest() {
+ // Create outgoing message. Simply set the required message fields:
+ // error code and Zone section. This is enough to create on-wire format
+ // of this message and send it.
+ D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+ ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+ ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+ const int timeout = 0;
+ // Try to send DNS Update with TSIG key. Currently TSIG is not supported
+ // and therefore we expect an exception.
+ TSIGKey tsig_key("key.example:MSG6Ng==");
+ EXPECT_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+ TEST_PORT, message, timeout,
+ tsig_key),
+ isc::NotImplemented);
+ }
+
+ // This test verifies the DNSClient behavior when a server does not respond
+ // do the DNS Update message. In such case, the callback function is
+ // expected to be called and the TIME_OUT error code should be returned.
+ void runSendNoReceiveTest() {
+ // We expect no response from a server.
+ expect_response_ = false;
+
+ // Create outgoing message. Simply set the required message fields:
+ // error code and Zone section. This is enough to create on-wire format
+ // of this message and send it.
+ D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+ ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+ ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+ // Set the response wait time to 0 so as our test is not hanging. This
+ // should cause instant timeout.
+ const int timeout = 500;
+ // The doUpdate() function starts asynchronous message exchange with DNS
+ // server. When message exchange is done or timeout occurs, the
+ // completion callback will be triggered. The doUpdate function returns
+ // immediately.
+ EXPECT_NO_THROW(dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS),
+ TEST_PORT, message, timeout));
+
+ // This starts the execution of tasks posted to IOService. run() blocks
+ // until stop() is called in the completion callback function.
+ service_.run();
+
+ }
+
+ // This test verifies that DNSClient can send DNS Update and receive a
+ // corresponding response from a server.
+ void runSendReceiveTest(const bool corrupt_response,
+ const bool two_sends) {
+ corrupt_response_ = corrupt_response;
+
+ // Create a request DNS Update message.
+ D2UpdateMessage message(D2UpdateMessage::OUTBOUND);
+ ASSERT_NO_THROW(message.setRcode(Rcode(Rcode::NOERROR_CODE)));
+ ASSERT_NO_THROW(message.setZone(Name("example.com"), RRClass::IN()));
+
+ // In order to perform the full test, when the client sends the request
+ // and receives a response from the server, we have to emulate the
+ // server's response in the test. A request will be sent via loopback
+ // interface to 127.0.0.1 and known test port. Response must be sent
+ // to 127.0.0.1 and a source port which has been used to send the
+ // request. A new socket is created, specifically to handle sending
+ // responses. The reuse address option is set so as both sockets can
+ // use the same address. This new socket is bound to the test address
+ // and port, where requests will be sent.
+ udp::socket udp_socket(service_.get_io_service(), asio::ip::udp::v4());
+ udp_socket.set_option(socket_base::reuse_address(true));
+ udp_socket.bind(udp::endpoint(address::from_string(TEST_ADDRESS),
+ TEST_PORT));
+ // Once socket is created, we can post an IO request to receive some
+ // a packet from this socket. This is asynchronous operation and
+ // nothing is received until another IO request to send a query is
+ // posted and the run() is invoked on this IO. A callback function is
+ // attached to this asynchronous read. This callback function requires
+ // that a socket object used to receive the request is passed to it,
+ // because the same socket will be used by the callback function to send
+ // a response. Also, the remote object is passed to the callback,
+ // because it holds a source address and port where request originated.
+ // Callback function will send a response to this address and port.
+ // The last parameter holds a length of the received request. It is
+ // required to construct a response.
+ udp::endpoint remote;
+ udp_socket.async_receive_from(asio::buffer(receive_buffer_,
+ sizeof(receive_buffer_)),
+ remote,
+ boost::bind(&DNSClientTest::udpReceiveHandler,
+ this, &udp_socket, &remote, _2,
+ corrupt_response));
+
+ // The socket is now ready to receive the data. Let's post some request
+ // message then.
+ const int timeout = 5;
+ expected_++;
+ dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+ message, timeout);
+
+ // It is possible to request that two packets are sent concurrently.
+ if (two_sends) {
+ expected_++;
+ dns_client_->doUpdate(service_, IOAddress(TEST_ADDRESS), TEST_PORT,
+ message, timeout);
+
+ }
+
+ // Kick of the message exchange by actually running the scheduled
+ // "send" and "receive" operations.
+ service_.run();
+
+ udp_socket.close();
+
+ // Since the callback, operator(), calls stop() on the io_service,
+ // we must reset it in order for subsequent calls to run() or
+ // run_one() to work.
+ service_.get_io_service().reset();
+ }
+};
+
+// Verify that the DNSClient object can be created if provided parameters are
+// valid. Constructor should throw exceptions when parameters are invalid.
+TEST_F(DNSClientTest, constructor) {
+ runConstructorTest();
+}
+
+// This test verifies that the maximal allowed timeout value is maximal int
+// value.
+TEST_F(DNSClientTest, getMaxTimeout) {
+ EXPECT_EQ(std::numeric_limits<int>::max(), DNSClient::getMaxTimeout());
+}
+
+// Verify that timeout is reported when no response is received from DNS.
+TEST_F(DNSClientTest, timeout) {
+ runSendNoReceiveTest();
+}
+
+// Verify that exception is thrown when invalid (too high) timeout value is
+// specified for asynchronous DNS Update.
+TEST_F(DNSClientTest, invalidTimeout) {
+ runInvalidTimeoutTest();
+}
+
+// Verify that exception is thrown when an attempt to send DNS Update with TSIG
+// is made. This test will be removed/changed once TSIG support is added.
+TEST_F(DNSClientTest, runTSIGTest) {
+ runTSIGTest();
+}
+
+// Verify that the DNSClient receives the response from DNS and the received
+// buffer can be decoded as DNS Update Response.
+TEST_F(DNSClientTest, sendReceive) {
+ // false means that server response is not corrupted.
+ runSendReceiveTest(false, false);
+}
+
+// Verify that the DNSClient reports an error when the response is received from
+// a DNS and this response is corrupted.
+TEST_F(DNSClientTest, sendReceiveCurrupted) {
+ // true means that server's response is corrupted.
+ runSendReceiveTest(true, false);
+}
+
+// Verify that it is possible to use the same DNSClient instance to
+// perform the following sequence of message exchanges:
+// 1. send
+// 2. receive
+// 3. send
+// 4. receive
+TEST_F(DNSClientTest, sendReceiveTwice) {
+ runSendReceiveTest(false, false);
+ runSendReceiveTest(false, false);
+ EXPECT_EQ(2, received_);
+}
+
+// Verify that it is possible to use the DNSClient instance to perform the
+// following sequence of message exchanges:
+// 1. send
+// 2. send
+// 3. receive
+// 4. receive
+// @todo THIS Test does not function. The method runSendReceive only
+// schedules one "server" receive. In other words only one request is
+// listened for and then received. Once it is received, the operator()
+// method calls stop() on the io_service, which causes the second receive
+// to be cancelled. It is also unclear, what the asio layer does with a
+// second receive on the same socket.
+TEST_F(DNSClientTest, DISABLED_concurrentSendReceive) {
+ runSendReceiveTest(false, true);
+}
+
+} // End of anonymous namespace
diff --git a/src/bin/d2/tests/labeled_value_unittests.cc b/src/bin/d2/tests/labeled_value_unittests.cc
new file mode 100644
index 0000000..a4cfc01
--- /dev/null
+++ b/src/bin/d2/tests/labeled_value_unittests.cc
@@ -0,0 +1,107 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/labeled_value.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Verifies basic construction and accessors for LabeledValue.
+TEST(LabeledValue, construction) {
+ /// Verify that an empty label is not allowed.
+ ASSERT_THROW(LabeledValue(1, ""), LabeledValueError);
+
+ /// Verify that a valid constructor works.
+ LabeledValuePtr lvp;
+ ASSERT_NO_THROW(lvp.reset(new LabeledValue(1, "NotBlank")));
+ ASSERT_TRUE(lvp);
+
+ // Verify that the value can be accessed.
+ EXPECT_EQ(1, lvp->getValue());
+
+ // Verify that the label can be accessed.
+ EXPECT_EQ("NotBlank", lvp->getLabel());
+}
+
+/// @brief Verifies the logical operators defined for LabeledValue.
+TEST(LabeledValue, operators) {
+ LabeledValuePtr lvp1;
+ LabeledValuePtr lvp1Also;
+ LabeledValuePtr lvp2;
+
+ // Create three instances, two of which have the same numeric value.
+ ASSERT_NO_THROW(lvp1.reset(new LabeledValue(1, "One")));
+ ASSERT_NO_THROW(lvp1Also.reset(new LabeledValue(1, "OneAlso")));
+ ASSERT_NO_THROW(lvp2.reset(new LabeledValue(2, "Two")));
+
+ // Verify each of the operators.
+ EXPECT_TRUE(*lvp1 == *lvp1Also);
+ EXPECT_TRUE(*lvp1 != *lvp2);
+ EXPECT_TRUE(*lvp1 < *lvp2);
+ EXPECT_FALSE(*lvp2 < *lvp1);
+}
+
+/// @brief Verifies the default constructor for LabeledValueSet.
+TEST(LabeledValueSet, construction) {
+ ASSERT_NO_THROW (LabeledValueSet());
+}
+
+/// @brief Verifies the basic operations of a LabeledValueSet.
+/// Essentially we verify that we can define a set of valid entries and
+/// look them up without issue.
+TEST(LabeledValueSet, basicOperation) {
+ const char* labels[] = {"Zero", "One", "Two", "Three" };
+ LabeledValueSet lvset;
+ LabeledValuePtr lvp;
+
+ // Verify the we cannot add an empty pointer to the set.
+ EXPECT_THROW(lvset.add(lvp), LabeledValueError);
+
+ // Verify that we can add an entry to the set via pointer.
+ ASSERT_NO_THROW(lvp.reset(new LabeledValue(0, labels[0])));
+ EXPECT_NO_THROW(lvset.add(lvp));
+
+ // Verify that we cannot add a duplicate entry.
+ EXPECT_THROW(lvset.add(lvp), LabeledValueError);
+
+ // Add the remaining entries using add(int,char*) variant.
+ for (int i = 1; i < 3; i++) {
+ EXPECT_NO_THROW(lvset.add(i, labels[i]));
+ }
+
+ // Verify that we can't add a duplicate entry this way either.
+ EXPECT_THROW ((lvset.add(0, labels[0])), LabeledValueError);
+
+ // Verify that we can look up all of the defined entries properly.
+ for (int i = 1; i < 3; i++) {
+ EXPECT_TRUE(lvset.isDefined(i));
+ EXPECT_NO_THROW(lvp = lvset.get(i));
+ EXPECT_EQ(lvp->getValue(), i);
+ EXPECT_EQ(lvp->getLabel(), labels[i]);
+ EXPECT_EQ(lvset.getLabel(i), labels[i]);
+ }
+
+ // Verify behavior for a value that is not defined.
+ EXPECT_FALSE(lvset.isDefined(4));
+ EXPECT_NO_THROW(lvp = lvset.get(4));
+ EXPECT_FALSE(lvp);
+ EXPECT_EQ(lvset.getLabel(4), LabeledValueSet::UNDEFINED_LABEL);
+}
+
+}
diff --git a/src/bin/d2/tests/nc_add_unittests.cc b/src/bin/d2/tests/nc_add_unittests.cc
new file mode 100644
index 0000000..05e8a1a
--- /dev/null
+++ b/src/bin/d2/tests/nc_add_unittests.cc
@@ -0,0 +1,1790 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_cfg_mgr.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/nc_add.h>
+#include <dns/messagerenderer.h>
+#include <nc_test_utils.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Test class derived from NameAddTransaction to provide visiblity
+// to protected methods.
+class NameAddStub : public NameAddTransaction {
+public:
+ NameAddStub(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain)
+ : NameAddTransaction(io_service, ncr, forward_domain, reverse_domain),
+ simulate_send_exception_(false),
+ simulate_build_request_exception_(false) {
+ }
+
+ virtual ~NameAddStub() {
+ }
+
+ /// @brief Simulates sending update requests to the DNS server
+ ///
+ /// This method simulates the initiation of an asynchronous send of
+ /// a DNS update request. It overrides the actual sendUpdate method in
+ /// the base class, thus avoiding an actual send, yet still increments
+ /// the update attempt count and posts a next event of NOP_EVT.
+ ///
+ /// It will also simulate an exception-based failure of sendUpdate, if
+ /// the simulate_send_exception_ flag is true.
+ ///
+ /// @param use_tsig_ Parameter is unused, but present in the base class
+ /// method.
+ ///
+ virtual void sendUpdate(bool /* use_tsig_ = false */) {
+ if (simulate_send_exception_) {
+ // Make the flag a one-shot by resetting it.
+ simulate_send_exception_ = false;
+ // Transition to failed.
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ return;
+ }
+
+ // Update send attempt count and post a NOP_EVT.
+ setUpdateAttempts(getUpdateAttempts() + 1);
+ postNextEvent(StateModel::NOP_EVT);
+ }
+
+ /// @brief Prepares the initial D2UpdateMessage
+ ///
+ /// This method overrides the NameChangeTransactio implementation to
+ /// provide the ability to simulate an exception throw in the build
+ /// request logic.
+ /// If the one-shot flag, simulate_build_request_exception_ is true,
+ /// this method will throw an exception, otherwise it will invoke the
+ /// base class method, providing normal functionality.
+ ///
+ /// For parameter description see the NameChangeTransaction implementation.
+ virtual D2UpdateMessagePtr prepNewRequest(DdnsDomainPtr domain) {
+ if (simulate_build_request_exception_) {
+ simulate_build_request_exception_ = false;
+ isc_throw (NameAddTransactionError,
+ "Simulated build requests exception");
+ }
+
+ return (NameChangeTransaction::prepNewRequest(domain));
+ }
+
+ /// @brief Simulates receiving a response
+ ///
+ /// This method simulates the completion of a DNSClient send. This allows
+ /// the state handler logic devoted to dealing with IO completion to be
+ /// fully exercised without requiring any actual IO. The two primary
+ /// pieces of information gleaned from IO completion are the DNSClient
+ /// status which indicates whether or not the IO exchange was successful
+ /// and the rcode, which indicates the server's reaction to the request.
+ ///
+ /// This method updates the transaction's DNS status value to that of the
+ /// given parameter, and then constructs and DNS update response message
+ /// with the given rcode value. To complete the simulation it then posts
+ /// a next event of IO_COMPLETED_EVT.
+ ///
+ /// @param status simulated DNSClient status
+ /// @param rcode simulated server response code
+ void fakeResponse(const DNSClient::Status& status,
+ const dns::Rcode& rcode) {
+ // Set the DNS update status. This is normally set in
+ // DNSClient IO completion handler.
+ setDnsUpdateStatus(status);
+
+ // Construct an empty message with the given Rcode.
+ D2UpdateMessagePtr msg(new D2UpdateMessage(D2UpdateMessage::OUTBOUND));
+ msg->setRcode(rcode);
+
+ // Set the update response to the message.
+ setDnsUpdateResponse(msg);
+
+ // Post the IO completion event.
+ postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+ }
+
+ /// @brief Selects the first forward server.
+ /// Some state handlers require a server to have been selected.
+ /// This selects a server without going through the state
+ /// transition(s) to do so.
+ bool selectFwdServer() {
+ if (getForwardDomain()) {
+ initServerSelection(getForwardDomain());
+ selectNextServer();
+ return (getCurrentServer());
+ }
+
+ return (false);
+ }
+
+ /// @brief Selects the first reverse server.
+ /// Some state handlers require a server to have been selected.
+ /// This selects a server without going through the state
+ /// transition(s) to do so.
+ bool selectRevServer() {
+ if (getReverseDomain()) {
+ initServerSelection(getReverseDomain());
+ selectNextServer();
+ return (getCurrentServer());
+ }
+
+ return (false);
+ }
+
+ /// @brief One-shot flag which will simulate sendUpdate failure if true.
+ bool simulate_send_exception_;
+
+ /// @brief One-shot flag which will simulate an exception when sendUpdate
+ /// failure if true.
+ bool simulate_build_request_exception_;
+
+ using StateModel::postNextEvent;
+ using StateModel::setState;
+ using StateModel::initDictionaries;
+ using NameAddTransaction::defineEvents;
+ using NameAddTransaction::verifyEvents;
+ using NameAddTransaction::defineStates;
+ using NameAddTransaction::verifyStates;
+ using NameAddTransaction::readyHandler;
+ using NameAddTransaction::selectingFwdServerHandler;
+ using NameAddTransaction::getCurrentServer;
+ using NameAddTransaction::addingFwdAddrsHandler;
+ using NameAddTransaction::setDnsUpdateStatus;
+ using NameAddTransaction::replacingFwdAddrsHandler;
+ using NameAddTransaction::selectingRevServerHandler;
+ using NameAddTransaction::replacingRevPtrsHandler;
+ using NameAddTransaction::processAddOkHandler;
+ using NameAddTransaction::processAddFailedHandler;
+ using NameAddTransaction::buildAddFwdAddressRequest;
+ using NameAddTransaction::buildReplaceFwdAddressRequest;
+ using NameAddTransaction::buildReplaceRevPtrsRequest;
+};
+
+typedef boost::shared_ptr<NameAddStub> NameAddStubPtr;
+
+/// @brief Test fixture for testing NameAddTransaction
+///
+/// Note this class uses NameAddStub class to exercise non-public
+/// aspects of NameAddTransaction.
+class NameAddTransactionTest : public TransactionTest {
+public:
+
+ NameAddTransactionTest() {
+ }
+
+ virtual ~NameAddTransactionTest() {
+ }
+
+ /// @brief Creates a transaction which requests an IPv4 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv4 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_mask determines which change directions are requested
+ NameAddStubPtr makeTransaction4(int change_mask = FWD_AND_REV_CHG) {
+ // Creates IPv4 remove request, forward, and reverse domains.
+ setupForIPv4Transaction(dhcp_ddns::CHG_ADD, change_mask);
+
+ // Now create the test transaction as would occur in update manager.
+ return (NameAddStubPtr(new NameAddStub(io_service_, ncr_,
+ forward_domain_,
+ reverse_domain_)));
+ }
+
+ /// @brief Creates a transaction which requests an IPv6 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv6 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_mask determines which change directions are requested
+ NameAddStubPtr makeTransaction6(int change_mask = FWD_AND_REV_CHG) {
+ // Creates IPv6 remove request, forward, and reverse domains.
+ setupForIPv6Transaction(dhcp_ddns::CHG_ADD, change_mask);
+
+ // Now create the test transaction as would occur in update manager.
+ return (NameAddStubPtr(new NameAddStub(io_service_, ncr_,
+ forward_domain_,
+ reverse_domain_)));
+ }
+
+ /// @brief Create a test transaction at a known point in the state model.
+ ///
+ /// Method prepares a new test transaction and sets its state and next
+ /// event values to those given. This makes the transaction appear to
+ /// be at that point in the state model without having to transition it
+ /// through prerequisite states. It also provides the ability to set
+ /// which change directions are requested: forward change only, reverse
+ /// change only, or both.
+ ///
+ /// @param state value to set as the current state
+ /// @param event value to post as the next event
+ /// @param change_mask determines which change directions are requested
+ /// @param family selects between an IPv4 (AF_INET) and IPv6 (AF_INET6)
+ /// transaction.
+ NameAddStubPtr prepHandlerTest(unsigned int state, unsigned int event,
+ unsigned int change_mask = FWD_AND_REV_CHG,
+ short family = AF_INET) {
+ NameAddStubPtr name_add = (family == AF_INET ?
+ makeTransaction4(change_mask) :
+ makeTransaction4(change_mask));
+ name_add->initDictionaries();
+ name_add->postNextEvent(event);
+ name_add->setState(state);
+ return (name_add);
+ }
+};
+
+/// @brief Tests NameAddTransaction construction.
+/// This test verifies that:
+/// 1. Construction with invalid type of request
+/// 2. Valid construction functions properly
+TEST(NameAddTransaction, construction) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 1 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : true , "
+ " \"fqdn\" : \"example.com.\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ DnsServerInfoStoragePtr servers;
+ DdnsDomainPtr forward_domain;
+ DdnsDomainPtr reverse_domain;
+ DdnsDomainPtr empty_domain;
+
+ ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
+ ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
+ ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+
+ // Verify that construction with wrong change type fails.
+ EXPECT_THROW(NameAddTransaction(io_service, ncr,
+ forward_domain, reverse_domain),
+ NameAddTransactionError);
+
+ // Verify that a valid construction attempt works.
+ ncr->setChangeType(isc::dhcp_ddns::CHG_ADD);
+ EXPECT_NO_THROW(NameAddTransaction(io_service, ncr,
+ forward_domain, reverse_domain));
+}
+
+/// @brief Tests event and state dictionary construction and verification.
+TEST_F(NameAddTransactionTest, dictionaryCheck) {
+ NameAddStubPtr name_add;
+ ASSERT_NO_THROW(name_add = makeTransaction4());
+ // Verify that the event and state dictionary validation fails prior
+ // dictionary construction.
+ ASSERT_THROW(name_add->verifyEvents(), StateModelError);
+ ASSERT_THROW(name_add->verifyStates(), StateModelError);
+
+ // Construct both dictionaries.
+ ASSERT_NO_THROW(name_add->defineEvents());
+ ASSERT_NO_THROW(name_add->defineStates());
+
+ // Verify both event and state dictionaries now pass validation.
+ ASSERT_NO_THROW(name_add->verifyEvents());
+ ASSERT_NO_THROW(name_add->verifyStates());
+}
+
+/// @brief Tests construction of a DNS update request for adding a forward
+/// dns entry.
+TEST_F(NameAddTransactionTest, buildForwardAdd) {
+ // Create a IPv4 forward add transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameAddStubPtr name_add;
+ ASSERT_NO_THROW(name_add = makeTransaction4());
+ ASSERT_NO_THROW(name_add->buildAddFwdAddressRequest());
+ checkAddFwdAddressRequest(*name_add);
+
+ // Create a IPv6 forward add transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_add = makeTransaction6());
+ ASSERT_NO_THROW(name_add->buildAddFwdAddressRequest());
+ checkAddFwdAddressRequest(*name_add);
+}
+
+/// @brief Tests construction of a DNS update request for replacing a forward
+/// dns entry.
+TEST_F(NameAddTransactionTest, buildReplaceFwdAddressRequest) {
+ // Create a IPv4 forward replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameAddStubPtr name_add;
+ ASSERT_NO_THROW(name_add = makeTransaction4());
+ ASSERT_NO_THROW(name_add->buildReplaceFwdAddressRequest());
+ checkReplaceFwdAddressRequest(*name_add);
+
+ // Create a IPv6 forward replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_add = makeTransaction6());
+ ASSERT_NO_THROW(name_add->buildReplaceFwdAddressRequest());
+ checkReplaceFwdAddressRequest(*name_add);
+}
+
+/// @brief Tests the construction of a DNS update request for replacing a
+/// reverse dns entry.
+TEST_F(NameAddTransactionTest, buildReplaceRevPtrsRequest) {
+ // Create a IPv4 reverse replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameAddStubPtr name_add;
+ ASSERT_NO_THROW(name_add = makeTransaction4());
+ ASSERT_NO_THROW(name_add->buildReplaceRevPtrsRequest());
+ checkReplaceRevPtrsRequest(*name_add);
+
+ // Create a IPv6 reverse replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_add = makeTransaction6());
+ ASSERT_NO_THROW(name_add->buildReplaceRevPtrsRequest());
+ checkReplaceRevPtrsRequest(*name_add);
+}
+
+// Tests the readyHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is START_EVT and request includes only a forward change
+// 2. Posted event is START_EVT and request includes both a forward and a
+// reverse change
+// 3. Posted event is START_EVT and request includes only a reverse change
+// 4. Posted event is invalid
+//
+TEST_F(NameAddTransactionTest, readyHandler) {
+ NameAddStubPtr name_add;
+
+ // Create a transaction which includes only a forward change.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, FORWARD_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_add->readyHandler());
+
+ // Verify that a request requiring only a forward change, transitions to
+ // selecting a forward server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_add->getNextEvent());
+
+
+ // Create a transaction which includes both a forward and a reverse change.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, FWD_AND_REV_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_add->readyHandler());
+
+ // Verify that a request requiring both forward and reverse, starts with
+ // the forward change by transitioning to selecting a forward server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_add->getNextEvent());
+
+
+ // Create and prep a reverse only transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, REVERSE_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_add->readyHandler());
+
+ // Verify that a request requiring only a reverse change, transitions to
+ // selecting a reverse server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_add->getNextEvent());
+
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::NOP_EVT));
+
+ // Running the readyHandler should throw.
+ EXPECT_THROW(name_add->readyHandler(), NameAddTransactionError);
+}
+
+// Tests the selectingFwdServerHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is SELECT_SERVER_EVT
+// 2. Posted event is SERVER_IO_ERROR_EVT
+// 3. Posted event is invalid
+//
+TEST_F(NameAddTransactionTest, selectingFwdServerHandler) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_FWD_SERVER_ST,
+ NameChangeTransaction::SELECT_SERVER_EVT));
+
+ // Call selectingFwdServerHandler enough times to select all of the
+ // servers in it's current domain. The first time, it will be with
+ // next event of SELECT_SERVER_EVT. Thereafter it will be with a next
+ // event of SERVER_IO_ERROR_EVT.
+ int num_servers = name_add->getForwardDomain()->getServers()->size();
+ for (int i = 0; i < num_servers; ++i) {
+ // Run selectingFwdServerHandler.
+ ASSERT_NO_THROW(name_add->selectingFwdServerHandler())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that a server was selected.
+ ASSERT_TRUE(name_add->getCurrentServer())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Verify that we transitioned correctly.
+ ASSERT_EQ(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ name_add->getCurrState())
+ << " num_servers: " << num_servers << " selections: " << i;
+ ASSERT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Post a server IO error event. This simulates an IO error occuring
+ // and a need to select the new server.
+ ASSERT_NO_THROW(name_add->postNextEvent(NameChangeTransaction::
+ SERVER_IO_ERROR_EVT))
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+ }
+
+ // We should have exhausted the list of servers. Processing another
+ // SERVER_IO_ERROR_EVT should transition us to failure.
+ EXPECT_NO_THROW(name_add->selectingFwdServerHandler());
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NO_MORE_SERVERS_EVT,
+ name_add->getNextEvent());
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_FWD_SERVER_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->selectingFwdServerHandler(),
+ NameAddTransactionError);
+}
+
+// ************************ addingFwdAddrHandler Tests *****************
+
+// Tests that addingFwdAddrsHandler rejects invalid events.
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_InvalidEvent) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->addingFwdAddrsHandler(),
+ NameAddTransactionError);
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_FwdOnlyAddOK) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_add->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Run addingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkAddFwdAddressRequest(*name_add);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NOP_EVT,
+ name_add->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Forward completion should be true, reverse should be false.
+ EXPECT_TRUE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_fwdAndRevAddOK) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FWD_AND_REV_CHG));
+
+ // Run addingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Forward change completion should be true, reverse flag should be false.
+ EXPECT_TRUE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since the request also includes a reverse change we should
+ // be poised to start it. Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the FQDN is in use.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_FqdnInUse) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Run addingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Simulate receiving a FQDN in use response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::YXDOMAIN());
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since the FQDN is in use, per the RFC we must attempt to replace it.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameAddTransaction::FQDN_IN_USE_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_OtherRcode) {
+ NameAddStubPtr name_add;
+
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+ // Run addingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error or FQDN in use is failure. Arbitrarily choosing refused.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verify that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_Timeout) {
+ NameAddStubPtr name_add;
+
+ // Create and prep a transaction, poised to run the handler.
+ // The log message issued when this test succeeds, displays the
+ // selected server, so we need to select a server before running this
+ // test.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_add->getDnsUpdateRequest();
+
+ // Run addingFwdAddrsHandler to send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_add->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time out we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_add->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_InvalidResponse) {
+ NameAddStubPtr name_add;
+
+ // Create and prep a transaction, poised to run the handler.
+ // The log message issued when this test succeeds, displays the
+ // selected server, so we need to select a server before running this
+ // test.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ // Run addingFwdAddrsHandler to construct send the request.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Simulate a corrupt server response.
+ name_add->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run addingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+
+}
+
+// ************************ replacingFwdAddrHandler Tests *****************
+
+// Tests that replacingFwdAddrsHandler rejects invalid events.
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_InvalidEvent) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->replacingFwdAddrsHandler(),
+ NameAddTransactionError);
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is FQDN_IN_USE_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_FwdOnlyAddOK) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FORWARD_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_add->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkReplaceFwdAddressRequest(*name_add);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NOP_EVT,
+ name_add->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Forward completion should be true, reverse should be false.
+ EXPECT_TRUE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_FwdOnlyAddOK2) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Forward completion should be true, reverse should be false.
+ EXPECT_TRUE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is FQDN_IN_USE_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_FwdAndRevAddOK) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FWD_AND_REV_CHG));
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Forward change completion should be true, reverse flag should be false.
+ EXPECT_TRUE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since the request also includes a reverse change we should
+ // be poised to start it. Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_add->getNextEvent());
+}
+
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is FQDN_IN_USE_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the FQDN is NOT in use.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_FqdnNotInUse) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FWD_AND_REV_CHG));
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Simulate receiving a FQDN not in use response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since the FQDN is no longer in use, per the RFC, try to add it.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+}
+
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_OtherRcode) {
+ NameAddStubPtr name_add;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error or FQDN in use is failure. Arbitrarily choosing refused.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verifiy that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is FQDN_IN_USE_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_Timeout) {
+ NameAddStubPtr name_add;
+
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_add->getDnsUpdateRequest();
+
+ // Run replacingFwdAddrsHandler to send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_add->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time out we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_add->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is FQDN_IN_USE_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_CorruptResponse) {
+ NameAddStubPtr name_add;
+
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameAddTransaction::
+ FQDN_IN_USE_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_add->getDnsUpdateRequest();
+
+ // Run replacingFwdAddrsHandler to send the request.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_add->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time out we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a corrupt server response.
+ name_add->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run replacingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_FWD_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+}
+
+// Tests the selectingRevServerHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is SELECT_SERVER_EVT
+// 2. Posted event is SERVER_IO_ERROR_EVT
+// 3. Posted event is invalid
+//
+TEST_F(NameAddTransactionTest, selectingRevServerHandler) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_REV_SERVER_ST,
+ NameChangeTransaction::SELECT_SERVER_EVT));
+
+ // Call selectingRevServerHandler enough times to select all of the
+ // servers in it's current domain. The first time, it will be with
+ // next event of SELECT_SERVER_EVT. Thereafter it will be with a next
+ // event of SERVER_IO_ERROR_EVT.
+ int num_servers = name_add->getReverseDomain()->getServers()->size();
+ for (int i = 0; i < num_servers; ++i) {
+ // Run selectingRevServerHandler.
+ ASSERT_NO_THROW(name_add->selectingRevServerHandler())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that a server was selected.
+ ASSERT_TRUE(name_add->getCurrentServer())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that we transitioned correctly.
+ ASSERT_EQ(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ name_add->getCurrState())
+ << " num_servers: " << num_servers << " selections: " << i;
+ ASSERT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Post a server IO error event. This simulates an IO error occuring
+ // and a need to select the new server.
+ ASSERT_NO_THROW(name_add->postNextEvent(NameChangeTransaction::
+ SERVER_IO_ERROR_EVT))
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+ }
+
+ // We should have exhausted the list of servers. Processing another
+ // SERVER_IO_ERROR_EVT should transition us to failure.
+ EXPECT_NO_THROW(name_add->selectingRevServerHandler());
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NO_MORE_SERVERS_EVT,
+ name_add->getNextEvent());
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_REV_SERVER_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->selectingRevServerHandler(),
+ NameAddTransactionError);
+}
+
+//************************** replacingRevPtrsHandler tests *****************
+
+// Tests that replacingRevPtrsHandler rejects invalid events.
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_InvalidEvent) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameChangeTransaction::
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->replacingRevPtrsHandler(),
+ NameAddTransactionError);
+}
+
+// Tests replacingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_FwdOnlyAddOK) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameAddTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_add->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Run replacingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkReplaceRevPtrsRequest(*name_add);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NOP_EVT,
+ name_add->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run replacingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Forward completion should be false, reverse should be true.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_TRUE(name_add->getReverseChangeCompleted());
+
+ // Since it is a reverse change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_OtherRcode) {
+ NameAddStubPtr name_add;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameAddTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectRevServer());
+
+ // Run replacingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error is failure. Arbitrarily choosing refused.
+ name_add->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run replacingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verify that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_Timeout) {
+ NameAddStubPtr name_add;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameAddTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectRevServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_add->getDnsUpdateRequest();
+
+ // Run replacingRevPtrsHandler to send the request.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_add->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time out we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_add->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run replacingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_REV_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+}
+
+// Tests replacingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_CorruptResponse) {
+ NameAddStubPtr name_add;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameAddTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_add->selectRevServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_add->getDnsUpdateRequest();
+
+ // Run replacingRevPtrsHandler to send the request.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_add->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time out we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server corrupt response.
+ name_add->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_add->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run replacingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_add->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameAddTransaction::SELECTING_REV_SERVER_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_add->getNextEvent());
+ }
+ }
+}
+
+// Tests the processAddOkHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is UPDATE_OK_EVT
+// 2. Posted event is invalid
+//
+TEST_F(NameAddTransactionTest, processAddOkHandler) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ NameChangeTransaction::UPDATE_OK_EVT));
+ // Run processAddOkHandler.
+ EXPECT_NO_THROW(name_add->processAddOkHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_COMPLETED, name_add->getNcrStatus());
+
+ // Verify that the model has ended.
+ EXPECT_EQ(StateModel::END_ST, name_add->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_add->getNextEvent());
+
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ StateModel::NOP_EVT));
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->processAddOkHandler(), NameAddTransactionError);
+}
+
+// Tests the processAddFailedHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is UPDATE_FAILED_EVT
+// 2. Posted event is invalid
+//
+TEST_F(NameAddTransactionTest, processAddFailedHandler) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ NameChangeTransaction::UPDATE_FAILED_EVT));
+ // Run processAddFailedHandler.
+ EXPECT_NO_THROW(name_add->processAddFailedHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_add->getNcrStatus());
+
+ // Verify that the model has ended. (Remember, the transaction failed NOT
+ // the model. The model should have ended normally.)
+ EXPECT_EQ(StateModel::END_ST, name_add->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_add->getNextEvent());
+
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ StateModel::NOP_EVT));
+ // Running the handler should throw.
+ EXPECT_THROW(name_add->processAddFailedHandler(), NameAddTransactionError);
+}
+
+// Tests the processAddFailedHandler functionality.
+// It verifies behavior for posted event of NO_MORE_SERVERS_EVT.
+TEST_F(NameAddTransactionTest, processAddFailedHandler_NoMoreServers) {
+ NameAddStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ NameChangeTransaction::
+ NO_MORE_SERVERS_EVT));
+
+ // Run processAddFailedHandler.
+ EXPECT_NO_THROW(name_remove->processAddFailedHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_remove->getNcrStatus());
+
+ // Verify that the model has ended. (Remember, the transaction failed NOT
+ // the model. The model should have ended normally.)
+ EXPECT_EQ(StateModel::END_ST, name_remove->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_remove->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_sendUpdateException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ name_add->simulate_send_exception_ = true;
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ ASSERT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_SendUpdateException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ name_add->simulate_send_exception_ = true;
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ ASSERT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingRevPtrHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_SendUpdateException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ name_add->simulate_send_exception_ = true;
+
+ // Run replacingRevPtrsHandler to construct and send the request.
+ ASSERT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, addingFwdAddrsHandler_BuildRequestException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::ADDING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_add->simulate_build_request_exception_ = true;
+
+ // Run replacingRevPtrsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_add->addingFwdAddrsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_add->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+// Tests replacingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, replacingFwdAddrsHandler_BuildRequestException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_add->simulate_build_request_exception_ = true;
+
+ // Run replacingFwdAddrsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_add->replacingFwdAddrsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_add->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+
+// Tests replacingRevPtrHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameAddTransactionTest, replacingRevPtrsHandler_BuildRequestException) {
+ NameAddStubPtr name_add;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_add =
+ prepHandlerTest(NameAddTransaction::REPLACING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_add->simulate_build_request_exception_ = true;
+
+ // Run replacingRevPtrsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_add->replacingRevPtrsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_add->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_add->getForwardChangeCompleted());
+ EXPECT_FALSE(name_add->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_add->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_add->getNextEvent());
+}
+
+
+}
diff --git a/src/bin/d2/tests/nc_remove_unittests.cc b/src/bin/d2/tests/nc_remove_unittests.cc
new file mode 100644
index 0000000..37efad6
--- /dev/null
+++ b/src/bin/d2/tests/nc_remove_unittests.cc
@@ -0,0 +1,1872 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_cfg_mgr.h>
+#include <d2/d2_cfg_mgr.h>
+#include <d2/nc_remove.h>
+#include <dns/messagerenderer.h>
+#include <nc_test_utils.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Test class derived from NameRemoveTransaction to provide visiblity
+// to protected methods.
+class NameRemoveStub : public NameRemoveTransaction {
+public:
+ NameRemoveStub(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr& forward_domain,
+ DdnsDomainPtr& reverse_domain)
+ : NameRemoveTransaction(io_service, ncr, forward_domain,
+ reverse_domain),
+ simulate_send_exception_(false),
+ simulate_build_request_exception_(false) {
+ }
+
+ virtual ~NameRemoveStub() {
+ }
+
+ /// @brief Simulates sending update requests to the DNS server
+ ///
+ /// This method simulates the initiation of an asynchronous send of
+ /// a DNS update request. It overrides the actual sendUpdate method in
+ /// the base class, thus avoiding an actual send, yet still increments
+ /// the update attempt count and posts a next event of NOP_EVT.
+ ///
+ /// It will also simulate an exception-based failure of sendUpdate, if
+ /// the simulate_send_exception_ flag is true.
+ ///
+ /// @param use_tsig_ Parameter is unused, but present in the base class
+ /// method.
+ ///
+ virtual void sendUpdate(bool /* use_tsig_ = false */) {
+ if (simulate_send_exception_) {
+ // Make the flag a one-shot by resetting it.
+ simulate_send_exception_ = false;
+ // Transition to failed.
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ return;
+ }
+
+ // Update send attempt count and post a NOP_EVT.
+ setUpdateAttempts(getUpdateAttempts() + 1);
+ postNextEvent(StateModel::NOP_EVT);
+ }
+
+ /// @brief Prepares the initial D2UpdateMessage
+ ///
+ /// This method overrides the NameChangeTransactio implementation to
+ /// provide the ability to simulate an exception throw in the build
+ /// request logic.
+ /// If the one-shot flag, simulate_build_request_exception_ is true,
+ /// this method will throw an exception, otherwise it will invoke the
+ /// base class method, providing normal functionality.
+ ///
+ /// For parameter description see the NameChangeTransaction implementation.
+ virtual D2UpdateMessagePtr prepNewRequest(DdnsDomainPtr domain) {
+ if (simulate_build_request_exception_) {
+ simulate_build_request_exception_ = false;
+ isc_throw (NameRemoveTransactionError,
+ "Simulated build requests exception");
+ }
+
+ return (NameChangeTransaction::prepNewRequest(domain));
+ }
+
+ /// @brief Simulates receiving a response
+ ///
+ /// This method simulates the completion of a DNSClient send. This allows
+ /// the state handler logic devoted to dealing with IO completion to be
+ /// fully exercised without requiring any actual IO. The two primary
+ /// pieces of information gleaned from IO completion are the DNSClient
+ /// status which indicates whether or not the IO exchange was successful
+ /// and the rcode, which indicates the server's reaction to the request.
+ ///
+ /// This method updates the transaction's DNS status value to that of the
+ /// given parameter, and then constructs and DNS update response message
+ /// with the given rcode value. To complete the simulation it then posts
+ /// a next event of IO_COMPLETED_EVT.
+ ///
+ /// @param status simulated DNSClient status
+ /// @param rcode simulated server response code
+ void fakeResponse(const DNSClient::Status& status,
+ const dns::Rcode& rcode) {
+ // Set the DNS update status. This is normally set in
+ // DNSClient IO completion handler.
+ setDnsUpdateStatus(status);
+
+ // Construct an empty message with the given Rcode.
+ D2UpdateMessagePtr msg(new D2UpdateMessage(D2UpdateMessage::OUTBOUND));
+ msg->setRcode(rcode);
+
+ // Set the update response to the message.
+ setDnsUpdateResponse(msg);
+
+ // Post the IO completion event.
+ postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+ }
+
+ /// @brief Selects the first forward server.
+ /// Some state handlers require a server to have been selected.
+ /// This selects a server without going through the state
+ /// transition(s) to do so.
+ bool selectFwdServer() {
+ if (getForwardDomain()) {
+ initServerSelection(getForwardDomain());
+ selectNextServer();
+ return (getCurrentServer());
+ }
+
+ return (false);
+ }
+
+ /// @brief Selects the first reverse server.
+ /// Some state handlers require a server to have been selected.
+ /// This selects a server without going through the state
+ /// transition(s) to do so.
+ bool selectRevServer() {
+ if (getReverseDomain()) {
+ initServerSelection(getReverseDomain());
+ selectNextServer();
+ return (getCurrentServer());
+ }
+
+ return (false);
+ }
+
+ /// @brief One-shot flag which will simulate sendUpdate failure if true.
+ bool simulate_send_exception_;
+
+ /// @brief One-shot flag which will simulate an exception when sendUpdate
+ /// failure if true.
+ bool simulate_build_request_exception_;
+
+ using StateModel::postNextEvent;
+ using StateModel::setState;
+ using StateModel::initDictionaries;
+ using NameRemoveTransaction::defineEvents;
+ using NameRemoveTransaction::verifyEvents;
+ using NameRemoveTransaction::defineStates;
+ using NameRemoveTransaction::verifyStates;
+ using NameRemoveTransaction::readyHandler;
+ using NameRemoveTransaction::selectingFwdServerHandler;
+ using NameRemoveTransaction::getCurrentServer;
+ using NameRemoveTransaction::removingFwdAddrsHandler;
+ using NameRemoveTransaction::setDnsUpdateStatus;
+ using NameRemoveTransaction::removingFwdRRsHandler;
+ using NameRemoveTransaction::selectingRevServerHandler;
+ using NameRemoveTransaction::removingRevPtrsHandler;
+ using NameRemoveTransaction::processRemoveOkHandler;
+ using NameRemoveTransaction::processRemoveFailedHandler;
+ using NameRemoveTransaction::buildRemoveFwdAddressRequest;
+ using NameRemoveTransaction::buildRemoveFwdRRsRequest;
+ using NameRemoveTransaction::buildRemoveRevPtrsRequest;
+};
+
+typedef boost::shared_ptr<NameRemoveStub> NameRemoveStubPtr;
+
+/// @brief Test fixture for testing NameRemoveTransaction
+///
+/// Note this class uses NameRemoveStub class to exercise non-public
+/// aspects of NameRemoveTransaction.
+class NameRemoveTransactionTest : public TransactionTest {
+public:
+ NameRemoveTransactionTest() {
+ }
+
+ virtual ~NameRemoveTransactionTest() {
+ }
+
+ /// @brief Creates a transaction which requests an IPv4 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv4 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_mask determines which change directions are requested
+ NameRemoveStubPtr makeTransaction4(int change_mask) {
+ // Creates IPv4 remove request, forward, and reverse domains.
+ setupForIPv4Transaction(dhcp_ddns::CHG_REMOVE, change_mask);
+
+ // Now create the test transaction as would occur in update manager.
+ return (NameRemoveStubPtr(new NameRemoveStub(io_service_, ncr_,
+ forward_domain_,
+ reverse_domain_)));
+ }
+
+ /// @brief Creates a transaction which requests an IPv6 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv6 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_mask determines which change directions are requested
+ NameRemoveStubPtr makeTransaction6(int change_mask) {
+ // Creates IPv6 remove request, forward, and reverse domains.
+ setupForIPv6Transaction(dhcp_ddns::CHG_REMOVE, change_mask);
+
+ // Now create the test transaction as would occur in update manager.
+ return (NameRemoveStubPtr(new NameRemoveStub(io_service_, ncr_,
+ forward_domain_,
+ reverse_domain_)));
+ }
+
+ /// @brief Create a test transaction at a known point in the state model.
+ ///
+ /// Method prepares a new test transaction and sets its state and next
+ /// event values to those given. This makes the transaction appear to
+ /// be at that point in the state model without having to transition it
+ /// through prerequisite states. It also provides the ability to set
+ /// which change directions are requested: forward change only, reverse
+ /// change only, or both.
+ ///
+ /// @param state value to set as the current state
+ /// @param event value to post as the next event
+ /// @param change_mask determines which change directions are requested
+ /// @param family selects between an IPv4 (AF_INET) and IPv6 (AF_INET6)
+ /// transaction.
+ NameRemoveStubPtr prepHandlerTest(unsigned int state, unsigned int event,
+ unsigned int change_mask
+ = FWD_AND_REV_CHG,
+ short family = AF_INET) {
+ NameRemoveStubPtr name_remove = (family == AF_INET ?
+ makeTransaction4(change_mask) :
+ makeTransaction6(change_mask));
+ name_remove->initDictionaries();
+ name_remove->postNextEvent(event);
+ name_remove->setState(state);
+ return (name_remove);
+ }
+
+};
+
+/// @brief Tests NameRemoveTransaction construction.
+/// This test verifies that:
+/// 1. Construction with invalid type of request
+/// 2. Valid construction functions properly
+TEST(NameRemoveTransaction, construction) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : true , "
+ " \"fqdn\" : \"example.com.\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ dhcp_ddns::NameChangeRequestPtr ncr;
+ DnsServerInfoStoragePtr servers;
+ DdnsDomainPtr forward_domain;
+ DdnsDomainPtr reverse_domain;
+ DdnsDomainPtr empty_domain;
+
+ ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
+ ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
+ ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+
+ // Verify that construction with wrong change type fails.
+ EXPECT_THROW(NameRemoveTransaction(io_service, ncr,
+ forward_domain, reverse_domain),
+ NameRemoveTransactionError);
+
+ // Verify that a valid construction attempt works.
+ ncr->setChangeType(isc::dhcp_ddns::CHG_REMOVE);
+ EXPECT_NO_THROW(NameRemoveTransaction(io_service, ncr,
+ forward_domain, reverse_domain));
+}
+
+/// @brief Tests event and state dictionary construction and verification.
+TEST_F(NameRemoveTransactionTest, dictionaryCheck) {
+ NameRemoveStubPtr name_remove;
+ ASSERT_NO_THROW(name_remove = makeTransaction4(FWD_AND_REV_CHG));
+ // Verify that the event and state dictionary validation fails prior
+ // dictionary construction.
+ ASSERT_THROW(name_remove->verifyEvents(), StateModelError);
+ ASSERT_THROW(name_remove->verifyStates(), StateModelError);
+
+ // Construct both dictionaries.
+ ASSERT_NO_THROW(name_remove->defineEvents());
+ ASSERT_NO_THROW(name_remove->defineStates());
+
+ // Verify both event and state dictionaries now pass validation.
+ ASSERT_NO_THROW(name_remove->verifyEvents());
+ ASSERT_NO_THROW(name_remove->verifyStates());
+}
+
+
+/// @brief Tests construction of a DNS update request for removing forward
+/// DNS address RRs.
+TEST_F(NameRemoveTransactionTest, buildRemoveFwdAddressRequest) {
+ // Create a IPv4 forward add transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameRemoveStubPtr name_remove;
+ ASSERT_NO_THROW(name_remove = makeTransaction4(FORWARD_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveFwdAddressRequest());
+ checkRemoveFwdAddressRequest(*name_remove);
+
+ // Create a IPv6 forward add transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_remove = makeTransaction6(FORWARD_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveFwdAddressRequest());
+ checkRemoveFwdAddressRequest(*name_remove);
+}
+
+/// @brief Tests construction of a DNS update request for removing forward
+/// dns RR entries.
+TEST_F(NameRemoveTransactionTest, buildRemoveFwdRRsRequest) {
+ // Create a IPv4 forward replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameRemoveStubPtr name_remove;
+ ASSERT_NO_THROW(name_remove = makeTransaction4(FORWARD_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveFwdRRsRequest());
+ checkRemoveFwdRRsRequest(*name_remove);
+
+ // Create a IPv6 forward replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_remove = makeTransaction6(FORWARD_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveFwdRRsRequest());
+ checkRemoveFwdRRsRequest(*name_remove);
+}
+
+/// @brief Tests the construction of a DNS update request for removing a
+/// reverse dns entry.
+TEST_F(NameRemoveTransactionTest, buildRemoveRevPtrsRequest) {
+ // Create a IPv4 reverse replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ NameRemoveStubPtr name_remove;
+ ASSERT_NO_THROW(name_remove = makeTransaction4(REVERSE_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveRevPtrsRequest());
+ checkRemoveRevPtrsRequest(*name_remove);
+
+ // Create a IPv6 reverse replace transaction.
+ // Verify the request builds without error.
+ // and then verify the request contents.
+ ASSERT_NO_THROW(name_remove = makeTransaction6(REVERSE_CHG));
+ ASSERT_NO_THROW(name_remove->buildRemoveRevPtrsRequest());
+ checkRemoveRevPtrsRequest(*name_remove);
+}
+
+// Tests the readyHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is START_EVT and request includes only a forward change
+// 2. Posted event is START_EVT and request includes both a forward and a
+// reverse change
+// 3. Posted event is START_EVT and request includes only a reverse change
+// 4. Posted event is invalid
+//
+TEST_F(NameRemoveTransactionTest, readyHandler) {
+ NameRemoveStubPtr name_remove;
+
+ // Create a transaction which includes only a forward change.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, FORWARD_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_remove->readyHandler());
+
+ // Verify that a request requiring only a forward change, transitions to
+ // selecting a forward server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_FWD_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_remove->getNextEvent());
+
+ // Create a transaction which includes both a forward and a reverse change.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, FWD_AND_REV_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_remove->readyHandler());
+
+ // Verify that a request requiring both forward and reverse, starts with
+ // the forward change by transitioning to selecting a forward server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_FWD_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_remove->getNextEvent());
+
+ // Create and prep a reverse only transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::START_EVT, REVERSE_CHG));
+ // Run readyHandler.
+ EXPECT_NO_THROW(name_remove->readyHandler());
+
+ // Verify that a request requiring only a reverse change, transitions to
+ // selecting a reverse server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_remove->getNextEvent());
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::READY_ST,
+ StateModel::NOP_EVT));
+
+ // Running the readyHandler should throw.
+ EXPECT_THROW(name_remove->readyHandler(), NameRemoveTransactionError);
+}
+
+
+// Tests the selectingFwdServerHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is SELECT_SERVER_EVT
+// 2. Posted event is SERVER_IO_ERROR_EVT
+// 3. Posted event is invalid
+//
+TEST_F(NameRemoveTransactionTest, selectingFwdServerHandler) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_FWD_SERVER_ST,
+ NameChangeTransaction::SELECT_SERVER_EVT));
+
+ // Call selectingFwdServerHandler enough times to select all of the
+ // servers in it's current domain. The first time, it will be with
+ // next event of SELECT_SERVER_EVT. Thereafter it will be with a next
+ // event of SERVER_IO_ERROR_EVT.
+ int num_servers = name_remove->getForwardDomain()->getServers()->size();
+ for (int i = 0; i < num_servers; ++i) {
+ // Run selectingFwdServerHandler.
+ ASSERT_NO_THROW(name_remove->selectingFwdServerHandler())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that a server was selected.
+ ASSERT_TRUE(name_remove->getCurrentServer())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Verify that we transitioned correctly.
+ ASSERT_EQ(NameRemoveTransaction::REMOVING_FWD_ADDRS_ST,
+ name_remove->getCurrState())
+ << " num_servers: " << num_servers << " selections: " << i;
+ ASSERT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Post a server IO error event. This simulates an IO error occuring
+ // and a need to select the new server.
+ ASSERT_NO_THROW(name_remove->postNextEvent(NameChangeTransaction::
+ SERVER_IO_ERROR_EVT))
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+ }
+
+ // We should have exhausted the list of servers. Processing another
+ // SERVER_IO_ERROR_EVT should transition us to failure.
+ EXPECT_NO_THROW(name_remove->selectingFwdServerHandler());
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NO_MORE_SERVERS_EVT,
+ name_remove->getNextEvent());
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_FWD_SERVER_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->selectingFwdServerHandler(),
+ NameRemoveTransactionError);
+}
+
+// ************************ addingFwdAddrHandler Tests *****************
+
+// Tests that removingFwdAddrsHandler rejects invalid events.
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_InvalidEvent) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->removingFwdAddrsHandler(),
+ NameRemoveTransactionError);
+}
+
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_FwdOnlyOK) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_remove->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Run removingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkRemoveFwdAddressRequest(*name_remove);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_ADDRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NOP_EVT,
+ name_remove->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run removingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should both still be false, as we are only partly
+ // done with forward updates.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since we succeeded, we should now attempt to remove any remaining
+ // forward RRs.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_RRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameRemoveTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests addingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates FQDN is not in use.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_FqdnNotInUse) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Run removingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Simulate receiving a FQDN not in use response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+
+ // Run removingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should both still be false, as we are only partly
+ // done with forward updates.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // There was no address RR to remove, but we will still make sure there
+ // are no other RRs for this FQDN.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_RRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameRemoveTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_OtherRcode) {
+ NameRemoveStubPtr name_remove;
+
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Run removingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error or FQDN not in use is failure. Arbitrarily choosing
+ // refused.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run removingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verify that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_Timeout) {
+ NameRemoveStubPtr name_remove;
+
+ // Create and prep a transaction, poised to run the handler.
+ // The log message issued when this test succeeds, displays the
+ // selected server, so we need to select a server before running this
+ // test.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_remove->getDnsUpdateRequest();
+
+ // Run removingFwdAddrsHandler to send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_remove->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time around we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_remove->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_ADDRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameRemoveTransaction::SELECTING_FWD_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+}
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_InvalidResponse) {
+ NameRemoveStubPtr name_remove;
+
+ // Create and prep a transaction, poised to run the handler.
+ // The log message issued when this test succeeds, displays the
+ // selected server, so we need to select a server before running this
+ // test.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ // Run removingFwdAddrsHandler to construct send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Simulate a corrupt server response.
+ name_remove->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingFwdAddrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_ADDRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameRemoveTransaction::SELECTING_FWD_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+
+}
+
+// ************************ removingFwdRRsHandler Tests *****************
+
+// Tests that removingFwdRRsHandler rejects invalid events.
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_InvalidEvent) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_RRS_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->removingFwdRRsHandler(),
+ NameRemoveTransactionError);
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is UPDATE_OK_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_FwdOnlyOK) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FORWARD_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_remove->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkRemoveFwdRRsRequest(*name_remove);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_ADDRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NOP_EVT,
+ name_remove->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Forward completion should be true, reverse should be false.
+ EXPECT_TRUE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_FwdOnlyOK2) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Simulate receiving a successful update response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Forward completion should be true, reverse should be false.
+ EXPECT_TRUE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is UPDATE_OK_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_FwdAndRevOK) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FWD_AND_REV_CHG));
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Simulate receiving a successful update response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Forward change completion should be true, reverse flag should be false.
+ EXPECT_TRUE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since the request also includes a reverse change we should
+ // be poised to start it. Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SELECT_SERVER_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is UPDATE_OK_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the FQDN is NOT in use.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_FqdnNotInUse) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FORWARD_CHG));
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Simulate receiving a FQDN not in use response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Forwad completion flag should be true, reverse should still be false.
+ EXPECT_TRUE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // The FQDN is no longer in use, RFC is unclear about this,
+ // but we will treat this as success.
+ // Since it is a forward only change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_OtherRcode) {
+ NameRemoveStubPtr name_remove;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error is failure (we are also treating FQDN not in use is
+ // success). Arbitrarily choosing refused.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verifiy that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is UPDATE_OK_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_Timeout) {
+ NameRemoveStubPtr name_remove;
+
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_RRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_remove->getDnsUpdateRequest();
+
+ // Run removingFwdRRsHandler to send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_remove->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time around we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_remove->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_RRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted.
+ // We should abandon the transaction.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes a forward and reverse change.
+// Initial posted event is UPDATE_OK_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_InvalidResponse) {
+ NameRemoveStubPtr name_remove;
+
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_RRS_ST,
+ NameChangeTransaction::
+ UPDATE_OK_EVT, FWD_AND_REV_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectFwdServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_remove->getDnsUpdateRequest();
+
+ // Run removingFwdRRsHandler to send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_remove->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time around we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a corrupt server response.
+ name_remove->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingFwdRRsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_FWD_RRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted.
+ // We should abandon the transaction.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+}
+
+
+// Tests the selectingRevServerHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is SELECT_SERVER_EVT
+// 2. Posted event is SERVER_IO_ERROR_EVT
+// 3. Posted event is invalid
+//
+TEST_F(NameRemoveTransactionTest, selectingRevServerHandler) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_REV_SERVER_ST,
+ NameChangeTransaction::SELECT_SERVER_EVT));
+
+ // Call selectingRevServerHandler enough times to select all of the
+ // servers in it's current domain. The first time, it will be with
+ // next event of SELECT_SERVER_EVT. Thereafter it will be with a next
+ // event of SERVER_IO_ERROR_EVT.
+ int num_servers = name_remove->getReverseDomain()->getServers()->size();
+ for (int i = 0; i < num_servers; ++i) {
+ // Run selectingRevServerHandler.
+ ASSERT_NO_THROW(name_remove->selectingRevServerHandler())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that a server was selected.
+ ASSERT_TRUE(name_remove->getCurrentServer())
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+
+ // Verify that we transitioned correctly.
+ ASSERT_EQ(NameRemoveTransaction::REMOVING_REV_PTRS_ST,
+ name_remove->getCurrState())
+ << " num_servers: " << num_servers << " selections: " << i;
+ ASSERT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent())
+ << " num_servers: " << num_servers << " selections: " << i;
+
+ // Post a server IO error event. This simulates an IO error occuring
+ // and a need to select the new server.
+ ASSERT_NO_THROW(name_remove->postNextEvent(NameChangeTransaction::
+ SERVER_IO_ERROR_EVT))
+ << " num_servers: " << num_servers
+ << " selections: " << i;
+ }
+
+ // We should have exhausted the list of servers. Processing another
+ // SERVER_IO_ERROR_EVT should transition us to failure.
+ EXPECT_NO_THROW(name_remove->selectingRevServerHandler());
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::NO_MORE_SERVERS_EVT,
+ name_remove->getNextEvent());
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ SELECTING_REV_SERVER_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->selectingRevServerHandler(),
+ NameRemoveTransactionError);
+}
+
+//************************** removingRevPtrsHandler tests *****************
+
+// Tests that removingRevPtrsHandler rejects invalid events.
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_InvalidEvent) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler but with
+ // an invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ StateModel::NOP_EVT));
+
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->removingRevPtrsHandler(),
+ NameRemoveTransactionError);
+}
+
+// Tests removingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates successful update.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_RevOnlyOK) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_remove->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Run removingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkRemoveRevPtrsRequest(*name_remove);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_REV_PTRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(StateModel::NOP_EVT,
+ name_remove->getNextEvent());
+
+ // Simulate receiving a successful update response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NOERROR());
+
+ // Run removingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Forward completion should be false, reverse should be true.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_TRUE(name_remove->getReverseChangeCompleted());
+
+ // Since it is a reverse change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates FQDN is NOT in use.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_FqdnNotInUse) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Should not be an update message yet.
+ D2UpdateMessagePtr update_msg = name_remove->getDnsUpdateRequest();
+ ASSERT_FALSE(update_msg);
+
+ // At this point completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Run removingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Verify that an update message was constructed properly.
+ checkRemoveRevPtrsRequest(*name_remove);
+
+ // Verify that we are still in this state and next event is NOP_EVT.
+ // This indicates we "sent" the message and are waiting for IO completion.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_REV_PTRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(StateModel::NOP_EVT,
+ name_remove->getNextEvent());
+
+ // Simulate receiving a FQDN not in use response.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::NXDOMAIN());
+
+ // Run removingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Forward completion should be false, reverse should be true.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_TRUE(name_remove->getReverseChangeCompleted());
+
+ // Since it is a reverse change, we should be done.
+ // Verify that we transitioned correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_OK_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent without error.
+// A server response is received which indicates the update was rejected.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_OtherRcode) {
+ NameRemoveStubPtr name_remove;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectRevServer());
+
+ // Run removingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Simulate receiving server rejection response. Per RFC, anything other
+ // than no error is failure. Arbitrarily choosing refused.
+ name_remove->fakeResponse(DNSClient::SUCCESS, dns::Rcode::REFUSED());
+
+ // Run removingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Completion flags should still be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // We should have failed the transaction. Verify that we transitioned
+ // correctly.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request send times out MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_Timeout) {
+ NameRemoveStubPtr name_remove;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectRevServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_remove->getDnsUpdateRequest();
+
+ // Run removingRevPtrsHandler to send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_remove->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time around we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server IO timeout.
+ name_remove->setDnsUpdateStatus(DNSClient::TIMEOUT);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_REV_PTRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+}
+
+
+// Tests removingRevPtrsHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The update request is sent but a corrupt response is received, this occurs
+// MAX_UPDATE_TRIES_PER_SERVER times.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_CorruptResponse) {
+ NameRemoveStubPtr name_remove;
+ // Create the transaction.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ // Select a server to satisfy log statements.
+ ASSERT_TRUE(name_remove->selectRevServer());
+
+ // Verify that we can make maximum number of update attempts permitted
+ // and then transition to selecting a new server.
+ int max_tries = NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER;
+ for (int i = 1; i <= max_tries; ++i) {
+ const D2UpdateMessagePtr prev_msg = name_remove->getDnsUpdateRequest();
+
+ // Run removingRevPtrsHandler to send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ const D2UpdateMessagePtr curr_msg = name_remove->getDnsUpdateRequest();
+ if (i == 1) {
+ // First time around we should build the message.
+ EXPECT_FALSE(prev_msg);
+ EXPECT_TRUE(curr_msg);
+ } else {
+ // Subsequent passes should reuse the request. We are only
+ // looking to check that we have not replaced the pointer value
+ // with a new pointer. This tests the on_entry() logic which
+ // clears the request ONLY upon initial entry into the state.
+ EXPECT_TRUE(prev_msg == curr_msg);
+ }
+
+ // Simulate a server corrupt response.
+ name_remove->setDnsUpdateStatus(DNSClient::INVALID_RESPONSE);
+ name_remove->postNextEvent(NameChangeTransaction::IO_COMPLETED_EVT);
+
+ // Run removingRevPtrsHandler again to process the response.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ if (i < max_tries) {
+ // We should be ready to try again.
+ EXPECT_EQ(NameRemoveTransaction::REMOVING_REV_PTRS_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_remove->getNextEvent());
+ } else {
+ // Server retries should be exhausted, time for a new server.
+ EXPECT_EQ(NameChangeTransaction::SELECTING_REV_SERVER_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_remove->getNextEvent());
+ }
+ }
+}
+
+// Tests the processRemoveOkHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is UPDATE_OK_EVT
+// 2. Posted event is invalid
+//
+TEST_F(NameRemoveTransactionTest, processRemoveOkHandler) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ NameChangeTransaction::UPDATE_OK_EVT));
+ // Run processRemoveOkHandler.
+ EXPECT_NO_THROW(name_remove->processRemoveOkHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_COMPLETED, name_remove->getNcrStatus());
+
+ // Verify that the model has ended.
+ EXPECT_EQ(StateModel::END_ST, name_remove->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_remove->getNextEvent());
+
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::PROCESS_TRANS_OK_ST,
+ StateModel::NOP_EVT));
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->processRemoveOkHandler(),
+ NameRemoveTransactionError);
+}
+
+// Tests the processRemoveFailedHandler functionality.
+// It verifies behavior for the following scenarios:
+//
+// 1. Posted event is UPDATE_FAILED_EVT
+// 2. Posted event is invalid
+//
+TEST_F(NameRemoveTransactionTest, processRemoveFailedHandler) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ NameChangeTransaction::UPDATE_FAILED_EVT));
+ // Run processRemoveFailedHandler.
+ EXPECT_NO_THROW(name_remove->processRemoveFailedHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_remove->getNcrStatus());
+
+ // Verify that the model has ended. (Remember, the transaction failed NOT
+ // the model. The model should have ended normally.)
+ EXPECT_EQ(StateModel::END_ST, name_remove->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_remove->getNextEvent());
+
+
+ // Create and prep transaction, poised to run the handler but with an
+ // invalid event.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ StateModel::NOP_EVT));
+ // Running the handler should throw.
+ EXPECT_THROW(name_remove->processRemoveFailedHandler(),
+ NameRemoveTransactionError);
+}
+
+// Tests the processRemoveFailedHandler functionality.
+// It verifies behavior for posted event of NO_MORE_SERVERS_EVT.
+TEST_F(NameRemoveTransactionTest, processRemoveFailedHandler_NoMoreServers) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ NameChangeTransaction::
+ NO_MORE_SERVERS_EVT));
+
+ // Run processRemoveFailedHandler.
+ EXPECT_NO_THROW(name_remove->processRemoveFailedHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_remove->getNcrStatus());
+
+ // Verify that the model has ended. (Remember, the transaction failed NOT
+ // the model. The model should have ended normally.)
+ EXPECT_EQ(StateModel::END_ST, name_remove->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_remove->getNextEvent());
+}
+
+// Tests the processRemoveFailedHandler functionality.
+// It verifies behavior for posted event of SERVER_IO_ERROR_EVT.
+TEST_F(NameRemoveTransactionTest, processRemoveFailedHandler_ServerIOError) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameChangeTransaction::
+ PROCESS_TRANS_FAILED_ST,
+ NameChangeTransaction::
+ SERVER_IO_ERROR_EVT));
+
+ // Run processRemoveFailedHandler.
+ EXPECT_NO_THROW(name_remove->processRemoveFailedHandler());
+
+ // Verify that a server was selected.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_remove->getNcrStatus());
+
+ // Verify that the model has ended. (Remember, the transaction failed NOT
+ // the model. The model should have ended normally.)
+ EXPECT_EQ(StateModel::END_ST, name_remove->getCurrState());
+ EXPECT_EQ(StateModel::END_EVT, name_remove->getNextEvent());
+}
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdAddrsHandler_sendUpdateException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ name_remove->simulate_send_exception_ = true;
+
+ // Run removingFwdAddrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest, removingFwdRRsHandler_SendUpdateException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_RRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ name_remove->simulate_send_exception_ = true;
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingRevPtrHandler with the following scenario:
+//
+// The request includes only a reverse change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The send update request fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest, removingRevPtrsHandler_SendUpdateException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, REVERSE_CHG));
+
+ name_remove->simulate_send_exception_ = true;
+
+ // Run removingRevPtrsHandler to construct and send the request.
+ EXPECT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdAddrsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest,
+ removingFwdAddrsHandler_BuildRequestException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_ADDRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_remove->simulate_build_request_exception_ = true;
+
+ // Run removingFwdAddrsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_remove->removingFwdAddrsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_remove->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingFwdRRsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest,
+ removingFwdRRsHandler_BuildRequestException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_FWD_RRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_remove->simulate_build_request_exception_ = true;
+
+ // Run removingFwdRRsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_remove->removingFwdRRsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_remove->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+// Tests removingRevPTRsHandler with the following scenario:
+//
+// The request includes only a forward change.
+// Initial posted event is SERVER_SELECTED_EVT.
+// The request build fails due to an unexpected exception.
+//
+TEST_F(NameRemoveTransactionTest,
+ removingRevPTRsHandler_BuildRequestException) {
+ NameRemoveStubPtr name_remove;
+ // Create and prep a transaction, poised to run the handler.
+ ASSERT_NO_THROW(name_remove =
+ prepHandlerTest(NameRemoveTransaction::
+ REMOVING_REV_PTRS_ST,
+ NameChangeTransaction::
+ SERVER_SELECTED_EVT, FORWARD_CHG));
+
+ // Set the one-shot exception simulation flag.
+ name_remove->simulate_build_request_exception_ = true;
+
+ // Run removingRevPtrsHandler to construct and send the request.
+ // This should fail with a build request throw which should be caught
+ // in the state handler.
+ ASSERT_NO_THROW(name_remove->removingRevPtrsHandler());
+
+ // Verify we did not attempt to send anything.
+ EXPECT_EQ(0, name_remove->getUpdateAttempts());
+
+ // Completion flags should be false.
+ EXPECT_FALSE(name_remove->getForwardChangeCompleted());
+ EXPECT_FALSE(name_remove->getReverseChangeCompleted());
+
+ // Since IO exceptions should be gracefully handled, any that occur
+ // are unanticipated, and deemed unrecoverable, so the transaction should
+ // be transitioned to failure.
+ EXPECT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_remove->getCurrState());
+ EXPECT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_remove->getNextEvent());
+}
+
+}
diff --git a/src/bin/d2/tests/nc_test_utils.cc b/src/bin/d2/tests/nc_test_utils.cc
new file mode 100644
index 0000000..b65b9e4
--- /dev/null
+++ b/src/bin/d2/tests/nc_test_utils.cc
@@ -0,0 +1,702 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/d2_cfg_mgr.h>
+#include <dns/opcode.h>
+#include <dns/messagerenderer.h>
+#include <nc_test_utils.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace isc {
+namespace d2 {
+
+const char* TEST_DNS_SERVER_IP = "127.0.0.1";
+size_t TEST_DNS_SERVER_PORT = 5301;
+
+const bool HAS_RDATA = true;
+const bool NO_RDATA = false;
+
+//*************************** FauxServer class ***********************
+
+FauxServer::FauxServer(asiolink::IOService& io_service,
+ asiolink::IOAddress& address, size_t port)
+ :io_service_(io_service), address_(address), port_(port),
+ server_socket_(), receive_pending_(false), perpetual_receive_(true) {
+
+ server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
+ asio::ip::udp::v4()));
+ server_socket_->set_option(asio::socket_base::reuse_address(true));
+ server_socket_->bind(asio::ip::udp::endpoint(address_.getAddress(), port_));
+}
+
+FauxServer::FauxServer(asiolink::IOService& io_service,
+ DnsServerInfo& server)
+ :io_service_(io_service), address_(server.getIpAddress()),
+ port_(server.getPort()), server_socket_(), receive_pending_(false),
+ perpetual_receive_(true) {
+ server_socket_.reset(new asio::ip::udp::socket(io_service_.get_io_service(),
+ asio::ip::udp::v4()));
+ server_socket_->set_option(asio::socket_base::reuse_address(true));
+ server_socket_->bind(asio::ip::udp::endpoint(address_.getAddress(), port_));
+}
+
+
+FauxServer::~FauxServer() {
+}
+
+void
+FauxServer::receive (const ResponseMode& response_mode,
+ const dns::Rcode& response_rcode) {
+ if (receive_pending_) {
+ return;
+ }
+
+ receive_pending_ = true;
+ server_socket_->async_receive_from(asio::buffer(receive_buffer_,
+ sizeof(receive_buffer_)),
+ remote_,
+ boost::bind(&FauxServer::requestHandler,
+ this, _1, _2,
+ response_mode,
+ response_rcode));
+}
+
+void
+FauxServer::requestHandler(const asio::error_code& error,
+ std::size_t bytes_recvd,
+ const ResponseMode& response_mode,
+ const dns::Rcode& response_rcode) {
+ // If we encountered an error or received no data then fail.
+ // We expect the client to send good requests.
+ if (error.value() != 0 || bytes_recvd < 1) {
+ ADD_FAILURE() << "FauxServer receive failed" << error.message();
+ receive_pending_ = false;
+ return;
+ }
+
+ // We have a successfully received data. We need to turn it into
+ // a request in order to build a proper response.
+ // Note D2UpdateMessage is geared towards making requests and
+ // reading responses. This is the opposite perspective so we have
+ // to a bit of roll-your-own here.
+ dns::Message request(dns::Message::PARSE);
+ util::InputBuffer request_buf(receive_buffer_, bytes_recvd);
+ try {
+ request.fromWire(request_buf);
+ } catch (const std::exception& ex) {
+ // If the request cannot be parsed, then fail the test.
+ // We expect the client to send good requests.
+ ADD_FAILURE() << "FauxServer request is corrupt:" << ex.what();
+ receive_pending_ = false;
+ return;
+ }
+
+ // The request parsed OK, so let's build a response.
+ // We must use the QID we received in the response or IOFetch will
+ // toss the response out, resulting in eventual timeout.
+ // We fill in the zone with data we know is from the "server".
+ dns::Message response(dns::Message::RENDER);
+ response.setQid(request.getQid());
+ dns::Question question(dns::Name("response.example.com"),
+ dns::RRClass::ANY(), dns::RRType::SOA());
+ response.addQuestion(question);
+ response.setOpcode(dns::Opcode(dns::Opcode::UPDATE_CODE));
+ response.setHeaderFlag(dns::Message::HEADERFLAG_QR, true);
+
+ // Set the response Rcode to value passed in, default is NOERROR.
+ response.setRcode(response_rcode);
+
+ // Render the response to a buffer.
+ dns::MessageRenderer renderer;
+ util::OutputBuffer response_buf(TEST_MSG_MAX);
+ renderer.setBuffer(&response_buf);
+ response.toWire(renderer);
+
+ // If mode is to ship garbage, then stomp on part of the rendered
+ // message.
+ if (response_mode == CORRUPT_RESP) {
+ response_buf.writeUint16At(0xFFFF, 2);
+ }
+
+ // Ship the response via synchronous send.
+ try {
+ int cnt = server_socket_->send_to(asio::
+ buffer(response_buf.getData(),
+ response_buf.getLength()),
+ remote_);
+ // Make sure we sent what we expect to send.
+ if (cnt != response_buf.getLength()) {
+ ADD_FAILURE() << "FauxServer sent: " << cnt << " expected: "
+ << response_buf.getLength();
+ }
+ } catch (const std::exception& ex) {
+ ADD_FAILURE() << "FauxServer send failed: " << ex.what();
+ }
+
+ receive_pending_ = false;
+ if (perpetual_receive_) {
+ // Schedule the next receive
+ receive (response_mode, response_rcode);
+ }
+}
+
+
+//********************** TimedIO class ***********************
+
+TimedIO::TimedIO()
+ : io_service_(new isc::asiolink::IOService()),
+ timer_(*io_service_), run_time_(0) {
+}
+
+TimedIO::~TimedIO() {
+}
+
+int
+TimedIO::runTimedIO(int run_time) {
+ run_time_ = run_time;
+ int cnt = io_service_->get_io_service().poll();
+ if (cnt == 0) {
+ timer_.setup(boost::bind(&TimedIO::timesUp, this), run_time_);
+ cnt = io_service_->get_io_service().run_one();
+ timer_.cancel();
+ }
+
+ return (cnt);
+}
+
+void
+TimedIO::timesUp() {
+ io_service_->stop();
+ FAIL() << "Test Time: " << run_time_ << " expired";
+}
+
+//********************** TransactionTest class ***********************
+
+const unsigned int TransactionTest::FORWARD_CHG = 0x01;
+const unsigned int TransactionTest::REVERSE_CHG = 0x02;
+const unsigned int TransactionTest::FWD_AND_REV_CHG = REVERSE_CHG | FORWARD_CHG;
+
+TransactionTest::TransactionTest() : ncr_() {
+}
+
+TransactionTest::~TransactionTest() {
+}
+
+void
+TransactionTest::setupForIPv4Transaction(dhcp_ddns::NameChangeType chg_type,
+ int change_mask) {
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : true , "
+ " \"fqdn\" : \"my.forward.example.com.\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ // Create NameChangeRequest from JSON string.
+ ncr_ = dhcp_ddns::NameChangeRequest::fromJSON(msg_str);
+
+ // Set the change type.
+ ncr_->setChangeType(chg_type);
+
+ // If the change mask does not include a forward change clear the
+ // forward domain; otherwise create the domain and its servers.
+ if (!(change_mask & FORWARD_CHG)) {
+ ncr_->setForwardChange(false);
+ forward_domain_.reset();
+ } else {
+ // Create the forward domain and then its servers.
+ forward_domain_ = makeDomain("example.com.");
+ addDomainServer(forward_domain_, "forward.example.com",
+ "127.0.0.1", 5301);
+ addDomainServer(forward_domain_, "forward2.example.com",
+ "127.0.0.1", 5302);
+ }
+
+ // If the change mask does not include a reverse change clear the
+ // reverse domain; otherwise create the domain and its servers.
+ if (!(change_mask & REVERSE_CHG)) {
+ ncr_->setReverseChange(false);
+ reverse_domain_.reset();
+ } else {
+ // Create the reverse domain and its server.
+ reverse_domain_ = makeDomain("2.168.192.in.addr.arpa.");
+ addDomainServer(reverse_domain_, "reverse.example.com",
+ "127.0.0.1", 5301);
+ addDomainServer(reverse_domain_, "reverse2.example.com",
+ "127.0.0.1", 5302);
+ }
+}
+
+void
+TransactionTest::setupForIPv6Transaction(dhcp_ddns::NameChangeType chg_type,
+ int change_mask) {
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : true , "
+ " \"fqdn\" : \"my6.forward.example.com.\" , "
+ " \"ip_address\" : \"2001:1::100\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ // Create NameChangeRequest from JSON string.
+ ncr_ = makeNcrFromString(msg_str);
+
+ // Set the change type.
+ ncr_->setChangeType(chg_type);
+
+ // If the change mask does not include a forward change clear the
+ // forward domain; otherwise create the domain and its servers.
+ if (!(change_mask & FORWARD_CHG)) {
+ ncr_->setForwardChange(false);
+ forward_domain_.reset();
+ } else {
+ // Create the forward domain and then its servers.
+ forward_domain_ = makeDomain("example.com.");
+ addDomainServer(forward_domain_, "fwd6-server.example.com",
+ "::1", 5301);
+ addDomainServer(forward_domain_, "fwd6-server2.example.com",
+ "::1", 5302);
+ }
+
+ // If the change mask does not include a reverse change clear the
+ // reverse domain; otherwise create the domain and its servers.
+ if (!(change_mask & REVERSE_CHG)) {
+ ncr_->setReverseChange(false);
+ reverse_domain_.reset();
+ } else {
+ // Create the reverse domain and its server.
+ reverse_domain_ = makeDomain("1.2001.ip6.arpa.");
+ addDomainServer(reverse_domain_, "rev6-server.example.com",
+ "::1", 5301);
+ addDomainServer(reverse_domain_, "rev6-server2.example.com",
+ "::1", 5302);
+ }
+}
+
+
+//********************** Functions ****************************
+
+void
+checkRRCount(const D2UpdateMessagePtr& request,
+ D2UpdateMessage::UpdateMsgSection section, int count) {
+ dns::RRsetIterator rrset_it = request->beginSection(section);
+ dns::RRsetIterator rrset_end = request->endSection(section);
+
+ ASSERT_EQ(count, std::distance(rrset_it, rrset_end));
+}
+
+void
+checkZone(const D2UpdateMessagePtr& request, const std::string& exp_zone_name) {
+ // Verify the zone section info.
+ D2ZonePtr zone = request->getZone();
+ EXPECT_TRUE(zone);
+ EXPECT_EQ(exp_zone_name, zone->getName().toText());
+ EXPECT_EQ(dns::RRClass::IN().getCode(), zone->getClass().getCode());
+}
+
+void
+checkRR(dns::RRsetPtr rrset, const std::string& exp_name,
+ const dns::RRClass& exp_class, const dns::RRType& exp_type,
+ unsigned int exp_ttl, dhcp_ddns::NameChangeRequestPtr ncr,
+ bool has_rdata) {
+ // Verify the FQDN/DHCID RR fields.
+ EXPECT_EQ(exp_name, rrset->getName().toText());
+ EXPECT_EQ(exp_class.getCode(), rrset->getClass().getCode());
+ EXPECT_EQ(exp_type.getCode(), rrset->getType().getCode());
+ EXPECT_EQ(exp_ttl, rrset->getTTL().getValue());
+ if ((!has_rdata) ||
+ (exp_type == dns::RRType::ANY() || exp_class == dns::RRClass::ANY())) {
+ // ANY types do not have RData
+ ASSERT_EQ(0, rrset->getRdataCount());
+ return;
+ }
+
+ ASSERT_EQ(1, rrset->getRdataCount());
+ dns::RdataIteratorPtr rdata_it = rrset->getRdataIterator();
+ ASSERT_TRUE(rdata_it);
+
+ if ((exp_type == dns::RRType::A()) ||
+ (exp_type == dns::RRType::AAAA())) {
+ // should have lease rdata
+ EXPECT_EQ(ncr->getIpAddress(), rdata_it->getCurrent().toText());
+ } else if (exp_type == dns::RRType::PTR()) {
+ // should have PTR rdata
+ EXPECT_EQ(ncr->getFqdn(), rdata_it->getCurrent().toText());
+ } else if (exp_type == dns::RRType::DHCID()) {
+ // should have DHCID rdata
+ const std::vector<uint8_t>& ncr_dhcid = ncr->getDhcid().getBytes();
+ util::InputBuffer buffer(ncr_dhcid.data(), ncr_dhcid.size());
+ dns::rdata::in::DHCID rdata_ref(buffer, ncr_dhcid.size());
+ EXPECT_EQ(0, rdata_ref.compare(rdata_it->getCurrent()));
+ } else {
+ // we got a problem
+ FAIL();
+ }
+}
+
+dns::RRsetPtr
+getRRFromSection(const D2UpdateMessagePtr& request,
+ D2UpdateMessage::UpdateMsgSection section, int index) {
+ dns::RRsetIterator rrset_it = request->beginSection(section);
+ dns::RRsetIterator rrset_end = request->endSection(section);
+
+ if (std::distance(rrset_it, rrset_end) <= index) {
+ // Return an empty pointer if index is out of range.
+ return (dns::RRsetPtr());
+ }
+
+ std::advance(rrset_it, index);
+ return (*rrset_it);
+}
+
+dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str) {
+ return (dhcp_ddns::NameChangeRequest::fromJSON(ncr_str));
+}
+
+DdnsDomainPtr makeDomain(const std::string& zone_name,
+ const std::string& key_name) {
+ DdnsDomainPtr domain;
+ DnsServerInfoStoragePtr servers(new DnsServerInfoStorage());
+ domain.reset(new DdnsDomain(zone_name, key_name, servers));
+ return (domain);
+}
+
+void addDomainServer(DdnsDomainPtr& domain, const std::string& name,
+ const std::string& ip, const size_t port) {
+ DnsServerInfoPtr server(new DnsServerInfo(name, asiolink::IOAddress(ip),
+ port));
+ domain->getServers()->push_back(server);
+}
+
+// Verifies that the contents of the given transaction's DNS update request
+// is correct for adding a forward DNS entry
+void checkAddFwdAddressRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getForwardDomain()->getName();
+ std::string exp_fqdn = ncr->getFqdn();
+ const dns::RRType& exp_ip_rr_type = tran.getAddressRRType();
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify the PREREQUISITE SECTION
+ // Should be 1 which tests for FQDN does not exist.
+ dns::RRsetPtr rrset;
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 1);
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::NONE(), dns::RRType::ANY(), 0, ncr);
+
+ // Verify the UPDATE SECTION
+ // Should be 2 RRs: 1 to add the FQDN/IP and one to add the DHCID RR
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 2);
+
+ // First, Verify the FQDN/IP add RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), exp_ip_rr_type, 0, ncr);
+
+ // Now, verify the DHCID add RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 1));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), dns::RRType::DHCID(), 0, ncr);
+
+ // Verify there are no RRs in the ADDITIONAL Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_ADDITIONAL, 0);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+// Verifies that the contents of the given transaction's DNS update request
+// is correct for replacing a forward DNS entry
+void checkReplaceFwdAddressRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getForwardDomain()->getName();
+ std::string exp_fqdn = ncr->getFqdn();
+ const dns::RRType& exp_ip_rr_type = tran.getAddressRRType();
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify the PREREQUISITE SECTION
+ // Should be 2, 1 which tests for FQDN does exist and the other
+ // checks for a matching DHCID.
+ dns::RRsetPtr rrset;
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 2);
+
+ // Verify the FQDN test RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::ANY(), dns::RRType::ANY(), 0, ncr);
+
+ // Verify the DHCID test RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 1));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), dns::RRType::DHCID(), 0, ncr);
+
+ // Verify the UPDATE SECTION
+ // Should be 2, 1 which deletes the existing FQDN/IP and one that
+ // adds the new one.
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 2);
+
+ // Verify the FQDN delete RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::ANY(), exp_ip_rr_type, 0, ncr);
+
+ // Verify the FQDN/IP add RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 1));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), exp_ip_rr_type, 0, ncr);
+
+ // Verify there are no RRs in the ADDITIONAL Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_ADDITIONAL, 0);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+// Verifies that the contents of the given transaction's DNS update request
+// is correct for replacing a reverse DNS entry
+void checkReplaceRevPtrsRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getReverseDomain()->getName();
+ std::string exp_rev_addr = D2CfgMgr::reverseIpAddress(ncr->getIpAddress());
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify there are no RRs in the PREREQUISITE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 0);
+
+ // Verify the UPDATE Section.
+ // It should contain 4 RRs:
+ // 1. A delete all PTR RRs for the given IP
+ // 2. A delete of all DHCID RRs for the given IP
+ // 3. An add of the new PTR RR
+ // 4. An add of the new DHCID RR
+ dns::RRsetPtr rrset;
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 4);
+
+ // Verify the PTR delete RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::ANY(), dns::RRType::PTR(),
+ 0, ncr);
+
+ // Verify the DHCID delete RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 1));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::ANY(), dns::RRType::DHCID(),
+ 0, ncr);
+
+ // Verify the PTR add RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 2));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::IN(), dns::RRType::PTR(),
+ 0, ncr);
+
+ // Verify the DHCID add RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 3));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::IN(), dns::RRType::DHCID(),
+ 0, ncr);
+
+ // Verify there are no RRs in the ADDITIONAL Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_ADDITIONAL, 0);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+void checkRemoveFwdAddressRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getForwardDomain()->getName();
+ std::string exp_fqdn = ncr->getFqdn();
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify there is 1 RR in the PREREQUISITE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 1);
+
+ // Verify the DHCID matching assertion RR.
+ dns::RRsetPtr rrset;
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), dns::RRType::DHCID(),
+ 0, ncr);
+
+ // Verify there is 1 RR in the UPDATE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 1);
+
+ // Verify the FQDN/IP delete RR.
+ const dns::RRType& exp_ip_rr_type = tran.getAddressRRType();
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::NONE(), exp_ip_rr_type,
+ 0, ncr);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+void checkRemoveFwdRRsRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getForwardDomain()->getName();
+ std::string exp_fqdn = ncr->getFqdn();
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify there is 1 RR in the PREREQUISITE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 3);
+
+ // Verify the DHCID matches assertion.
+ dns::RRsetPtr rrset;
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::IN(), dns::RRType::DHCID(),
+ 0, ncr);
+
+ // Verify the NO A RRs assertion.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 1));
+ checkRR(rrset, exp_fqdn, dns::RRClass::NONE(), dns::RRType::A(),
+ 0, ncr, NO_RDATA);
+
+ // Verify the NO AAAA RRs assertion.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 2));
+ checkRR(rrset, exp_fqdn, dns::RRClass::NONE(), dns::RRType::AAAA(),
+ 0, ncr, NO_RDATA);
+
+ // Verify there is 1 RR in the UPDATE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 1);
+
+ // Verify the delete all for the FQDN RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_fqdn, dns::RRClass::ANY(), dns::RRType::ANY(),
+ 0, ncr);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+void checkRemoveRevPtrsRequest(NameChangeTransaction& tran) {
+ const D2UpdateMessagePtr& request = tran.getDnsUpdateRequest();
+ ASSERT_TRUE(request);
+
+ // Safety check.
+ dhcp_ddns::NameChangeRequestPtr ncr = tran.getNcr();
+ ASSERT_TRUE(ncr);
+
+ std::string exp_zone_name = tran.getReverseDomain()->getName();
+ std::string exp_rev_addr = D2CfgMgr::reverseIpAddress(ncr->getIpAddress());
+
+ // Verify the zone section.
+ checkZone(request, exp_zone_name);
+
+ // Verify there is 1 RR in the PREREQUISITE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_PREREQUISITE, 1);
+
+ // Verify the FQDN-PTRNAME assertion RR.
+ dns::RRsetPtr rrset;
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_PREREQUISITE, 0));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::IN(), dns::RRType::PTR(),
+ 0, ncr);
+
+ // Verify there is 1 RR in the UPDATE Section.
+ checkRRCount(request, D2UpdateMessage::SECTION_UPDATE, 1);
+
+ // Verify the delete all for the FQDN RR.
+ ASSERT_TRUE(rrset = getRRFromSection(request, D2UpdateMessage::
+ SECTION_UPDATE, 0));
+ checkRR(rrset, exp_rev_addr, dns::RRClass::ANY(), dns::RRType::ANY(),
+ 0, ncr);
+
+ // Verify that it will render toWire without throwing.
+ dns::MessageRenderer renderer;
+ ASSERT_NO_THROW(request->toWire(renderer));
+}
+
+std::string toHexText(const uint8_t* data, size_t len) {
+ std::ostringstream stream;
+ stream << "Data length is: " << len << std::endl;
+ for (int i = 0; i < len; ++i) {
+ if (i > 0 && ((i % 16) == 0)) {
+ stream << std::endl;
+ }
+
+ stream << setfill('0') << setw(2) << setbase(16)
+ << static_cast<unsigned int>(data[i]) << " ";
+ }
+
+ return (stream.str());
+}
+
+}; // namespace isc::d2
+}; // namespace isc
diff --git a/src/bin/d2/tests/nc_test_utils.h b/src/bin/d2/tests/nc_test_utils.h
new file mode 100644
index 0000000..a997525
--- /dev/null
+++ b/src/bin/d2/tests/nc_test_utils.h
@@ -0,0 +1,372 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NC_TEST_UTILS_H
+#define NC_TEST_UTILS_H
+
+/// @file nc_test_utils.h prototypes for functions related transaction testing.
+
+#include <d2/nc_trans.h>
+
+#include <asio/ip/udp.hpp>
+#include <asio/socket_base.hpp>
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace d2 {
+
+extern const char* TEST_DNS_SERVER_IP;
+extern size_t TEST_DNS_SERVER_PORT;
+
+// Not extern'ed to allow use as array size
+const int TEST_MSG_MAX = 1024;
+
+typedef boost::shared_ptr<asio::ip::udp::socket> SocketPtr;
+
+/// @brief This class simulates a DNS server. It is capable of performing
+/// an asynchronous read, governed by an IOService, and responding to received
+/// requests in a given manner.
+class FauxServer {
+public:
+ enum ResponseMode {
+ USE_RCODE, // Generate a response with a given RCODE
+ CORRUPT_RESP // Generate a corrupt response
+ };
+
+ // Reference to IOService to use for IO processing.
+ asiolink::IOService& io_service_;
+ // IP address at which to listen for requests.
+ const asiolink::IOAddress& address_;
+ // Port on which to listen for requests.
+ size_t port_;
+ // Socket on which listening is done.
+ SocketPtr server_socket_;
+ // Stores the end point of requesting client.
+ asio::ip::udp::endpoint remote_;
+ // Buffer in which received packets are stuffed.
+ uint8_t receive_buffer_[TEST_MSG_MAX];
+ // Flag which indicates if a receive has been initiated but
+ // not yet completed.
+ bool receive_pending_;
+ // Indicates if server is in perpetual receive mode. If true once
+ // a receive has been completed, a new one will be automatically
+ // initiated.
+ bool perpetual_receive_;
+
+ /// @brief Constructor
+ ///
+ /// @param io_service IOService to be used for socket IO.
+ /// @param address IP address at which the server should listen.
+ /// @param port Port number at which the server should listen.
+ FauxServer(asiolink::IOService& io_service, asiolink::IOAddress& address,
+ size_t port);
+
+ /// @brief Constructor
+ ///
+ /// @param io_service IOService to be used for socket IO.
+ /// @param server DnsServerInfo of server the DNS server. This supplies the
+ /// server's ip address and port.
+ FauxServer(asiolink::IOService& io_service, DnsServerInfo& server);
+
+ /// @brief Destructor
+ virtual ~FauxServer();
+
+ /// @brief Initiates an asynchronous receive
+ ///
+ /// Starts the server listening for requests. Upon completion of the
+ /// listen, the callback method, requestHandler, is invoked.
+ ///
+ /// @param response_mode Selects how the server responds to a request
+ /// @param response_rcode The Rcode value set in the response. Not used
+ /// for all modes.
+ void receive (const ResponseMode& response_mode,
+ const dns::Rcode& response_rcode=dns::Rcode::NOERROR());
+
+ /// @brief Socket IO Completion callback
+ ///
+ /// This method servers as the Server's UDP socket receive callback handler.
+ /// When the receive completes the handler is invoked with the
+ /// @param error result code of the receive (determined by asio layer)
+ /// @param bytes_recvd number of bytes received, if any
+ /// @param response_mode type of response the handler should produce
+ /// @param response_rcode value of Rcode in the response constructed by
+ /// handler
+ void requestHandler(const asio::error_code& error,
+ std::size_t bytes_recvd,
+ const ResponseMode& response_mode,
+ const dns::Rcode& response_rcode);
+
+ /// @brief Returns true if a receive has been started but not completed.
+ bool isReceivePending() {
+ return receive_pending_;
+ }
+};
+
+/// @brief Provides a means to process IOService IO for a finite amount of time.
+///
+/// This class instantiates an IOService provides a single method, runTimedIO
+/// which will run the IOService for no more than a finite amount of time,
+/// at least one event is executed or the IOService is stopped.
+/// It provides an virtual handler for timer expiration event. It is
+/// intended to be used as a base class for test fixtures that need to process
+/// IO by providing them a consistent way to do so while retaining a safety
+/// valve so tests do not hang.
+class TimedIO {
+public:
+ IOServicePtr io_service_;
+ asiolink::IntervalTimer timer_;
+ int run_time_;
+
+ // Constructor
+ TimedIO();
+
+ // Destructor
+ virtual ~TimedIO();
+
+ /// @brief IO Timer expiration handler
+ ///
+ /// Stops the IOService and fails the current test.
+ virtual void timesUp();
+
+ /// @brief Processes IO till time expires or at least one handler executes.
+ ///
+ /// This method first polls IOService to run any ready handlers. If no
+ /// handlers are ready, it starts the internal time to run for the given
+ /// amount of time and invokes service's run_one method. This method
+ /// blocks until at least one handler executes or the IO Service is stopped.
+ /// Upon completion of this method the timer is cancelled. Should the
+ /// timer expires prior to run_one returning, the timesUp handler will be
+ /// invoked which stops the IO service and fails the test.
+ ///
+ /// Note that this method closely mimics the runIO method in D2Process.
+ ///
+ /// @param run_time maximum length of time to run in milliseconds before
+ /// timing out.
+ ///
+ /// @return Returns the number of handlers executed or zero. A return of
+ /// zero indicates that the IOService has been stopped.
+ int runTimedIO(int run_time);
+
+};
+
+/// @brief Base class Test fixture for testing transactions.
+class TransactionTest : public TimedIO, public ::testing::Test {
+public:
+ dhcp_ddns::NameChangeRequestPtr ncr_;
+ DdnsDomainPtr forward_domain_;
+ DdnsDomainPtr reverse_domain_;
+
+ /// #brief constants used to specify change directions for a transaction.
+ static const unsigned int FORWARD_CHG; // Only forward change.
+ static const unsigned int REVERSE_CHG; // Only reverse change.
+ static const unsigned int FWD_AND_REV_CHG; // Both forward and reverse.
+
+ TransactionTest();
+ virtual ~TransactionTest();
+
+ /// @brief Creates a transaction which requests an IPv4 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv4 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_type selects the type of change requested, CHG_ADD or
+ /// CHG_REMOVE.
+ /// @param change_mask determines which change directions are requested
+ /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+ void setupForIPv4Transaction(dhcp_ddns::NameChangeType change_type,
+ int change_mask);
+
+ /// @brief Creates a transaction which requests an IPv6 DNS update.
+ ///
+ /// The transaction is constructed around a predefined (i.e. "canned")
+ /// IPv6 NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested. Based upon the change mask, the transaction
+ /// will have either the forward, reverse, or both domains populated.
+ ///
+ /// @param change_type selects the type of change requested, CHG_ADD or
+ /// CHG_REMOVE.
+ /// @param change_mask determines which change directions are requested
+ /// FORWARD_CHG, REVERSE_CHG, or FWD_AND_REV_CHG.
+ void setupForIPv6Transaction(dhcp_ddns::NameChangeType change_type,
+ int change_mask);
+};
+
+
+/// @brief Tests the number of RRs in a request section against a given count.
+///
+/// This function actually returns the number of RRsetPtrs in a section. Since
+/// D2 only uses RRsets with a single RData in each (i.e. 1 RR), it is used
+/// as the number of RRs. The dns::Message::getRRCount() cannot be used for
+/// this as it returns the number of RDatas in an RRSet which does NOT equate
+/// to the number of RRs. RRs with no RData, those with class or type of ANY,
+/// are not counted.
+///
+/// @param request DNS update request to test
+/// @param section enum value of the section to count
+/// @param count the expected number of RRs
+extern void checkRRCount(const D2UpdateMessagePtr& request,
+ D2UpdateMessage::UpdateMsgSection section, int count);
+
+/// @brief Tests the zone content of a given request.
+///
+/// @param request DNS update request to validate
+/// @param exp_zone_name expected value of the zone name in the zone section
+extern void checkZone(const D2UpdateMessagePtr& request,
+ const std::string& exp_zone_name);
+
+/// @brief Tests the contents of an RRset
+///
+/// @param rrset Pointer the RRset to test
+/// @param exp_name expected value of RRset name (FQDN or reverse ip)
+/// @param exp_class expected RRClass value of RRset
+/// @param exp_typ expected RRType value of RRset
+/// @param exp_ttl expected TTL value of RRset
+/// @param ncr NameChangeRequest on which the RRset is based
+/// @param has_rdata if true, RRset's rdata will be checked based on it's
+/// RRType. Set this to false if the RRset's type supports Rdata but it does
+/// not contain it. For instance, prerequisites of type NONE have no Rdata
+/// where updates of type NONE may.
+extern void checkRR(dns::RRsetPtr rrset, const std::string& exp_name,
+ const dns::RRClass& exp_class, const dns::RRType& exp_type,
+ unsigned int exp_ttl, dhcp_ddns::NameChangeRequestPtr ncr,
+ bool has_rdata=true);
+
+/// @brief Fetches an RR(set) from a given section of a request
+///
+/// @param request DNS update request from which the RR should come
+/// @param section enum value of the section from which the RR should come
+/// @param index zero-based index of the RR of interest.
+///
+/// @return Pointer to the RR of interest, empty pointer if the index is out
+/// of range.
+extern dns::RRsetPtr getRRFromSection(const D2UpdateMessagePtr& request,
+ D2UpdateMessage::UpdateMsgSection section,
+ int index);
+/// @brief Creates a NameChangeRequest from a JSON string
+///
+/// @param ncr_str JSON string form of a NameChangeRequest. Example:
+/// @code
+/// const char* msg_str =
+/// "{"
+/// " \"change_type\" : 0 , "
+/// " \"forward_change\" : true , "
+/// " \"reverse_change\" : true , "
+/// " \"fqdn\" : \"my.example.com.\" , "
+/// " \"ip_address\" : \"192.168.2.1\" , "
+/// " \"dhcid\" : \"0102030405060708\" , "
+/// " \"lease_expires_on\" : \"20130121132405\" , "
+/// " \"lease_length\" : 1300 "
+/// "}";
+///
+/// @endcode
+
+/// @brief Verifies a forward mapping addition DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// adding a forward DNS mapping.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkAddFwdAddressRequest(NameChangeTransaction& tran);
+
+/// @brief Verifies a forward mapping replacement DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// replacing a forward DNS mapping.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkReplaceFwdAddressRequest(NameChangeTransaction& tran);
+
+/// @brief Verifies a reverse mapping replacement DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// replacing a reverse DNS mapping.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkReplaceRevPtrsRequest(NameChangeTransaction& tran);
+
+/// @brief Verifies a forward address removal DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// removing the forward address DNS entry.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkRemoveFwdAddressRequest(NameChangeTransaction& tran);
+
+/// @brief Verifies a forward RR removal DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// removing forward RR DNS entries.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkRemoveFwdRRsRequest(NameChangeTransaction& tran);
+
+/// @brief Verifies a reverse mapping removal DNS update request
+///
+/// Tests that the DNS Update request for a given transaction, is correct for
+/// removing a reverse DNS mapping.
+///
+/// @param tran Transaction containing the request to be verified.
+extern void checkRemoveRevPtrsRequest(NameChangeTransaction& tran);
+
+
+/// @brief Creates a NameChangeRequest from JSON string.
+///
+/// @param ncr_str string of JSON text from which to make the request.
+///
+/// @return Pointer to newly created request.
+///
+/// @throw Underlying methods may throw.
+extern
+dhcp_ddns::NameChangeRequestPtr makeNcrFromString(const std::string& ncr_str);
+
+/// @brief Creates a DdnsDomain with the one server.
+///
+/// @param zone_name zone name of the domain
+/// @param key_name TSIG key name of the TSIG key for this domain
+///
+/// @throw Underlying methods may throw.
+extern DdnsDomainPtr makeDomain(const std::string& zone_name,
+ const std::string& key_name = "");
+
+/// @brief Creates a DnsServerInfo and adds it to the given DdnsDomain.
+///
+/// The server is created and added to the domain, without duplicate entry
+/// checking.
+///
+/// @param domain DdnsDomain to which to add the server
+/// @param name new server's host name of the server
+/// @param ip new server's ip address
+/// @param port new server's port
+///
+/// @throw Underlying methods may throw.
+extern void addDomainServer(DdnsDomainPtr& domain, const std::string& name,
+ const std::string& ip = TEST_DNS_SERVER_IP,
+ const size_t port = TEST_DNS_SERVER_PORT);
+
+/// @brief Creates a hex text dump of the given data buffer.
+///
+/// This method is not used for testing but is handy for debugging. It creates
+/// a pleasantly formatted string of 2-digits per byte separated by spaces with
+/// 16 bytes per line.
+///
+/// @param data pointer to the data to dump
+/// @param len size (in bytes) of data
+extern std::string toHexText(const uint8_t* data, size_t len);
+
+}; // namespace isc::d2
+}; // namespace isc
+
+#endif
diff --git a/src/bin/d2/tests/nc_trans_unittests.cc b/src/bin/d2/tests/nc_trans_unittests.cc
new file mode 100644
index 0000000..010d197
--- /dev/null
+++ b/src/bin/d2/tests/nc_trans_unittests.cc
@@ -0,0 +1,1010 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/interval_timer.h>
+#include <d2/nc_trans.h>
+#include <dns/opcode.h>
+#include <dns/messagerenderer.h>
+#include <log/logger_support.h>
+#include <log/macros.h>
+#include <util/buffer.h>
+#include <nc_test_utils.h>
+
+#include <asio/ip/udp.hpp>
+#include <asio/socket_base.hpp>
+#include <asio.hpp>
+#include <asio/error_code.hpp>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Test derivation of NameChangeTransaction for exercising state
+/// model mechanics.
+///
+/// This class facilitates testing by making non-public methods accessible so
+/// they can be invoked directly in test routines. It implements a very
+/// rudimentary state model, sufficient to test the state model mechanics
+/// supplied by the base class.
+class NameChangeStub : public NameChangeTransaction {
+public:
+
+ // NameChangeStub states
+ static const int DOING_UPDATE_ST = NCT_DERIVED_STATE_MIN + 1;
+
+ // NameChangeStub events
+ static const int SEND_UPDATE_EVT = NCT_DERIVED_EVENT_MIN + 2;
+
+ bool use_stub_callback_;
+
+ /// @brief Constructor
+ ///
+ /// Parameters match those needed by NameChangeTransaction.
+ NameChangeStub(IOServicePtr& io_service,
+ dhcp_ddns::NameChangeRequestPtr& ncr,
+ DdnsDomainPtr forward_domain,
+ DdnsDomainPtr reverse_domain)
+ : NameChangeTransaction(io_service, ncr, forward_domain,
+ reverse_domain),
+ use_stub_callback_(false) {
+ }
+
+ /// @brief Destructor
+ virtual ~NameChangeStub() {
+ }
+
+ /// @brief DNSClient callback
+ /// Overrides the callback in NameChangeTranscation to allow testing
+ /// sendUpdate without incorporating exectution of the state model
+ /// into the test.
+ /// It sets the DNS status update and posts IO_COMPLETED_EVT as does
+ /// the normal callback, but rather than invoking runModel it stops
+ /// the IO service. This allows tests to be constructed that consisted
+ /// of generating a DNS request and invoking sendUpdate to post it and
+ /// wait for response.
+ virtual void operator()(DNSClient::Status status) {
+ if (use_stub_callback_) {
+ setDnsUpdateStatus(status);
+ postNextEvent(IO_COMPLETED_EVT);
+ getIOService()->stop();
+ } else {
+ // For tests which need to use the real callback.
+ NameChangeTransaction::operator()(status);
+ }
+ }
+
+ /// @brief Some tests require a server to be selected. This method
+ /// selects the first server in the forward domain without involving
+ /// state model execution to do so.
+ bool selectFwdServer() {
+ if (getForwardDomain()) {
+ initServerSelection(getForwardDomain());
+ selectNextServer();
+ return (getCurrentServer());
+ }
+
+ return (false);
+ }
+
+ /// @brief Empty handler used to satisfy map verification.
+ void dummyHandler() {
+ isc_throw(NameChangeTransactionError,
+ "dummyHandler - invalid event: " << getContextStr());
+ }
+
+ /// @brief State handler for the READY_ST.
+ ///
+ /// Serves as the starting state handler, it consumes the
+ /// START_EVT "transitioning" to the state, DOING_UPDATE_ST and
+ /// sets the next event to SEND_UPDATE_EVT.
+ void readyHandler() {
+ switch(getNextEvent()) {
+ case START_EVT:
+ transition(DOING_UPDATE_ST, SEND_UPDATE_EVT);
+ break;
+ default:
+ // its bogus
+ isc_throw(NameChangeTransactionError,
+ "readyHandler - invalid event: " << getContextStr());
+ }
+ }
+
+ /// @brief State handler for the DOING_UPDATE_ST.
+ ///
+ /// Simulates a state that starts some form of asynchronous work.
+ /// When next event is SEND_UPDATE_EVT it sets the status to pending
+ /// and signals the state model must "wait" for an event by setting
+ /// next event to NOP_EVT.
+ ///
+ /// When next event is IO_COMPLETED_EVT, it transitions to the state,
+ /// PROCESS_TRANS_OK_ST, and sets the next event to UPDATE_OK_EVT.
+ void doingUpdateHandler() {
+ switch(getNextEvent()) {
+ case SEND_UPDATE_EVT:
+ setNcrStatus(dhcp_ddns::ST_PENDING);
+ postNextEvent(NOP_EVT);
+ break;
+ case IO_COMPLETED_EVT:
+ if (getDnsUpdateStatus() == DNSClient::SUCCESS) {
+ setForwardChangeCompleted(true);
+ transition(PROCESS_TRANS_OK_ST, UPDATE_OK_EVT);
+ } else {
+ transition(PROCESS_TRANS_FAILED_ST, UPDATE_FAILED_EVT);
+ }
+ break;
+ default:
+ // its bogus
+ isc_throw(NameChangeTransactionError,
+ "doingUpdateHandler - invalid event: "
+ << getContextStr());
+ }
+ }
+
+ /// @brief State handler for the PROCESS_TRANS_OK_ST.
+ ///
+ /// This is the last state in the model. Note that it sets the
+ /// status to completed and next event to NOP_EVT.
+ void processTransDoneHandler() {
+ switch(getNextEvent()) {
+ case UPDATE_OK_EVT:
+ setNcrStatus(dhcp_ddns::ST_COMPLETED);
+ endModel();
+ break;
+ case UPDATE_FAILED_EVT:
+ setNcrStatus(dhcp_ddns::ST_FAILED);
+ endModel();
+ break;
+ default:
+ // its bogus
+ isc_throw(NameChangeTransactionError,
+ "processTransDoneHandler - invalid event: "
+ << getContextStr());
+ }
+ }
+
+ /// @brief Construct the event dictionary.
+ virtual void defineEvents() {
+ // Invoke the base call implementation first.
+ NameChangeTransaction::defineEvents();
+
+ // Define our events.
+ defineEvent(SEND_UPDATE_EVT, "SEND_UPDATE_EVT");
+ }
+
+ /// @brief Verify the event dictionary.
+ virtual void verifyEvents() {
+ // Invoke the base call implementation first.
+ NameChangeTransaction::verifyEvents();
+
+ // Define our events.
+ getEvent(SEND_UPDATE_EVT);
+ }
+
+ /// @brief Construct the state dictionary.
+ virtual void defineStates() {
+ // Invoke the base call implementation first.
+ NameChangeTransaction::defineStates();
+
+ // Define our states.
+ defineState(READY_ST, "READY_ST",
+ boost::bind(&NameChangeStub::readyHandler, this));
+
+ defineState(SELECTING_FWD_SERVER_ST, "SELECTING_FWD_SERVER_ST",
+ boost::bind(&NameChangeStub::dummyHandler, this));
+
+ defineState(SELECTING_REV_SERVER_ST, "SELECTING_REV_SERVER_ST",
+ boost::bind(&NameChangeStub::dummyHandler, this));
+
+ defineState(DOING_UPDATE_ST, "DOING_UPDATE_ST",
+ boost::bind(&NameChangeStub::doingUpdateHandler,
+ this));
+
+ defineState(PROCESS_TRANS_OK_ST, "PROCESS_TRANS_OK_ST",
+ boost::bind(&NameChangeStub::
+ processTransDoneHandler, this));
+
+ defineState(PROCESS_TRANS_FAILED_ST, "PROCESS_TRANS_FAILED_ST",
+ boost::bind(&NameChangeStub::
+ processTransDoneHandler, this));
+ }
+
+ /// @brief Verify the event dictionary.
+ virtual void verifyStates() {
+ // Invoke the base call implementation first.
+ NameChangeTransaction::verifyStates();
+
+ // Check our states.
+ getState(DOING_UPDATE_ST);
+ }
+
+ // Expose the protected methods to be tested.
+ using StateModel::runModel;
+ using StateModel::postNextEvent;
+ using StateModel::setState;
+ using StateModel::initDictionaries;
+ using NameChangeTransaction::initServerSelection;
+ using NameChangeTransaction::selectNextServer;
+ using NameChangeTransaction::getCurrentServer;
+ using NameChangeTransaction::getDNSClient;
+ using NameChangeTransaction::setNcrStatus;
+ using NameChangeTransaction::setDnsUpdateRequest;
+ using NameChangeTransaction::clearDnsUpdateRequest;
+ using NameChangeTransaction::setDnsUpdateStatus;
+ using NameChangeTransaction::getDnsUpdateResponse;
+ using NameChangeTransaction::setDnsUpdateResponse;
+ using NameChangeTransaction::clearDnsUpdateResponse;
+ using NameChangeTransaction::getForwardChangeCompleted;
+ using NameChangeTransaction::getReverseChangeCompleted;
+ using NameChangeTransaction::setForwardChangeCompleted;
+ using NameChangeTransaction::setReverseChangeCompleted;
+ using NameChangeTransaction::setUpdateAttempts;
+ using NameChangeTransaction::transition;
+ using NameChangeTransaction::retryTransition;
+ using NameChangeTransaction::sendUpdate;
+ using NameChangeTransaction::prepNewRequest;
+ using NameChangeTransaction::addLeaseAddressRdata;
+ using NameChangeTransaction::addDhcidRdata;
+ using NameChangeTransaction::addPtrRdata;
+};
+
+// Declare them so Gtest can see them.
+const int NameChangeStub::DOING_UPDATE_ST;
+const int NameChangeStub::SEND_UPDATE_EVT;
+
+/// @brief Defines a pointer to a NameChangeStubPtr instance.
+typedef boost::shared_ptr<NameChangeStub> NameChangeStubPtr;
+
+/// @brief Test fixture for testing NameChangeTransaction
+///
+/// Note this class uses NameChangeStub class to exercise non-public
+/// aspects of NameChangeTransaction.
+class NameChangeTransactionTest : public TransactionTest {
+public:
+ NameChangeTransactionTest() {
+ }
+
+ virtual ~NameChangeTransactionTest() {
+ }
+
+ /// @brief Instantiates a NameChangeStub test transaction
+ /// The transaction is constructed around a predefined (i.e "canned")
+ /// NameChangeRequest. The request has both forward and reverse DNS
+ /// changes requested, and both forward and reverse domains are populated.
+ NameChangeStubPtr makeCannedTransaction() {
+ // Creates IPv4 remove request, forward, and reverse domains.
+ setupForIPv4Transaction(dhcp_ddns::CHG_ADD, FWD_AND_REV_CHG);
+
+ // Now create the test transaction as would occur in update manager.
+ // Instantiate the transaction as would be done by update manager.
+ return (NameChangeStubPtr(new NameChangeStub(io_service_, ncr_,
+ forward_domain_, reverse_domain_)));
+ }
+
+ /// @brief Builds and then sends an update request
+ ///
+ /// This method is used to build and send and update request. It is used
+ /// in conjuction with FauxServer to test various message response
+ /// scenarios.
+ /// @param name_change Transaction under test
+ /// @param run_time Maximum time to permit IO processing to run before
+ /// timing out (in milliseconds)
+ void doOneExchange(NameChangeStubPtr name_change,
+ unsigned int run_time = 500) {
+ // Create a valid request for the transaction.
+ D2UpdateMessagePtr req;
+ ASSERT_NO_THROW(req.reset(new D2UpdateMessage(D2UpdateMessage::
+ OUTBOUND)));
+ ASSERT_NO_THROW(name_change->setDnsUpdateRequest(req));
+ req->setZone(dns::Name("request.example.com"), dns::RRClass::ANY());
+ req->setRcode(dns::Rcode(dns::Rcode::NOERROR_CODE));
+
+ // Set the flag to use the NameChangeStub's DNSClient callback.
+ name_change->use_stub_callback_ = true;
+
+ // Invoke sendUpdate.
+ ASSERT_NO_THROW(name_change->sendUpdate());
+
+ // Update attempt count should be 1, next event should be NOP_EVT.
+ ASSERT_EQ(1, name_change->getUpdateAttempts());
+ ASSERT_EQ(NameChangeTransaction::NOP_EVT,
+ name_change->getNextEvent());
+
+ int cnt = 0;
+ while (name_change->getNextEvent() == NameChangeTransaction::NOP_EVT) {
+ ASSERT_NO_THROW(cnt = runTimedIO(run_time));
+ if (cnt == 0) {
+ FAIL() << "IO Service stopped unexpectedly";
+ }
+ }
+ }
+};
+
+/// @brief Tests NameChangeTransaction construction.
+/// This test verifies that:
+/// 1. Construction with null NameChangeRequest
+/// 2. Construction with null forward domain is not allowed when the request
+/// requires forward change.
+/// 3. Construction with null reverse domain is not allowed when the request
+/// requires reverse change.
+/// 4. Valid construction functions properly
+TEST(NameChangeTransaction, construction) {
+ IOServicePtr io_service(new isc::asiolink::IOService());
+
+ const char* msg_str =
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : true , "
+ " \"fqdn\" : \"example.com.\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"0102030405060708\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}";
+
+ dhcp_ddns::NameChangeRequestPtr ncr;
+
+ dhcp_ddns::NameChangeRequestPtr empty_ncr;
+ DnsServerInfoStoragePtr servers;
+ DdnsDomainPtr forward_domain;
+ DdnsDomainPtr reverse_domain;
+ DdnsDomainPtr empty_domain;
+
+ ASSERT_NO_THROW(ncr = dhcp_ddns::NameChangeRequest::fromJSON(msg_str));
+ ASSERT_NO_THROW(forward_domain.reset(new DdnsDomain("*", "", servers)));
+ ASSERT_NO_THROW(reverse_domain.reset(new DdnsDomain("*", "", servers)));
+
+ // Verify that construction with a null IOServicePtr fails.
+ // @todo Subject to change if multi-threading is implemented.
+ IOServicePtr empty;
+ EXPECT_THROW(NameChangeTransaction(empty, ncr,
+ forward_domain, reverse_domain),
+ NameChangeTransactionError);
+
+ // Verify that construction with an empty NameChangeRequest throws.
+ EXPECT_THROW(NameChangeTransaction(io_service, empty_ncr,
+ forward_domain, reverse_domain),
+ NameChangeTransactionError);
+
+ // Verify that construction with an empty forward domain when the
+ // NameChangeRequest calls for a forward change throws.
+ EXPECT_THROW(NameChangeTransaction(io_service, ncr,
+ empty_domain, reverse_domain),
+ NameChangeTransactionError);
+
+ // Verify that construction with an empty reverse domain when the
+ // NameChangeRequest calls for a reverse change throws.
+ EXPECT_THROW(NameChangeTransaction(io_service, ncr,
+ forward_domain, empty_domain),
+ NameChangeTransactionError);
+
+ // Verify that a valid construction attempt works.
+ EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+ forward_domain, reverse_domain));
+
+ // Verify that an empty forward domain is allowed when the requests does
+ // not include a forward change.
+ ncr->setForwardChange(false);
+ ncr->setReverseChange(true);
+ EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+ empty_domain, reverse_domain));
+
+ // Verify that an empty reverse domain is allowed when the requests does
+ // not include a reverse change.
+ ncr->setForwardChange(true);
+ ncr->setReverseChange(false);
+ EXPECT_NO_THROW(NameChangeTransaction(io_service, ncr,
+ forward_domain, empty_domain));
+}
+
+/// @brief General testing of member accessors.
+/// Most if not all of these are also tested as a byproduct off larger tests.
+TEST_F(NameChangeTransactionTest, accessors) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Verify that fetching the NameChangeRequest works.
+ dhcp_ddns::NameChangeRequestPtr ncr = name_change->getNcr();
+ ASSERT_TRUE(ncr);
+
+ // Verify that getTransactionKey works.
+ EXPECT_EQ(ncr->getDhcid(), name_change->getTransactionKey());
+
+ // Verify that NcrStatus can be set and retrieved.
+ EXPECT_NO_THROW(name_change->setNcrStatus(dhcp_ddns::ST_FAILED));
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, ncr->getStatus());
+
+ // Verify that the forward domain can be retrieved.
+ ASSERT_TRUE(name_change->getForwardDomain());
+ EXPECT_EQ(forward_domain_, name_change->getForwardDomain());
+
+ // Verify that the reverse domain can be retrieved.
+ ASSERT_TRUE(name_change->getReverseDomain());
+ EXPECT_EQ(reverse_domain_, name_change->getReverseDomain());
+
+ // Neither of these have direct setters, but are tested under server
+ // selection.
+ EXPECT_FALSE(name_change->getDNSClient());
+ EXPECT_FALSE(name_change->getCurrentServer());
+
+ // Verify that DNS update status can be set and retrieved.
+ EXPECT_NO_THROW(name_change->setDnsUpdateStatus(DNSClient::TIMEOUT));
+ EXPECT_EQ(DNSClient::TIMEOUT, name_change->getDnsUpdateStatus());
+
+ // Verify that the forward change complete flag can be set and fetched.
+ EXPECT_NO_THROW(name_change->setForwardChangeCompleted(true));
+ EXPECT_TRUE(name_change->getForwardChangeCompleted());
+
+ // Verify that the reverse change complete flag can be set and fetched.
+ EXPECT_NO_THROW(name_change->setReverseChangeCompleted(true));
+ EXPECT_TRUE(name_change->getReverseChangeCompleted());
+}
+
+/// @brief Tests DNS update request accessor methods.
+TEST_F(NameChangeTransactionTest, dnsUpdateRequestAccessors) {
+ // Create a transaction.
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Post transaction construction, there should not be an update request.
+ EXPECT_FALSE(name_change->getDnsUpdateRequest());
+
+ // Create a request.
+ D2UpdateMessagePtr req;
+ ASSERT_NO_THROW(req.reset(new D2UpdateMessage(D2UpdateMessage::OUTBOUND)));
+
+ // Use the setter and then verify we can fetch the request.
+ ASSERT_NO_THROW(name_change->setDnsUpdateRequest(req));
+
+ // Post set, we should be able to fetch it.
+ ASSERT_TRUE(name_change->getDnsUpdateRequest());
+
+ // Should be able to clear it.
+ ASSERT_NO_THROW(name_change->clearDnsUpdateRequest());
+
+ // Should be empty again.
+ EXPECT_FALSE(name_change->getDnsUpdateRequest());
+}
+
+/// @brief Tests DNS update request accessor methods.
+TEST_F(NameChangeTransactionTest, dnsUpdateResponseAccessors) {
+ // Create a transaction.
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Post transaction construction, there should not be an update response.
+ EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+ // Create a response.
+ D2UpdateMessagePtr resp;
+ ASSERT_NO_THROW(resp.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND)));
+
+ // Use the setter and then verify we can fetch the response.
+ ASSERT_NO_THROW(name_change->setDnsUpdateResponse(resp));
+
+ // Post set, we should be able to fetch it.
+ EXPECT_TRUE(name_change->getDnsUpdateResponse());
+
+ // Should be able to clear it.
+ ASSERT_NO_THROW(name_change->clearDnsUpdateResponse());
+
+ // Should be empty again.
+ EXPECT_FALSE(name_change->getDnsUpdateResponse());
+}
+
+
+/// @brief Tests event and state dictionary construction and verification.
+TEST_F(NameChangeTransactionTest, dictionaryCheck) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Verify that the event and state dictionary validation fails prior
+ // dictionary construction.
+ ASSERT_THROW(name_change->verifyEvents(), StateModelError);
+ ASSERT_THROW(name_change->verifyStates(), StateModelError);
+
+ // Construct both dictionaries.
+ ASSERT_NO_THROW(name_change->defineEvents());
+ ASSERT_NO_THROW(name_change->defineStates());
+
+ // Verify both event and state dictionaries now pass validation.
+ ASSERT_NO_THROW(name_change->verifyEvents());
+ ASSERT_NO_THROW(name_change->verifyStates());
+}
+
+/// @brief Tests server selection methods.
+/// Each transaction has a list of one or more servers for each DNS direction
+/// it is required to update. The transaction must be able to start at the
+/// beginning of a server list and cycle through them one at time, as needed,
+/// when a DNS exchange fails due to an IO error. This test verifies the
+/// ability to iteratively select a server from the list as the current server.
+TEST_F(NameChangeTransactionTest, serverSelectionTest) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Verify that the forward domain and its list of servers can be retrieved.
+ DdnsDomainPtr& domain = name_change->getForwardDomain();
+ ASSERT_TRUE(domain);
+ DnsServerInfoStoragePtr servers = domain->getServers();
+ ASSERT_TRUE(servers);
+
+ // Get the number of entries in the server list.
+ int num_servers = servers->size();
+ ASSERT_TRUE(num_servers > 0);
+
+ // Verify that we can initialize server selection. This "resets" the
+ // selection process to start over using the list of servers in the
+ // given domain.
+ ASSERT_NO_THROW(name_change->initServerSelection(domain));
+
+ // The server selection process determines the current server,
+ // instantiates a new DNSClient, and a DNS response message buffer.
+ // We need to save the values before each selection, so we can verify
+ // they are correct after each selection.
+ DnsServerInfoPtr prev_server = name_change->getCurrentServer();
+ DNSClientPtr prev_client = name_change->getDNSClient();
+
+ // Verify response pointer is empty.
+ EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+ // Create dummy response so we can verify it is cleared at each
+ // new server select.
+ D2UpdateMessagePtr dummyResp;
+ dummyResp.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
+ ASSERT_NO_THROW(name_change->setDnsUpdateResponse(dummyResp));
+ ASSERT_TRUE(name_change->getDnsUpdateResponse());
+
+ // Iteratively select through the list of servers.
+ int passes = 0;
+ while (name_change->selectNextServer()) {
+ // Get the new values after the selection has been made.
+ DnsServerInfoPtr server = name_change->getCurrentServer();
+ DNSClientPtr client = name_change->getDNSClient();
+ D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+
+ // Verify that the new values are not empty.
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(client);
+
+ // Verify response pointer is now empty.
+ EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+ // Verify that the new values are indeed new.
+ EXPECT_NE(server, prev_server);
+ EXPECT_NE(client, prev_client);
+
+ // Remember the selected values for the next pass.
+ prev_server = server;
+ prev_client = client;
+
+ // Create new dummy response.
+ dummyResp.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
+ ASSERT_NO_THROW(name_change->setDnsUpdateResponse(dummyResp));
+ ASSERT_TRUE(name_change->getDnsUpdateResponse());
+
+ ++passes;
+ }
+
+ // Verify that the number of passes made equal the number of servers.
+ EXPECT_EQ (passes, num_servers);
+
+ // Repeat the same test using the reverse domain.
+ // Verify that the reverse domain and its list of servers can be retrieved.
+ domain = name_change->getReverseDomain();
+ ASSERT_TRUE(domain);
+ servers = domain->getServers();
+ ASSERT_TRUE(servers);
+
+ // Get the number of entries in the server list.
+ num_servers = servers->size();
+ ASSERT_TRUE(num_servers > 0);
+
+ // Verify that we can initialize server selection. This "resets" the
+ // selection process to start over using the list of servers in the
+ // given domain.
+ ASSERT_NO_THROW(name_change->initServerSelection(domain));
+
+ // The server selection process determines the current server,
+ // instantiates a new DNSClient, and resets the DNS response message buffer.
+ // We need to save the values before each selection, so we can verify
+ // they are correct after each selection.
+ prev_server = name_change->getCurrentServer();
+ prev_client = name_change->getDNSClient();
+
+ // Iteratively select through the list of servers.
+ passes = 0;
+ while (name_change->selectNextServer()) {
+ // Get the new values after the selection has been made.
+ DnsServerInfoPtr server = name_change->getCurrentServer();
+ DNSClientPtr client = name_change->getDNSClient();
+ D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+
+ // Verify that the new values are not empty.
+ EXPECT_TRUE(server);
+ EXPECT_TRUE(client);
+
+ // Verify response pointer is now empty.
+ EXPECT_FALSE(name_change->getDnsUpdateResponse());
+
+ // Verify that the new values are indeed new.
+ EXPECT_NE(server, prev_server);
+ EXPECT_NE(client, prev_client);
+
+ // Remember the selected values for the next pass.
+ prev_server = server;
+ prev_client = client;
+
+ // Create new dummy response.
+ dummyResp.reset(new D2UpdateMessage(D2UpdateMessage::INBOUND));
+ ASSERT_NO_THROW(name_change->setDnsUpdateResponse(dummyResp));
+ ASSERT_TRUE(name_change->getDnsUpdateResponse());
+
+ ++passes;
+ }
+
+ // Verify that the number of passes made equal the number of servers.
+ EXPECT_EQ (passes, num_servers);
+}
+
+/// @brief Tests that the transaction will be "failed" upon model errors.
+TEST_F(NameChangeTransactionTest, modelFailure) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Now call runModel() with an undefined event which should not throw,
+ // but should result in a failed model and failed transaction.
+ EXPECT_NO_THROW(name_change->runModel(9999));
+
+ // Verify that the model reports are done but failed.
+ EXPECT_TRUE(name_change->isModelDone());
+ EXPECT_TRUE(name_change->didModelFail());
+
+ // Verify that the transaction has failed.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_change->getNcrStatus());
+}
+
+/// @brief Tests the ability to use startTransaction to initiate the state
+/// model execution, and DNSClient callback, operator(), to resume the
+/// model with a update successful outcome.
+TEST_F(NameChangeTransactionTest, successfulUpdateTest) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ EXPECT_TRUE(name_change->isModelNew());
+ EXPECT_FALSE(name_change->getForwardChangeCompleted());
+
+ // Launch the transaction by calling startTransaction. The state model
+ // should run up until the "IO" operation is initiated in DOING_UPDATE_ST.
+ ASSERT_NO_THROW(name_change->startTransaction());
+
+ // Verify that the model is running but waiting, and that forward change
+ // completion is still false.
+ EXPECT_TRUE(name_change->isModelRunning());
+ EXPECT_TRUE(name_change->isModelWaiting());
+ EXPECT_FALSE(name_change->getForwardChangeCompleted());
+
+ // Simulate completion of DNSClient exchange by invoking the callback, as
+ // DNSClient would. This should cause the state model to progress through
+ // completion.
+ EXPECT_NO_THROW((*name_change)(DNSClient::SUCCESS));
+
+ // The model should have worked through to completion.
+ // Verify that the model is done and not failed.
+ EXPECT_TRUE(name_change->isModelDone());
+ EXPECT_FALSE(name_change->didModelFail());
+
+ // Verify that NCR status is completed, and that the forward change
+ // was completed.
+ EXPECT_EQ(dhcp_ddns::ST_COMPLETED, name_change->getNcrStatus());
+ EXPECT_TRUE(name_change->getForwardChangeCompleted());
+}
+
+/// @brief Tests the ability to use startTransaction to initate the state
+/// model execution, and DNSClient callback, operator(), to resume the
+/// model with a update failure outcome.
+TEST_F(NameChangeTransactionTest, failedUpdateTest) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ // Launch the transaction by calling startTransaction. The state model
+ // should run up until the "IO" operation is initiated in DOING_UPDATE_ST.
+ ASSERT_NO_THROW(name_change->startTransaction());
+
+ // Vefity that the model is running but waiting, and that the forward
+ // change has not been completed.
+ EXPECT_TRUE(name_change->isModelRunning());
+ EXPECT_TRUE(name_change->isModelWaiting());
+ EXPECT_FALSE(name_change->getForwardChangeCompleted());
+
+ // Simulate completion of DNSClient exchange by invoking the callback, as
+ // DNSClient would. This should cause the state model to progress through
+ // to completion.
+ EXPECT_NO_THROW((*name_change)(DNSClient::TIMEOUT));
+
+ // The model should have worked through to completion.
+ // Verify that the model is done and not failed.
+ EXPECT_TRUE(name_change->isModelDone());
+ EXPECT_FALSE(name_change->didModelFail());
+
+ // Verify that the NCR status is failed and that the forward change
+ // was not completed.
+ EXPECT_EQ(dhcp_ddns::ST_FAILED, name_change->getNcrStatus());
+ EXPECT_FALSE(name_change->getForwardChangeCompleted());
+}
+
+/// @brief Tests update attempt accessors.
+TEST_F(NameChangeTransactionTest, updateAttempts) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Post transaction construction, update attempts should be 0.
+ EXPECT_EQ(0, name_change->getUpdateAttempts());
+
+ // Set it to a known value.
+ name_change->setUpdateAttempts(5);
+
+ // Verify that the value is as expected.
+ EXPECT_EQ(5, name_change->getUpdateAttempts());
+}
+
+/// @brief Tests retryTransition method
+///
+/// Verifies that while the maximum number of update attempts has not
+/// been exceeded, the method will leave the state unchanged but post a
+/// SERVER_SELECTED_EVT. Once the maximum is exceeded, the method should
+/// transition to the state given with a next event of SERVER_IO_ERROR_EVT.
+TEST_F(NameChangeTransactionTest, retryTransition) {
+ // Create the transaction.
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+
+ // Define dictionaries.
+ ASSERT_NO_THROW(name_change->initDictionaries());
+
+ // Transition to a known spot.
+ ASSERT_NO_THROW(name_change->transition(
+ NameChangeStub::DOING_UPDATE_ST,
+ NameChangeStub::SEND_UPDATE_EVT));
+
+ // Verify we are at the known spot.
+ ASSERT_EQ(NameChangeStub::DOING_UPDATE_ST,
+ name_change->getCurrState());
+ ASSERT_EQ(NameChangeStub::SEND_UPDATE_EVT,
+ name_change->getNextEvent());
+
+ // Verify that we have not exceeded maximum number of attempts.
+ ASSERT_LT(name_change->getUpdateAttempts(),
+ NameChangeTransaction::MAX_UPDATE_TRIES_PER_SERVER);
+
+ // Call retryTransition.
+ ASSERT_NO_THROW(name_change->retryTransition(
+ NameChangeTransaction::PROCESS_TRANS_FAILED_ST));
+
+ // Since the number of update attempts is less than the maximum allowed
+ // we should remain in our current state but with next event of
+ // SERVER_SELECTED_EVT posted.
+ ASSERT_EQ(NameChangeStub::DOING_UPDATE_ST,
+ name_change->getCurrState());
+ ASSERT_EQ(NameChangeTransaction::SERVER_SELECTED_EVT,
+ name_change->getNextEvent());
+
+ // Now set the number of attempts to the maximum.
+ name_change->setUpdateAttempts(NameChangeTransaction::
+ MAX_UPDATE_TRIES_PER_SERVER);
+ // Call retryTransition.
+ ASSERT_NO_THROW(name_change->retryTransition(
+ NameChangeTransaction::PROCESS_TRANS_FAILED_ST));
+
+ // Since we have exceeded maximum attempts, we should transition to
+ // PROCESS_UPDATE_FAILED_ST with a next event of SERVER_IO_ERROR_EVT.
+ ASSERT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_change->getCurrState());
+ ASSERT_EQ(NameChangeTransaction::SERVER_IO_ERROR_EVT,
+ name_change->getNextEvent());
+}
+
+/// @brief Tests sendUpdate method when underlying doUpdate throws.
+///
+/// DNSClient::doUpdate can throw for a variety of reasons. This tests
+/// sendUpdate handling of such a throw by passing doUpdate a request
+/// that will not render.
+TEST_F(NameChangeTransactionTest, sendUpdateDoUpdateFailure) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_NO_THROW(name_change->initDictionaries());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ // Set the transaction's request to an empty DNS update.
+ D2UpdateMessagePtr req;
+ ASSERT_NO_THROW(req.reset(new D2UpdateMessage(D2UpdateMessage::OUTBOUND)));
+ ASSERT_NO_THROW(name_change->setDnsUpdateRequest(req));
+
+ // Verify that sendUpdate does not throw, but it should fail because
+ // the request won't render.
+ ASSERT_NO_THROW(name_change->sendUpdate());
+
+ // Verify that we transition to failed state and event.
+ ASSERT_EQ(NameChangeTransaction::PROCESS_TRANS_FAILED_ST,
+ name_change->getCurrState());
+ ASSERT_EQ(NameChangeTransaction::UPDATE_FAILED_EVT,
+ name_change->getNextEvent());
+}
+
+/// @brief Tests sendUpdate method when underlying doUpdate times out.
+TEST_F(NameChangeTransactionTest, sendUpdateTimeout) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_NO_THROW(name_change->initDictionaries());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ // Build a valid request, call sendUpdate and process the response.
+ // Note we have to wait for DNSClient timeout plus a bit more to allow
+ // DNSClient to timeout.
+ // The method, doOneExchange, can suffer fatal assertions which invalidate
+ // not only it but the invoking test as well. In other words, if the
+ // doOneExchange blows up the rest of test is pointless. I use
+ // ASSERT_NO_FATAL_FAILURE to abort the test immediately.
+ ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change,
+ NameChangeTransaction::
+ DNS_UPDATE_DEFAULT_TIMEOUT + 100));
+
+ // Verify that next event is IO_COMPLETED_EVT and DNS status is TIMEOUT.
+ ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+ name_change->getNextEvent());
+ ASSERT_EQ(DNSClient::TIMEOUT, name_change->getDnsUpdateStatus());
+}
+
+/// @brief Tests sendUpdate method when it receives a corrupt response from
+/// the server.
+TEST_F(NameChangeTransactionTest, sendUpdateCorruptResponse) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_NO_THROW(name_change->initDictionaries());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ // Create a server and start it listening.
+ FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+ server.receive(FauxServer::CORRUPT_RESP);
+
+ // Build a valid request, call sendUpdate and process the response.
+ ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+ // Verify that next event is IO_COMPLETED_EVT and DNS status is INVALID.
+ ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+ name_change->getNextEvent());
+ ASSERT_EQ(DNSClient::INVALID_RESPONSE, name_change->getDnsUpdateStatus());
+}
+
+/// @brief Tests sendUpdate method when the exchange succeeds.
+TEST_F(NameChangeTransactionTest, sendUpdate) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ ASSERT_NO_THROW(name_change->initDictionaries());
+ ASSERT_TRUE(name_change->selectFwdServer());
+
+ // Create a server and start it listening.
+ FauxServer server(*io_service_, *(name_change->getCurrentServer()));
+ server.receive (FauxServer::USE_RCODE, dns::Rcode::NOERROR());
+
+ // Build a valid request, call sendUpdate and process the response.
+ ASSERT_NO_FATAL_FAILURE(doOneExchange(name_change));
+
+ // Verify that next event is IO_COMPLETED_EVT and DNS status is SUCCESS.
+ ASSERT_EQ(NameChangeTransaction::IO_COMPLETED_EVT,
+ name_change->getNextEvent());
+ ASSERT_EQ(DNSClient::SUCCESS, name_change->getDnsUpdateStatus());
+
+ // Verify that we have a response and it's Rcode is NOERROR,
+ // and the zone is as expected.
+ D2UpdateMessagePtr response = name_change->getDnsUpdateResponse();
+ ASSERT_TRUE(response);
+ ASSERT_EQ(dns::Rcode::NOERROR().getCode(), response->getRcode().getCode());
+ D2ZonePtr zone = response->getZone();
+ EXPECT_TRUE(zone);
+ EXPECT_EQ("response.example.com.", zone->getName().toText());
+}
+
+/// @brief Tests the prepNewRequest method
+TEST_F(NameChangeTransactionTest, prepNewRequest) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ D2UpdateMessagePtr request;
+
+ // prepNewRequest should fail on empty domain.
+ ASSERT_THROW(request = name_change->prepNewRequest(DdnsDomainPtr()),
+ NameChangeTransactionError);
+
+ // Verify that prepNewRequest fails on invalid zone name.
+ // @todo This test becomes obsolete if/when DdnsDomain enforces valid
+ // names as is done in dns::Name.
+ DdnsDomainPtr bsDomain = makeDomain(".badname","");
+ ASSERT_THROW(request = name_change->prepNewRequest(bsDomain),
+ NameChangeTransactionError);
+
+ // Verify that prepNewRequest properly constructs a message given
+ // valid input.
+ ASSERT_NO_THROW(request = name_change->prepNewRequest(forward_domain_));
+ checkZone(request, forward_domain_->getName());
+}
+
+/// @brief Tests the addLeaseAddressRData method
+TEST_F(NameChangeTransactionTest, addLeaseAddressRData) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ dhcp_ddns::NameChangeRequestPtr ncr = name_change->getNcr();
+
+ // Verify we can add a lease RData to an valid RRset.
+ dns::RRsetPtr rrset(new dns::RRset(dns::Name("bs"), dns::RRClass::IN(),
+ name_change->getAddressRRType(),
+ dns::RRTTL(0)));
+ ASSERT_NO_THROW(name_change->addLeaseAddressRdata(rrset));
+
+ // Verify the Rdata was added and the value is correct.
+ ASSERT_EQ(1, rrset->getRdataCount());
+ dns::RdataIteratorPtr rdata_it = rrset->getRdataIterator();
+ ASSERT_TRUE(rdata_it);
+ EXPECT_EQ(ncr->getIpAddress(), rdata_it->getCurrent().toText());
+
+}
+
+/// @brief Tests the addDhcidRData method
+TEST_F(NameChangeTransactionTest, addDhcidRdata) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ dhcp_ddns::NameChangeRequestPtr ncr = name_change->getNcr();
+
+ // Verify we can add a lease RData to an valid RRset.
+ dns::RRsetPtr rrset(new dns::RRset(dns::Name("bs"), dns::RRClass::IN(),
+ dns::RRType::DHCID(), dns::RRTTL(0)));
+ ASSERT_NO_THROW(name_change->addDhcidRdata(rrset));
+
+ // Verify the Rdata was added and the value is correct.
+ ASSERT_EQ(1, rrset->getRdataCount());
+ dns::RdataIteratorPtr rdata_it = rrset->getRdataIterator();
+ ASSERT_TRUE(rdata_it);
+
+ const std::vector<uint8_t>& ncr_dhcid = ncr->getDhcid().getBytes();
+ util::InputBuffer buffer(ncr_dhcid.data(), ncr_dhcid.size());
+ dns::rdata::in::DHCID rdata_ref(buffer, ncr_dhcid.size());
+ EXPECT_EQ(0, rdata_ref.compare(rdata_it->getCurrent()));
+}
+
+/// @brief Tests the addPtrData method
+TEST_F(NameChangeTransactionTest, addPtrRdata) {
+ NameChangeStubPtr name_change;
+ ASSERT_NO_THROW(name_change = makeCannedTransaction());
+ dhcp_ddns::NameChangeRequestPtr ncr = name_change->getNcr();
+
+ // Verify we can add a PTR RData to an valid RRset.
+ dns::RRsetPtr rrset (new dns::RRset(dns::Name("bs"), dns::RRClass::IN(),
+ dns::RRType::PTR(), dns::RRTTL(0)));
+ ASSERT_NO_THROW(name_change->addPtrRdata(rrset));
+
+ // Verify the Rdata was added and the value is correct.
+ ASSERT_EQ(1, rrset->getRdataCount());
+ dns::RdataIteratorPtr rdata_it = rrset->getRdataIterator();
+ ASSERT_TRUE(rdata_it);
+
+ EXPECT_EQ(ncr->getFqdn(), rdata_it->getCurrent().toText());
+}
+
+}; // anonymous namespace
diff --git a/src/bin/d2/tests/state_model_unittests.cc b/src/bin/d2/tests/state_model_unittests.cc
new file mode 100644
index 0000000..63a0203
--- /dev/null
+++ b/src/bin/d2/tests/state_model_unittests.cc
@@ -0,0 +1,839 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <d2/state_model.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::d2;
+
+namespace {
+
+/// @brief Test derivation of StateModel for exercising state model mechanics.
+///
+/// This class facilitates testing by making non-public methods accessible so
+/// they can be invoked directly in test routines. It implements a very
+/// rudimentary state model, sufficient to test the state model mechanics
+/// supplied by the base class.
+class StateModelTest : public StateModel, public testing::Test {
+public:
+
+ ///@brief StateModelTest states
+ ///@brief Fake state used for handler mapping tests.
+ static const int DUMMY_ST = SM_DERIVED_STATE_MIN + 1;
+
+ ///@brief Starting state for the test state model.
+ static const int READY_ST = SM_DERIVED_STATE_MIN + 2;
+
+ ///@brief State which simulates doing asynchronous work.
+ static const int DO_WORK_ST = SM_DERIVED_STATE_MIN + 3;
+
+ ///@brief State which finishes off processing.
+ static const int DONE_ST = SM_DERIVED_STATE_MIN + 4;
+
+ // StateModelTest events
+ ///@brief Event used to trigger initiation of asynchronous work.
+ static const int WORK_START_EVT = SM_DERIVED_EVENT_MIN + 1;
+
+ ///@brief Event issued when the asynchronous work "completes".
+ static const int WORK_DONE_EVT = SM_DERIVED_EVENT_MIN + 2;
+
+ ///@brief Event issued when all the work is done.
+ static const int ALL_DONE_EVT = SM_DERIVED_EVENT_MIN + 3;
+
+ ///@brief Event used to trigger an attempt to transition to bad state
+ static const int FORCE_UNDEFINED_ST_EVT = SM_DERIVED_EVENT_MIN + 4;
+
+ ///@brief Event used to trigger an attempt to transition to bad state
+ static const int SIMULATE_ERROR_EVT = SM_DERIVED_EVENT_MIN + 5;
+
+ /// @brief Constructor
+ ///
+ /// Parameters match those needed by StateModel.
+ StateModelTest() : dummy_called_(false), work_completed_(false),
+ failure_explanation_("") {
+ }
+ /// @brief Destructor
+ virtual ~StateModelTest() {
+ }
+
+ /// @brief Fetches the value of the dummy called flag.
+ bool getDummyCalled() {
+ return (dummy_called_);
+ }
+
+ /// @brief StateHandler for fake state, DummyState.
+ ///
+ /// It simply sets the dummy called flag to indicate that this method
+ /// was invoked.
+ void dummyHandler() {
+ dummy_called_ = true;
+ }
+
+ /// @brief Returns the failure explanation string.
+ ///
+ /// This value is set only via onModelFailure and it stores whatever
+ /// explanation that method was passed.
+ const std::string& getFailureExplanation() {
+ return (failure_explanation_);
+ }
+
+ /// @brief Returns indication of whether or not the model succeeded.
+ ///
+ /// If true, this indicates that the test model executed correctly through
+ /// to completion. The flag is only by the DONE_ST handler.
+ bool getWorkCompleted() {
+ return (work_completed_);
+ }
+
+ /// @brief State handler for the READY_ST.
+ ///
+ /// Serves as the starting state handler, it consumes the
+ /// START_EVT "transitioning" to the state, DO_WORK_ST and
+ /// sets the next event to WORK_START_EVT.
+ void readyHandler() {
+ switch(getNextEvent()) {
+ case START_EVT:
+ transition(DO_WORK_ST, WORK_START_EVT);
+ break;
+ default:
+ // its bogus
+ isc_throw(StateModelError, "readyHandler:invalid event: "
+ << getContextStr());
+ }
+ }
+
+ /// @brief State handler for the DO_WORK_ST.
+ ///
+ /// Simulates a state that starts some form of asynchronous work.
+ /// When next event is WORK_START_EVT it sets the status to pending
+ /// and signals the state model must "wait" for an event by setting
+ /// next event to NOP_EVT.
+ ///
+ /// When next event is IO_COMPLETED_EVT, it transitions to the state,
+ /// DONE_ST, and sets the next event to WORK_DONE_EVT.
+ void doWorkHandler() {
+ switch(getNextEvent()) {
+ case WORK_START_EVT:
+ postNextEvent(NOP_EVT);
+ break;
+ case WORK_DONE_EVT:
+ work_completed_ = true;
+ transition(DONE_ST, ALL_DONE_EVT);
+ break;
+ case FORCE_UNDEFINED_ST_EVT:
+ transition(9999, ALL_DONE_EVT);
+ break;
+ case SIMULATE_ERROR_EVT:
+ throw std::logic_error("Simulated Unexpected Error");
+ break;
+ default:
+ // its bogus
+ isc_throw(StateModelError, "doWorkHandler:invalid event: "
+ << getContextStr());
+ }
+ }
+
+ /// @brief State handler for the DONE_ST.
+ ///
+ /// This is the last state in the model. Note that it sets the
+ /// status to completed and next event to NOP_EVT.
+ void doneWorkHandler() {
+ switch(getNextEvent()) {
+ case ALL_DONE_EVT:
+ endModel();
+ break;
+ default:
+ // its bogus
+ isc_throw(StateModelError, "doneWorkHandler:invalid event: "
+ << getContextStr());
+ }
+ }
+
+ /// @brief Construct the event dictionary.
+ virtual void defineEvents() {
+ // Invoke the base call implementation first.
+ StateModel::defineEvents();
+
+ // Define our events.
+ defineEvent(WORK_START_EVT, "WORK_START_EVT");
+ defineEvent(WORK_DONE_EVT , "WORK_DONE_EVT");
+ defineEvent(ALL_DONE_EVT, "ALL_DONE_EVT");
+ defineEvent(FORCE_UNDEFINED_ST_EVT, "FORCE_UNDEFINED_ST_EVT");
+ defineEvent(SIMULATE_ERROR_EVT, "SIMULATE_ERROR_EVT");
+ }
+
+ /// @brief Verify the event dictionary.
+ virtual void verifyEvents() {
+ // Invoke the base call implementation first.
+ StateModel::verifyEvents();
+
+ // Verify our events.
+ getEvent(WORK_START_EVT);
+ getEvent(WORK_DONE_EVT);
+ getEvent(ALL_DONE_EVT);
+ getEvent(FORCE_UNDEFINED_ST_EVT);
+ getEvent(SIMULATE_ERROR_EVT);
+ }
+
+ /// @brief Construct the state dictionary.
+ virtual void defineStates() {
+ // Invoke the base call implementation first.
+ StateModel::defineStates();
+
+ // Define our states.
+ defineState(DUMMY_ST, "DUMMY_ST",
+ boost::bind(&StateModelTest::dummyHandler, this));
+
+ defineState(READY_ST, "READY_ST",
+ boost::bind(&StateModelTest::readyHandler, this));
+
+ defineState(DO_WORK_ST, "DO_WORK_ST",
+ boost::bind(&StateModelTest::doWorkHandler, this));
+
+ defineState(DONE_ST, "DONE_ST",
+ boost::bind(&StateModelTest::doneWorkHandler, this));
+ }
+
+ /// @brief Verify the state dictionary.
+ virtual void verifyStates() {
+ // Invoke the base call implementation first.
+ StateModel::verifyStates();
+
+ // Verify our states.
+ getState(DUMMY_ST);
+ getState(READY_ST);
+ getState(DO_WORK_ST);
+ getState(DONE_ST);
+ }
+
+ /// @brief Manually construct the event and state dictionaries.
+ /// This allows testing without running startModel.
+ void initDictionaires() {
+ ASSERT_NO_THROW(defineEvents());
+ ASSERT_NO_THROW(verifyEvents());
+ ASSERT_NO_THROW(defineStates());
+ ASSERT_NO_THROW(verifyStates());
+ }
+
+ /// @brief Tests the event dictionary entry for the given event value.
+ bool checkEvent(const int value, const std::string& label) {
+ EventPtr event;
+ try {
+ event = getEvent(value);
+ EXPECT_TRUE(event);
+ EXPECT_EQ(value, event->getValue());
+ EXPECT_EQ(label, event->getLabel());
+ } catch (const std::exception& ex) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// @brief Tests the state dictionary entry for the given state value.
+ bool checkState(const int value, const std::string& label) {
+ EventPtr state;
+ try {
+ state = getState(value);
+ EXPECT_TRUE(state);
+ EXPECT_EQ(value, state->getValue());
+ EXPECT_EQ(label, state->getLabel());
+ } catch (const std::exception& ex) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /// @brief Handler called when the model suffers an execution error.
+ virtual void onModelFailure(const std::string& explanation) {
+ failure_explanation_ = explanation;
+ }
+
+ /// @brief Indicator of whether or not the DUMMY_ST handler has been called.
+ bool dummy_called_;
+
+ /// @brief Indicator of whether or not DONE_ST handler has been called.
+ bool work_completed_;
+
+ /// @brief Stores the failure explanation
+ std::string failure_explanation_;
+};
+
+// Declare them so gtest can see them.
+const int StateModelTest::DUMMY_ST;
+const int StateModelTest::READY_ST;
+const int StateModelTest::DO_WORK_ST;
+const int StateModelTest::DONE_ST;
+const int StateModelTest::WORK_START_EVT;
+const int StateModelTest::WORK_DONE_EVT;
+const int StateModelTest::ALL_DONE_EVT;
+
+/// @brief Checks the fundamentals of defining and retrieving events.
+TEST_F(StateModelTest, eventDefinition) {
+ // After construction, the event dictionary should be empty. Verify that
+ // getEvent will throw when event is not defined.
+ EXPECT_THROW(getEvent(NOP_EVT), StateModelError);
+
+ // Verify that we can add a handler to the map.
+ ASSERT_NO_THROW(defineEvent(NOP_EVT, "NOP_EVT"));
+
+ // Verify that we can find the event by value and its content is correct.
+ EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT"));
+
+ // Verify that we cannot add a duplicate.
+ ASSERT_THROW(defineEvent(NOP_EVT, "NOP_EVT"), StateModelError);
+
+ // Verify that we can still find the event.
+ EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT"));
+}
+
+/// @brief Tests event dictionary construction and verification.
+TEST_F(StateModelTest, eventDictionary) {
+ // After construction, the event dictionary should be empty.
+ // Make sure that verifyEvents() throws.
+ EXPECT_THROW(verifyEvents(), StateModelError);
+
+ // Construct the dictionary and verify it.
+ EXPECT_NO_THROW(defineEvents());
+ EXPECT_NO_THROW(verifyEvents());
+
+ // Verify base class events are defined.
+ EXPECT_TRUE(checkEvent(NOP_EVT, "NOP_EVT"));
+ EXPECT_TRUE(checkEvent(START_EVT, "START_EVT"));
+ EXPECT_TRUE(checkEvent(END_EVT, "END_EVT"));
+ EXPECT_TRUE(checkEvent(FAIL_EVT, "FAIL_EVT"));
+
+ // Verify stub class events are defined.
+ EXPECT_TRUE(checkEvent(WORK_START_EVT, "WORK_START_EVT"));
+ EXPECT_TRUE(checkEvent(WORK_DONE_EVT, "WORK_DONE_EVT"));
+ EXPECT_TRUE(checkEvent(ALL_DONE_EVT, "ALL_DONE_EVT"));
+ EXPECT_TRUE(checkEvent(FORCE_UNDEFINED_ST_EVT, "FORCE_UNDEFINED_ST_EVT"));
+ EXPECT_TRUE(checkEvent(SIMULATE_ERROR_EVT, "SIMULATE_ERROR_EVT"));
+
+ // Verify that undefined events are handled correctly.
+ EXPECT_THROW(getEvent(9999), StateModelError);
+ EXPECT_EQ(LabeledValueSet::UNDEFINED_LABEL, getEventLabel(9999));
+}
+
+/// @brief General testing of event context accessors.
+/// Most if not all of these are also tested as a byproduct off larger tests.
+TEST_F(StateModelTest, eventContextAccessors) {
+ // Construct the event definitions, normally done by startModel.
+ ASSERT_NO_THROW(defineEvents());
+ ASSERT_NO_THROW(verifyEvents());
+
+ // Verify the post-construction values.
+ EXPECT_EQ(NOP_EVT, getNextEvent());
+ EXPECT_EQ(NOP_EVT, getLastEvent());
+
+ // Call setEvent which will update both next event and last event.
+ EXPECT_NO_THROW(postNextEvent(START_EVT));
+
+ // Verify the values are what we expect.
+ EXPECT_EQ(START_EVT, getNextEvent());
+ EXPECT_EQ(NOP_EVT, getLastEvent());
+
+ // Call setEvent again.
+ EXPECT_NO_THROW(postNextEvent(WORK_START_EVT));
+
+ // Verify the values are what we expect.
+ EXPECT_EQ(WORK_START_EVT, getNextEvent());
+ EXPECT_EQ(START_EVT, getLastEvent());
+
+ // Verify that posting an undefined event throws.
+ EXPECT_THROW(postNextEvent(9999), StateModelError);
+}
+
+/// @brief Tests the fundamental methods used for state handler mapping.
+/// Verifies the ability to search for and add entries in the state handler map.
+TEST_F(StateModelTest, stateDefinition) {
+ // After construction, the state dictionary should be empty. Verify that
+ // getState will throw when, state is not defined.
+ EXPECT_THROW(getState(READY_ST), StateModelError);
+
+ // Verify that we can add a state to the dictionary.
+ ASSERT_NO_THROW(defineState(READY_ST, "READY_ST",
+ boost::bind(&StateModelTest::dummyHandler,
+ this)));
+
+ // Verify that we can find the state by its value.
+ StatePtr state;
+ EXPECT_NO_THROW(state = getState(READY_ST));
+ EXPECT_TRUE(state);
+
+ // Verify the state's value and label.
+ EXPECT_EQ(READY_ST, state->getValue());
+ EXPECT_EQ("READY_ST", state->getLabel());
+
+ // Now verify that retrieved state's handler executes the correct method.
+ // Make sure the dummy called flag is false prior to invocation.
+ EXPECT_FALSE(getDummyCalled());
+
+ // Invoke the state's handler.
+ EXPECT_NO_THROW(state->run());
+
+ // Verify the dummy called flag is now true.
+ EXPECT_TRUE(getDummyCalled());
+
+ // Verify that we cannot add a duplicate.
+ EXPECT_THROW(defineState(READY_ST, "READY_ST",
+ boost::bind(&StateModelTest::readyHandler, this)),
+ StateModelError);
+
+ // Verify that we can still find the state.
+ EXPECT_NO_THROW(getState(READY_ST));
+}
+
+/// @brief Tests state dictionary initialization and validation.
+/// This tests the basic concept of state dictionary initialization and
+/// verification by manually invoking the methods normally called by startModel.
+TEST_F(StateModelTest, stateDictionary) {
+ // Verify that the map validation throws prior to the dictionary being
+ // initialized.
+ EXPECT_THROW(verifyStates(), StateModelError);
+
+ // Construct the dictionary and verify it.
+ ASSERT_NO_THROW(defineStates());
+ EXPECT_NO_THROW(verifyStates());
+
+ // Verify the base class states.
+ EXPECT_TRUE(checkState(NEW_ST, "NEW_ST"));
+ EXPECT_TRUE(checkState(END_ST, "END_ST"));
+
+ // Verify stub class states.
+ EXPECT_TRUE(checkState(DUMMY_ST, "DUMMY_ST"));
+ EXPECT_TRUE(checkState(READY_ST, "READY_ST"));
+ EXPECT_TRUE(checkState(DO_WORK_ST, "DO_WORK_ST"));
+ EXPECT_TRUE(checkState(DONE_ST, "DONE_ST"));
+
+ // Verify that undefined states are handled correctly.
+ EXPECT_THROW(getState(9999), StateModelError);
+ EXPECT_EQ(LabeledValueSet::UNDEFINED_LABEL, getStateLabel(9999));
+}
+
+/// @brief General testing of state context accessors.
+/// Most if not all of these are also tested as a byproduct off larger tests.
+TEST_F(StateModelTest, stateContextAccessors) {
+ // setState will throw unless we initialize the handler map.
+ ASSERT_NO_THROW(defineStates());
+ ASSERT_NO_THROW(verifyStates());
+
+ // Verify post-construction state values.
+ EXPECT_EQ(NEW_ST, getCurrState());
+ EXPECT_EQ(NEW_ST, getPrevState());
+
+ // Call setState which will update both state and previous state.
+ EXPECT_NO_THROW(setState(READY_ST));
+
+ // Verify the values are what we expect.
+ EXPECT_EQ(READY_ST, getCurrState());
+ EXPECT_EQ(NEW_ST, getPrevState());
+
+ // Call setState again.
+ EXPECT_NO_THROW(setState(DO_WORK_ST));
+
+ // Verify the values are what we expect.
+ EXPECT_EQ(DO_WORK_ST, getCurrState());
+ EXPECT_EQ(READY_ST, getPrevState());
+
+ // Verify that calling setState with an state that has no handler
+ // will throw.
+ EXPECT_THROW(setState(-1), StateModelError);
+
+ // Verify that calling setState with NEW_ST is ok.
+ EXPECT_NO_THROW(setState(NEW_ST));
+
+ // Verify that calling setState with END_ST is ok.
+ EXPECT_NO_THROW(setState(END_ST));
+
+ // Verify that calling setState with an undefined state throws.
+ EXPECT_THROW(setState(9999), StateModelError);
+}
+
+/// @brief Checks that invoking runModel prior to startModel is not allowed.
+TEST_F(StateModelTest, runBeforeStart) {
+ // Verify that the failure explanation is empty and work is not done.
+ EXPECT_TRUE(getFailureExplanation().empty());
+
+ // Attempt to call runModel before startModel. This should result in an
+ // orderly model failure.
+ ASSERT_NO_THROW(runModel(START_EVT));
+
+ // Check that state and event are correct.
+ EXPECT_EQ(END_ST, getCurrState());
+ EXPECT_EQ(FAIL_EVT, getNextEvent());
+
+ // Verify that failure explanation is not empty.
+ EXPECT_FALSE(getFailureExplanation().empty());
+}
+
+/// @brief Tests that the endModel may be used to transition the model to
+/// a normal conclusion.
+TEST_F(StateModelTest, transitionWithEnd) {
+ // Init dictionaries manually, normally done by startModel.
+ initDictionaires();
+
+ // call transition to move from NEW_ST to DUMMY_ST with START_EVT
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+ // Verify that state and event members are as expected.
+ EXPECT_EQ(DUMMY_ST, getCurrState());
+ EXPECT_EQ(NEW_ST, getPrevState());
+ EXPECT_EQ(START_EVT, getNextEvent());
+ EXPECT_EQ(NOP_EVT, getLastEvent());
+
+ // Call endModel to transition us to the end of the model.
+ EXPECT_NO_THROW(endModel());
+
+ // Verify state and event members are correctly set.
+ EXPECT_EQ(END_ST, getCurrState());
+ EXPECT_EQ(DUMMY_ST, getPrevState());
+ EXPECT_EQ(END_EVT, getNextEvent());
+ EXPECT_EQ(START_EVT, getLastEvent());
+}
+
+/// @brief Tests that the abortModel may be used to transition the model to
+/// failed conclusion.
+TEST_F(StateModelTest, transitionWithAbort) {
+ // Init dictionaries manually, normally done by startModel.
+ initDictionaires();
+
+ // call transition to move from NEW_ST to DUMMY_ST with START_EVT
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+ // Verify that state and event members are as expected.
+ EXPECT_EQ(DUMMY_ST, getCurrState());
+ EXPECT_EQ(NEW_ST, getPrevState());
+ EXPECT_EQ(START_EVT, getNextEvent());
+ EXPECT_EQ(NOP_EVT, getLastEvent());
+
+ // Call endModel to transition us to the end of the model.
+ EXPECT_NO_THROW(abortModel("test invocation"));
+
+ // Verify state and event members are correctly set.
+ EXPECT_EQ(END_ST, getCurrState());
+ EXPECT_EQ(DUMMY_ST, getPrevState());
+ EXPECT_EQ(FAIL_EVT, getNextEvent());
+ EXPECT_EQ(START_EVT, getLastEvent());
+}
+
+/// @brief Tests that the boolean indicators for on state entry and exit
+/// work properly.
+TEST_F(StateModelTest, doFlags) {
+ // Init dictionaries manually, normally done by startModel.
+ initDictionaires();
+
+ // Verify that "do" flags are false.
+ EXPECT_FALSE(doOnEntry());
+ EXPECT_FALSE(doOnExit());
+
+ // call transition to move from NEW_ST to DUMMY_ST with START_EVT
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+ // We are transitioning states, so "do" flags should be true.
+ EXPECT_TRUE(doOnEntry());
+ EXPECT_TRUE(doOnExit());
+
+ // "do" flags are one-shots, so they should now both be false.
+ EXPECT_FALSE(doOnEntry());
+ EXPECT_FALSE(doOnExit());
+
+ // call transition to re-enter same state, "do" flags should be false.
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+ // "do" flags should be false.
+ EXPECT_FALSE(doOnEntry());
+ EXPECT_FALSE(doOnExit());
+
+}
+
+/// @brief Verifies that the model status methods accurately reflect the model
+/// status. It also verifies that the dictionaries can be modified before
+/// the model is running but not after.
+TEST_F(StateModelTest, statusMethods) {
+ // Init dictionaries manually, normally done by startModel.
+ initDictionaires();
+
+ // After construction, state model is "new", all others should be false.
+ EXPECT_TRUE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_FALSE(isModelDone());
+ EXPECT_FALSE(didModelFail());
+
+ // Verify that events and states can be added before the model is started.
+ EXPECT_NO_THROW(defineEvent(9998, "9998"));
+ EXPECT_NO_THROW(defineState(9998, "9998",
+ boost::bind(&StateModelTest::readyHandler,
+ this)));
+
+ // "START" the model.
+ // Fake out starting the model by calling transition to move from NEW_ST
+ // to DUMMY_ST with START_EVT. If we used startModel this would blow by
+ // the status of "running" but not "waiting".
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+
+ // Verify that events and states cannot be added after the model is started.
+ EXPECT_THROW(defineEvent(9999, "9999"), StateModelError);
+ EXPECT_THROW(defineState(9999, "9999",
+ boost::bind(&StateModelTest::readyHandler, this)),
+ StateModelError);
+
+ // The state and event combos set above, should show the model as
+ // "running", all others should be false.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_TRUE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_FALSE(isModelDone());
+ EXPECT_FALSE(didModelFail());
+
+ // call transition to submit NOP_EVT to current state, DUMMY_ST.
+ EXPECT_NO_THROW(transition(DUMMY_ST, NOP_EVT));
+
+ // Verify the status methods are correct: with next event set to NOP_EVT,
+ // model should be "running" and "waiting".
+ EXPECT_FALSE(isModelNew());
+ EXPECT_TRUE(isModelRunning());
+ EXPECT_TRUE(isModelWaiting());
+ EXPECT_FALSE(isModelDone());
+ EXPECT_FALSE(didModelFail());
+
+ // Call endModel to transition us to the end of the model.
+ EXPECT_NO_THROW(endModel());
+
+ // With state set to END_ST, model should be done.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+ EXPECT_FALSE(didModelFail());
+}
+
+/// @brief Tests that the model status methods are correct after a model
+/// failure.
+TEST_F(StateModelTest, statusMethodsOnFailure) {
+ // Construct the event and state definitions, normally done by startModel.
+ ASSERT_NO_THROW(defineEvents());
+ ASSERT_NO_THROW(verifyEvents());
+ ASSERT_NO_THROW(defineStates());
+ ASSERT_NO_THROW(verifyStates());
+
+ // call transition to move from NEW_ST to DUMMY_ST with START_EVT
+ EXPECT_NO_THROW(transition(DUMMY_ST, START_EVT));
+
+ // Call endModel to transition us to the end of the model.
+ EXPECT_NO_THROW(abortModel("test invocation"));
+
+ // With state set to END_ST, model should be done.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+ EXPECT_TRUE(didModelFail());
+}
+
+/// @brief Checks that the context strings accurately reflect context and
+/// are safe to invoke.
+TEST_F(StateModelTest, contextStrs) {
+ // Verify context methods do not throw prior to dictionary init.
+ ASSERT_NO_THROW(getContextStr());
+ ASSERT_NO_THROW(getPrevContextStr());
+
+ // Construct the event and state definitions, normally done by startModel.
+ ASSERT_NO_THROW(defineEvents());
+ ASSERT_NO_THROW(verifyEvents());
+ ASSERT_NO_THROW(defineStates());
+ ASSERT_NO_THROW(verifyStates());
+
+ // transition uses setState and setEvent, testing it tests all three.
+ EXPECT_NO_THROW(transition(READY_ST, START_EVT));
+
+ // Verify the current context string depicts correct state and event.
+ std::string ctx_str;
+ ASSERT_NO_THROW(ctx_str = getContextStr());
+ EXPECT_NE(std::string::npos, ctx_str.find(getStateLabel(READY_ST)));
+ EXPECT_NE(std::string::npos, ctx_str.find(getEventLabel(START_EVT)));
+
+ // Verify the previous context string depicts correct state and event.
+ ASSERT_NO_THROW(ctx_str = getPrevContextStr());
+ EXPECT_NE(std::string::npos, ctx_str.find(getStateLabel(NEW_ST)));
+ EXPECT_NE(std::string::npos, ctx_str.find(getEventLabel(NOP_EVT)));
+}
+
+/// @brief Tests that undefined states are handled gracefully.
+/// This test verifies that attempting to transition to an undefined state,
+/// which constitutes a model violation, results in an orderly model failure.
+TEST_F(StateModelTest, undefinedState) {
+ // Verify that the failure explanation is empty and work is not done.
+ EXPECT_TRUE(getFailureExplanation().empty());
+ EXPECT_FALSE(getWorkCompleted());
+
+ // First, lets execute the state model to a known valid point, by
+ // calling startModel. This should run the model through to DO_WORK_ST.
+ ASSERT_NO_THROW(startModel(READY_ST));
+
+ // Verify we are in the state of DO_WORK_ST with event of NOP_EVT.
+ EXPECT_EQ(DO_WORK_ST, getCurrState());
+ EXPECT_EQ(NOP_EVT, getNextEvent());
+
+ // Resume the model with next event set to cause the DO_WORK_ST handler
+ // to transition to an undefined state. This should cause it to return
+ // without throwing and yield a failed model.
+ EXPECT_NO_THROW(runModel(FORCE_UNDEFINED_ST_EVT));
+
+ // Verify that status methods are correct: model is done but failed.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+ EXPECT_TRUE(didModelFail());
+
+ // Verify that failure explanation is not empty.
+ EXPECT_FALSE(getFailureExplanation().empty());
+
+ // Verify that work completed flag is still false.
+ EXPECT_FALSE(getWorkCompleted());
+}
+
+/// @brief Tests that an unexpected exception thrown by a state handler is
+/// handled gracefully. State models are supposed to account for and handle
+/// all errors that they actions (i.e. handlers) may cause. In the event they
+/// do not, this constitutes a model violation. This test verifies such
+/// violations are handled correctly and result in an orderly model failure.
+TEST_F(StateModelTest, unexpectedError) {
+ // Verify that the failure explanation is empty and work is not done.
+ EXPECT_TRUE(getFailureExplanation().empty());
+ EXPECT_FALSE(getWorkCompleted());
+
+ // First, lets execute the state model to a known valid point, by
+ // calling startModel with a start state of READY_ST.
+ // This should run the model through to DO_WORK_ST.
+ ASSERT_NO_THROW(startModel(READY_ST));
+
+ // Verify we are in the state of DO_WORK_ST with event of NOP_EVT.
+ EXPECT_EQ(DO_WORK_ST, getCurrState());
+ EXPECT_EQ(NOP_EVT, getNextEvent());
+
+ // Resume the model with next event set to cause the DO_WORK_ST handler
+ // to transition to an undefined state. This should cause it to return
+ // without throwing and yield a failed model.
+ EXPECT_NO_THROW(runModel(SIMULATE_ERROR_EVT));
+
+ // Verify that status methods are correct: model is done but failed.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+ EXPECT_TRUE(didModelFail());
+
+ // Verify that failure explanation is not empty.
+ EXPECT_FALSE(getFailureExplanation().empty());
+
+ // Verify that work completed flag is still false.
+ EXPECT_FALSE(getWorkCompleted());
+}
+
+/// @brief Tests that undefined events are handled gracefully.
+/// This test verifies that submitting an undefined event to the state machine
+/// results, which constitutes a model violation, results in an orderly model
+/// failure.
+TEST_F(StateModelTest, undefinedEvent) {
+ // Verify that the failure explanation is empty and work is not done.
+ EXPECT_TRUE(getFailureExplanation().empty());
+ EXPECT_FALSE(getWorkCompleted());
+
+ // First, lets execute the state model to a known valid point, by
+ // calling startModel with a start state of READY_ST.
+ // This should run the model through to DO_WORK_ST.
+ ASSERT_NO_THROW(startModel(READY_ST));
+
+ // Verify we are in the state of DO_WORK_ST with event of NOP_EVT.
+ EXPECT_EQ(DO_WORK_ST, getCurrState());
+ EXPECT_EQ(NOP_EVT, getNextEvent());
+
+ // Attempting to post an undefined event within runModel should cause it
+ // to return without throwing and yield a failed model.
+ EXPECT_NO_THROW(runModel(9999));
+
+ // Verify that status methods are correct: model is done but failed.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+ EXPECT_TRUE(didModelFail());
+
+ // Verify that failure explanation is not empty.
+ EXPECT_FALSE(getFailureExplanation().empty());
+
+ // Verify that work completed flag is still false.
+ EXPECT_FALSE(getWorkCompleted());
+}
+
+/// @brief Test the basic mechanics of state model execution.
+/// This test exercises the ability to execute state model from start to
+/// finish, including the handling of a asynchronous IO operation.
+TEST_F(StateModelTest, stateModelTest) {
+ // Verify that status methods are correct: model is new.
+ EXPECT_TRUE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_FALSE(isModelDone());
+
+ // Verify that the failure explanation is empty and work is not done.
+ EXPECT_TRUE(getFailureExplanation().empty());
+ EXPECT_FALSE(getWorkCompleted());
+
+ // Launch the transaction by calling startModel. The state model
+ // should run up until the simulated async work operation is initiated
+ // in DO_WORK_ST.
+ ASSERT_NO_THROW(startModel(READY_ST));
+
+ // Verify that we are now in state of DO_WORK_ST, the last event was
+ // WORK_START_EVT, the next event is NOP_EVT.
+ EXPECT_EQ(DO_WORK_ST, getCurrState());
+ EXPECT_EQ(WORK_START_EVT, getLastEvent());
+ EXPECT_EQ(NOP_EVT, getNextEvent());
+
+ // Simulate completion of async work completion by resuming runModel with
+ // an event of WORK_DONE_EVT.
+ ASSERT_NO_THROW(runModel(WORK_DONE_EVT));
+
+ // Verify that the state model has progressed through to completion:
+ // it is in the DONE_ST, the status is ST_COMPLETED, and the next event
+ // is NOP_EVT.
+ EXPECT_EQ(END_ST, getCurrState());
+ EXPECT_EQ(END_EVT, getNextEvent());
+
+ // Verify that status methods are correct: model done.
+ EXPECT_FALSE(isModelNew());
+ EXPECT_FALSE(isModelRunning());
+ EXPECT_FALSE(isModelWaiting());
+ EXPECT_TRUE(isModelDone());
+
+ // Verify that failure explanation is empty.
+ EXPECT_TRUE(getFailureExplanation().empty());
+
+ // Verify that work completed flag is true.
+ EXPECT_TRUE(getWorkCompleted());
+}
+
+}
diff --git a/src/bin/d2/tests/test_data_files_config.h.in b/src/bin/d2/tests/test_data_files_config.h.in
new file mode 100644
index 0000000..b351f88
--- /dev/null
+++ b/src/bin/d2/tests/test_data_files_config.h.in
@@ -0,0 +1,17 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @brief Path to D2 source dir so tests against the dhcp-ddns.spec file
+/// can find it reliably.
+#define D2_SRC_DIR "@abs_top_srcdir@/src/bin/d2"
diff --git a/src/bin/dbutil/b10-dbutil.xml b/src/bin/dbutil/b10-dbutil.xml
index c93d060..f32301f 100644
--- a/src/bin/dbutil/b10-dbutil.xml
+++ b/src/bin/dbutil/b10-dbutil.xml
@@ -68,7 +68,7 @@
</para>
<para>
- <command>b10-dbutil</command> operates in one of two modesr: check mode
+ <command>b10-dbutil</command> operates in one of two modes: check mode
or upgrade mode.
</para>
diff --git a/src/bin/dbutil/dbutil.py.in b/src/bin/dbutil/dbutil.py.in
index 292a0ba..3b7b735 100755
--- a/src/bin/dbutil/dbutil.py.in
+++ b/src/bin/dbutil/dbutil.py.in
@@ -58,6 +58,7 @@ sys.excepthook = my_except_hook
import os, sqlite3, shutil
from optparse import OptionParser
import isc.util.process
+import isc.util.traceback_handler
import isc.log
from isc.log_messages.dbutil_messages import *
@@ -566,9 +567,11 @@ def parse_command():
sys.exit(EXIT_COMMAND_ERROR)
-if __name__ == "__main__":
+def main():
(options, args) = parse_command()
+ global logger
+
if options.verbose:
isc.log.init("b10-dbutil", "DEBUG", 99)
logger = isc.log.Logger("dbutil")
@@ -619,3 +622,6 @@ if __name__ == "__main__":
exit_code = EXIT_UPGRADE_ERROR
sys.exit(exit_code)
+
+if __name__ == "__main__":
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/dbutil/tests/dbutil_test.sh.in b/src/bin/dbutil/tests/dbutil_test.sh.in
index f14c564..e11c01b 100755
--- a/src/bin/dbutil/tests/dbutil_test.sh.in
+++ b/src/bin/dbutil/tests/dbutil_test.sh.in
@@ -140,7 +140,24 @@ get_schema() {
db1=@abs_builddir@/dbutil_test_schema_$$
copy_file $1 $db1
+ # The purpose of the following sed command is to join multi-line SQL
+ # statements to form single-line SQL statements.
+ #
+ # The sed command is explained as follows:
+ # ':a' creates a new label "a"
+ # 'N' appends the next line to the pattern space
+ # '$!ba' if it's not the last line, branch to "a"
+ #
+ # The above makes sed loop over the entire sqlite3 output. At this
+ # point, the pattern space contain all lines in the sqlite3 output.
+ #
+ # 's/,[\ ]*\n/, /g' then substitutes lines trailing with comma
+ # followed by zero or more spaces and the newline character, with
+ # just a comma and a single space.
+
db_schema=`sqlite3 $db1 '.schema' | \
+ sed -e ':a' -e 'N' -e '$!ba' -e 's/,[\ ]*\n/, /g' | \
+ sort | \
awk '{line = line $0} END {print line}' | \
sed -e 's/ //g' | \
tr [:upper:] [:lower:]`
diff --git a/src/bin/ddns/ddns.py.in b/src/bin/ddns/ddns.py.in
index 47f67ca..6bb81c5 100755
--- a/src/bin/ddns/ddns.py.in
+++ b/src/bin/ddns/ddns.py.in
@@ -28,6 +28,7 @@ from isc.config.ccsession import *
from isc.config.module_spec import ModuleSpecError
from isc.cc import SessionError, SessionTimeout, ProtocolError
import isc.util.process
+import isc.util.traceback_handler
import isc.util.cio.socketsession
import isc.server_common.tsig_keyring
from isc.server_common.dns_tcp import DNSTCPContext
@@ -738,9 +739,8 @@ def main(ddns_server=None):
logger.error(DDNS_CONFIG_ERROR, str(e))
except SessionTimeout as e:
logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
- except Exception as e:
- logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
- clear_socket()
+ finally:
+ clear_socket()
if '__main__' == __name__:
- main()
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/ddns/ddns_messages.mes b/src/bin/ddns/ddns_messages.mes
index c537ae4..1beb6df 100644
--- a/src/bin/ddns/ddns_messages.mes
+++ b/src/bin/ddns/ddns_messages.mes
@@ -237,11 +237,6 @@ DDNS UPDATE messages, it will return SERVFAIL. However, this does point to
an underlying problem in the messaging system, and should be inspected.
The specific error is printed in the log message.
-% DDNS_UNCAUGHT_EXCEPTION uncaught exception of type %1: %2
-The b10-ddns process encountered an uncaught exception and will now shut
-down. This is indicative of a programming error and should not happen under
-normal circumstances. The exception type and message are printed.
-
% DDNS_UPDATE_NOTIFY notified %1 of updates to %2
Debug message. b10-ddns has made updates to a zone based on an update
request and has successfully notified an external module of the updates.
diff --git a/src/bin/ddns/tests/ddns_test.py b/src/bin/ddns/tests/ddns_test.py
index 4cf31be..66e87a4 100755
--- a/src/bin/ddns/tests/ddns_test.py
+++ b/src/bin/ddns/tests/ddns_test.py
@@ -1375,7 +1375,6 @@ class TestMain(unittest.TestCase):
self.check_exception(isc.config.ModuleCCSessionError("error"))
self.check_exception(ddns.DDNSConfigError("error"))
self.check_exception(isc.cc.SessionTimeout("error"))
- self.check_exception(Exception("error"))
# Add one that is not a subclass of Exception, and hence not
# caught. Misuse BaseException for that.
diff --git a/src/bin/dhcp4/.gitignore b/src/bin/dhcp4/.gitignore
index 86965b9..2ca0e80 100644
--- a/src/bin/dhcp4/.gitignore
+++ b/src/bin/dhcp4/.gitignore
@@ -4,3 +4,4 @@
/dhcp4_messages.h
/spec_config.h
/spec_config.h.pre
+/s-messages
diff --git a/src/bin/dhcp4/Makefile.am b/src/bin/dhcp4/Makefile.am
index b3818c7..a38919a 100644
--- a/src/bin/dhcp4/Makefile.am
+++ b/src/bin/dhcp4/Makefile.am
@@ -16,7 +16,7 @@ endif
pkglibexecdir = $(libexecdir)/@PACKAGE@
-CLEANFILES = *.gcno *.gcda spec_config.h dhcp4_messages.h dhcp4_messages.cc
+CLEANFILES = *.gcno *.gcda spec_config.h dhcp4_messages.h dhcp4_messages.cc s-messages
man_MANS = b10-dhcp4.8
DISTCLEANFILES = $(man_MANS)
@@ -39,8 +39,11 @@ endif
spec_config.h: spec_config.h.pre
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" spec_config.h.pre >$@
-dhcp4_messages.h dhcp4_messages.cc: dhcp4_messages.mes
+dhcp4_messages.h dhcp4_messages.cc: s-messages
+
+s-messages: dhcp4_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/dhcp4/dhcp4_messages.mes
+ touch $@
BUILT_SOURCES = spec_config.h dhcp4_messages.h dhcp4_messages.cc
@@ -56,6 +59,7 @@ nodist_b10_dhcp4_SOURCES = dhcp4_messages.h dhcp4_messages.cc
EXTRA_DIST += dhcp4_messages.mes
b10_dhcp4_LDADD = $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+b10_dhcp4_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
@@ -63,6 +67,7 @@ b10_dhcp4_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
b10_dhcp4_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+b10_dhcp4_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
b10_dhcp4dir = $(pkgdatadir)
b10_dhcp4_DATA = dhcp4.spec
diff --git a/src/bin/dhcp4/config_parser.cc b/src/bin/dhcp4/config_parser.cc
index 3a54c28..7774edc 100644
--- a/src/bin/dhcp4/config_parser.cc
+++ b/src/bin/dhcp4/config_parser.cc
@@ -13,19 +13,21 @@
// PERFORMANCE OF THIS SOFTWARE.
#include <config/ccsession.h>
-#include <dhcp4/config_parser.h>
#include <dhcp4/dhcp4_log.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option_definition.h>
#include <dhcpsrv/cfgmgr.h>
+#include <dhcp4/config_parser.h>
#include <dhcpsrv/dbaccess_parser.h>
-#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/dhcp_parsers.h>
#include <dhcpsrv/option_space_container.h>
#include <util/encode/hex.h>
#include <util/strutil.h>
+
#include <boost/foreach.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/algorithm/string.hpp>
+
#include <limits>
#include <iostream>
#include <vector>
@@ -39,1410 +41,220 @@ using namespace isc::asiolink;
namespace {
-// Forward declarations of some of the parser classes.
-// They are used to define pointer types for these classes.
-class BooleanParser;
-class StringParser;
-class Uint32Parser;
-
-// Pointers to various parser objects.
-typedef boost::shared_ptr<BooleanParser> BooleanParserPtr;
-typedef boost::shared_ptr<StringParser> StringParserPtr;
-typedef boost::shared_ptr<Uint32Parser> Uint32ParserPtr;
-
-/// @brief a factory method that will create a parser for a given element name
-typedef isc::dhcp::DhcpConfigParser* ParserFactory(const std::string& config_id);
-
-/// @brief a collection of factories that creates parsers for specified element names
-typedef std::map<std::string, ParserFactory*> FactoryMap;
-
-/// @brief Storage for option definitions.
-typedef OptionSpaceContainer<OptionDefContainer,
- OptionDefinitionPtr> OptionDefStorage;
-
-/// @brief a collection of pools
-///
-/// That type is used as intermediate storage, when pools are parsed, but there is
-/// no subnet object created yet to store them.
-typedef std::vector<Pool4Ptr> PoolStorage;
-
-/// Collection of containers holding option spaces. Each container within
-/// a particular option space holds so-called option descriptors.
-typedef OptionSpaceContainer<Subnet::OptionContainer,
- Subnet::OptionDescriptor> OptionStorage;
-
-/// @brief Global uint32 parameters that will be used as defaults.
-Uint32Storage uint32_defaults;
-
-/// @brief global string parameters that will be used as defaults.
-StringStorage string_defaults;
-
-/// @brief Global storage for options that will be used as defaults.
-OptionStorage option_defaults;
-
-/// @brief Global storage for option definitions.
-OptionDefStorage option_def_intermediate;
-
-/// @brief a dummy configuration parser
-///
-/// It is a debugging parser. It does not configure anything,
-/// will accept any configuration and will just print it out
-/// on commit. Useful for debugging existing configurations and
-/// adding new ones.
-class DebugParser : public DhcpConfigParser {
-public:
-
- /// @brief Constructor
- ///
- /// See @ref DhcpConfigParser class for details.
- ///
- /// @param param_name name of the parsed parameter
- DebugParser(const std::string& param_name)
- :param_name_(param_name) {
- }
-
- /// @brief builds parameter value
- ///
- /// See @ref DhcpConfigParser class for details.
- ///
- /// @param new_config pointer to the new configuration
- virtual void build(ConstElementPtr new_config) {
- std::cout << "Build for token: [" << param_name_ << "] = ["
- << value_->str() << "]" << std::endl;
- value_ = new_config;
- }
-
- /// @brief pretends to apply the configuration
- ///
- /// This is a method required by base class. It pretends to apply the
- /// configuration, but in fact it only prints the parameter out.
- ///
- /// See @ref DhcpConfigParser class for details.
- virtual void commit() {
- // Debug message. The whole DebugParser class is used only for parser
- // debugging, and is not used in production code. It is very convenient
- // to keep it around. Please do not turn this cout into logger calls.
- std::cout << "Commit for token: [" << param_name_ << "] = ["
- << value_->str() << "]" << std::endl;
- }
-
- /// @brief factory that constructs DebugParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* Factory(const std::string& param_name) {
- return (new DebugParser(param_name));
- }
-
-private:
- /// name of the parsed parameter
- std::string param_name_;
-
- /// pointer to the actual value of the parameter
- ConstElementPtr value_;
-};
-
-/// @brief A boolean value parser.
-///
-/// This parser handles configuration values of the boolean type.
-/// Parsed values are stored in a provided storage. If no storage
-/// is provided then the build function throws an exception.
-class BooleanParser : public DhcpConfigParser {
-public:
- /// @brief Constructor.
- ///
- /// @param param_name name of the parameter.
- BooleanParser(const std::string& param_name)
- : storage_(NULL),
- param_name_(param_name),
- value_(false) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief Parse a boolean value.
- ///
- /// @param value a value to be parsed.
- ///
- /// @throw isc::InvalidOperation if a storage has not been set
- /// prior to calling this function
- /// @throw isc::dhcp::DhcpConfigError if a provided parameter's
- /// name is empty.
- virtual void build(ConstElementPtr value) {
- if (storage_ == NULL) {
- isc_throw(isc::InvalidOperation, "parser logic error:"
- << " storage for the " << param_name_
- << " value has not been set");
- } else if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- // The Config Manager checks if user specified a
- // valid value for a boolean parameter: True or False.
- // It is then ok to assume that if str() does not return
- // 'true' the value is 'false'.
- value_ = (value->str() == "true") ? true : false;
- }
-
- /// @brief Put a parsed value to the storage.
- virtual void commit() {
- if (storage_ != NULL && !param_name_.empty()) {
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief Create an instance of the boolean parser.
- ///
- /// @param param_name name of the parameter for which the
- /// parser is created.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new BooleanParser(param_name));
- }
-
- /// @brief Set the storage for parsed value.
- ///
- /// This function must be called prior to calling build.
- ///
- /// @param storage a pointer to the storage where parsed data
- /// is to be stored.
- void setStorage(BooleanStorage* storage) {
- storage_ = storage;
- }
-
-private:
- /// Pointer to the storage where parsed value is stored.
- BooleanStorage* storage_;
- /// Name of the parameter which value is parsed with this parser.
- std::string param_name_;
- /// Parsed value.
- bool value_;
-};
-
-/// @brief Configuration parser for uint32 parameters
-///
-/// This class is a generic parser that is able to handle any uint32 integer
-/// type. By default it stores the value in external global container
-/// (uint32_defaults). If used in smaller scopes (e.g. to parse parameters
-/// in subnet config), it can be pointed to a different storage, using
-/// setStorage() method. This class follows the parser interface, laid out
-/// in its base class, @ref DhcpConfigParser.
-///
-/// For overview of usability of this generic purpose parser, see
-/// @ref dhcpv4ConfigInherit page.
-class Uint32Parser : public DhcpConfigParser {
-public:
-
- /// @brief constructor for Uint32Parser
- /// @param param_name name of the configuration parameter being parsed
- Uint32Parser(const std::string& param_name)
- : storage_(&uint32_defaults),
- param_name_(param_name) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief Parses configuration configuration parameter as uint32_t.
- ///
- /// @param value pointer to the content of parsed values
- /// @throw BadValue if supplied value could not be base to uint32_t
- /// or the parameter name is empty.
- virtual void build(ConstElementPtr value) {
- if (param_name_.empty()) {
- isc_throw(DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
-
- int64_t check;
- string x = value->str();
- try {
- check = boost::lexical_cast<int64_t>(x);
- } catch (const boost::bad_lexical_cast &) {
- isc_throw(BadValue, "Failed to parse value " << value->str()
- << " as unsigned 32-bit integer.");
- }
- if (check > std::numeric_limits<uint32_t>::max()) {
- isc_throw(BadValue, "Value " << value->str() << "is too large"
- << " for unsigned 32-bit integer.");
- }
- if (check < 0) {
- isc_throw(BadValue, "Value " << value->str() << "is negative."
- << " Only 0 or larger are allowed for unsigned 32-bit integer.");
- }
-
- // value is small enough to fit
- value_ = static_cast<uint32_t>(check);
- }
-
- /// @brief Stores the parsed uint32_t value in a storage.
- virtual void commit() {
- if (storage_ != NULL && !param_name_.empty()) {
- // If a given parameter already exists in the storage we override
- // its value. If it doesn't we insert a new element.
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief factory that constructs Uint32Parser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new Uint32Parser(param_name));
- }
-
- /// @brief sets storage for value of this parameter
- ///
- /// See @ref dhcpv4ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container
- void setStorage(Uint32Storage* storage) {
- storage_ = storage;
- }
-
-private:
- /// pointer to the storage, where parsed value will be stored
- Uint32Storage* storage_;
-
- /// name of the parameter to be parsed
- std::string param_name_;
-
- /// the actual parsed value
- uint32_t value_;
-};
-
-/// @brief Configuration parser for string parameters
-///
-/// This class is a generic parser that is able to handle any string
-/// parameter. By default it stores the value in external global container
-/// (string_defaults). If used in smaller scopes (e.g. to parse parameters
-/// in subnet config), it can be pointed to a different storage, using
-/// setStorage() method. This class follows the parser interface, laid out
-/// in its base class, @ref DhcpConfigParser.
-///
-/// For overview of usability of this generic purpose parser, see
-/// @ref dhcpv4ConfigInherit page.
-class StringParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor for StringParser
- /// @param param_name name of the configuration parameter being parsed
- StringParser(const std::string& param_name)
- :storage_(&string_defaults), param_name_(param_name) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief parses parameter value
- ///
- /// Parses configuration entry and stores it in storage. See
- /// @ref setStorage() for details.
- ///
- /// @param value pointer to the content of parsed values
- virtual void build(ConstElementPtr value) {
- value_ = value->str();
- boost::erase_all(value_, "\"");
- }
-
- /// @brief Stores the parsed value in a storage.
- virtual void commit() {
- if (storage_ != NULL && !param_name_.empty()) {
- // If a given parameter already exists in the storage we override
- // its value. If it doesn't we insert a new element.
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief factory that constructs StringParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new StringParser(param_name));
- }
-
- /// @brief sets storage for value of this parameter
- ///
- /// See \ref dhcpv4ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container
- void setStorage(StringStorage* storage) {
- storage_ = storage;
- }
-
-private:
- /// pointer to the storage, where parsed value will be stored
- StringStorage* storage_;
-
- /// name of the parameter to be parsed
- std::string param_name_;
-
- /// the actual parsed value
- std::string value_;
-};
-
-
-/// @brief parser for interface list definition
-///
-/// This parser handles Dhcp4/interface entry.
-/// It contains a list of network interfaces that the server listens on.
-/// In particular, it can contain an entry called "all" or "any" that
-/// designates all interfaces.
-///
-/// It is useful for parsing Dhcp4/interface parameter.
-class InterfaceListConfigParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor
- ///
- /// As this is a dedicated parser, it must be used to parse
- /// "interface" parameter only. All other types will throw exception.
- ///
- /// @param param_name name of the configuration parameter being parsed
- /// @throw BadValue if supplied parameter name is not "interface"
- InterfaceListConfigParser(const std::string& param_name) {
- if (param_name != "interface") {
- isc_throw(BadValue, "Internal error. Interface configuration "
- "parser called for the wrong parameter: " << param_name);
- }
- }
-
- /// @brief parses parameters value
- ///
- /// Parses configuration entry (list of parameters) and adds each element
- /// to the interfaces list.
- ///
- /// @param value pointer to the content of parsed values
- virtual void build(ConstElementPtr value) {
- BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
- interfaces_.push_back(iface->str());
- }
- }
-
- /// @brief commits interfaces list configuration
- virtual void commit() {
- /// @todo: Implement per interface listening. Currently always listening
- /// on all interfaces.
- }
-
- /// @brief factory that constructs InterfaceListConfigParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new InterfaceListConfigParser(param_name));
- }
-
-private:
- /// contains list of network interfaces
- vector<string> interfaces_;
-};
-
-/// @brief parser for pool definition
-///
-/// This parser handles pool definitions, i.e. a list of entries of one
-/// of two syntaxes: min-max and prefix/len. Pool4 objects are created
-/// and stored in chosen PoolStorage container.
-///
-/// As there are no default values for pool, setStorage() must be called
-/// before build(). Otherwise exception will be thrown.
-///
-/// It is useful for parsing Dhcp4/subnet4[X]/pool parameters.
-class PoolParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor.
- PoolParser(const std::string& /*param_name*/)
- :pools_(NULL) {
- // ignore parameter name, it is always Dhcp4/subnet4[X]/pool
- }
-
- /// @brief parses the actual list
- ///
- /// This method parses the actual list of interfaces.
- /// No validation is done at this stage, everything is interpreted as
- /// interface name.
- /// @param pools_list list of pools defined for a subnet
- /// @throw InvalidOperation if storage was not specified (setStorage() not called)
- /// @throw DhcpConfigError when pool parsing fails
- void build(ConstElementPtr pools_list) {
- // setStorage() should have been called before build
- if (!pools_) {
- isc_throw(InvalidOperation, "Parser logic error. No pool storage set,"
- " but pool parser asked to parse pools");
- }
-
- BOOST_FOREACH(ConstElementPtr text_pool, pools_list->listValue()) {
-
- // That should be a single pool representation. It should contain
- // text is form prefix/len or first - last. Note that spaces
- // are allowed
- string txt = text_pool->stringValue();
-
- // first let's remove any whitespaces
- boost::erase_all(txt, " "); // space
- boost::erase_all(txt, "\t"); // tabulation
-
- // Is this prefix/len notation?
- size_t pos = txt.find("/");
- if (pos != string::npos) {
- IOAddress addr("::");
- uint8_t len = 0;
- try {
- addr = IOAddress(txt.substr(0, pos));
-
- // start with the first character after /
- string prefix_len = txt.substr(pos + 1);
-
- // It is lexical cast to int and then downcast to uint8_t.
- // Direct cast to uint8_t (which is really an unsigned char)
- // will result in interpreting the first digit as output
- // value and throwing exception if length is written on two
- // digits (because there are extra characters left over).
-
- // No checks for values over 128. Range correctness will
- // be checked in Pool4 constructor.
- len = boost::lexical_cast<int>(prefix_len);
- } catch (...) {
- isc_throw(DhcpConfigError, "Failed to parse pool "
- "definition: " << text_pool->stringValue());
- }
-
- Pool4Ptr pool(new Pool4(addr, len));
- local_pools_.push_back(pool);
- continue;
- }
-
- // Is this min-max notation?
- pos = txt.find("-");
- if (pos != string::npos) {
- // using min-max notation
- IOAddress min(txt.substr(0,pos));
- IOAddress max(txt.substr(pos + 1));
-
- Pool4Ptr pool(new Pool4(min, max));
-
- local_pools_.push_back(pool);
- continue;
- }
-
- isc_throw(DhcpConfigError, "Failed to parse pool definition:"
- << text_pool->stringValue() <<
- ". Does not contain - (for min-max) nor / (prefix/len)");
- }
- }
-
- /// @brief sets storage for value of this parameter
- ///
- /// See \ref dhcpv4ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container
- void setStorage(PoolStorage* storage) {
- pools_ = storage;
- }
-
- /// @brief Stores the parsed values in a storage provided
- /// by an upper level parser.
- virtual void commit() {
- if (pools_) {
- // local_pools_ holds the values produced by the build function.
- // At this point parsing should have completed successfuly so
- // we can append new data to the supplied storage.
- pools_->insert(pools_->end(), local_pools_.begin(),
- local_pools_.end());
- }
- }
-
- /// @brief factory that constructs PoolParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new PoolParser(param_name));
- }
-
-private:
- /// @brief pointer to the actual Pools storage
- ///
- /// That is typically a storage somewhere in Subnet parser
- /// (an upper level parser).
- PoolStorage* pools_;
- /// A temporary storage for pools configuration. It is a
- /// storage where pools are stored by build function.
- PoolStorage local_pools_;
-};
-
-/// @brief Parser for option data value.
+/// @brief Parser for DHCP4 option data value.
///
/// This parser parses configuration entries that specify value of
-/// a single option. These entries include option name, option code
-/// and data carried by the option. The option data can be specified
-/// in one of the two available formats: binary value represented as
-/// a string of hexadecimal digits or a list of comma separated values.
-/// The format being used is controlled by csv-format configuration
-/// parameter. When setting this value to True, the latter format is
-/// used. The subsequent values in the CSV format apply to relevant
-/// option data fields in the configured option. For example the
-/// configuration: "data" : "192.168.2.0, 56, hello world" can be
-/// used to set values for the option comprising IPv4 address,
-/// integer and string data field. Note that order matters. If the
-/// order of values does not match the order of data fields within
-/// an option the configuration will not be accepted. If parsing
-/// is successful then an instance of an option is created and
-/// added to the storage provided by the calling class.
-class OptionDataParser : public DhcpConfigParser {
+/// a single option specific to DHCP4. It provides the DHCP4-specific
+/// implementation of the abstract class OptionDataParser.
+class Dhcp4OptionDataParser : public OptionDataParser {
public:
-
/// @brief Constructor.
///
- /// Class constructor.
- OptionDataParser(const std::string&)
- : options_(NULL),
- // initialize option to NULL ptr
- option_descriptor_(false) { }
-
- /// @brief Parses the single option data.
- ///
- /// This method parses the data of a single option from the configuration.
- /// The option data includes option name, option code and data being
- /// carried by this option. Eventually it creates the instance of the
- /// option.
- ///
- /// @warning setStorage must be called with valid storage pointer prior
- /// to calling this method.
- ///
- /// @param option_data_entries collection of entries that define value
- /// for a particular option.
- /// @throw DhcpConfigError if invalid parameter specified in
- /// the configuration.
- /// @throw isc::InvalidOperation if failed to set storage prior to
- /// calling build.
- virtual void build(ConstElementPtr option_data_entries) {
- if (options_ == NULL) {
- isc_throw(isc::InvalidOperation, "Parser logic error: storage must be set before "
- "parsing option data.");
- }
- BOOST_FOREACH(ConfigPair param, option_data_entries->mapValue()) {
- ParserPtr parser;
- if (param.first == "name" || param.first == "data" ||
- param.first == "space") {
- boost::shared_ptr<StringParser>
- name_parser(dynamic_cast<StringParser*>(StringParser::factory(param.first)));
- if (name_parser) {
- name_parser->setStorage(&string_values_);
- parser = name_parser;
- }
- } else if (param.first == "code") {
- boost::shared_ptr<Uint32Parser>
- code_parser(dynamic_cast<Uint32Parser*>(Uint32Parser::factory(param.first)));
- if (code_parser) {
- code_parser->setStorage(&uint32_values_);
- parser = code_parser;
- }
- } else if (param.first == "csv-format") {
- boost::shared_ptr<BooleanParser>
- value_parser(dynamic_cast<BooleanParser*>(BooleanParser::factory(param.first)));
- if (value_parser) {
- value_parser->setStorage(&boolean_values_);
- parser = value_parser;
- }
- } else {
- isc_throw(DhcpConfigError,
- "Parser error: option-data parameter not supported: "
- << param.first);
- }
- parser->build(param.second);
- // Before we can create an option we need to get the data from
- // the child parsers. The only way to do it is to invoke commit
- // on them so as they store the values in appropriate storages
- // that this class provided to them. Note that this will not
- // modify values stored in the global storages so the configuration
- // will remain consistent even parsing fails somewhere further on.
- parser->commit();
- }
- // Try to create the option instance.
- createOption();
- }
-
- /// @brief Commits option value.
- ///
- /// This function adds a new option to the storage or replaces an existing option
- /// with the same code.
- ///
- /// @throw isc::InvalidOperation if failed to set pointer to storage or failed
- /// to call build() prior to commit. If that happens data in the storage
- /// remain un-modified.
- virtual void commit() {
- if (options_ == NULL) {
- isc_throw(isc::InvalidOperation, "parser logic error: storage must be set before "
- "committing option data.");
- } else if (!option_descriptor_.option) {
- // Before we can commit the new option should be configured. If it is not
- // than somebody must have called commit() before build().
- isc_throw(isc::InvalidOperation, "parser logic error: no option has been configured and"
- " thus there is nothing to commit. Has build() been called?");
- }
- uint16_t opt_type = option_descriptor_.option->getType();
- Subnet::OptionContainerPtr options = options_->getItems(option_space_);
- // The getItems() should never return NULL pointer. If there are no
- // options configured for the particular option space a pointer
- // to an empty container should be returned.
- assert(options);
- Subnet::OptionContainerTypeIndex& idx = options->get<1>();
- // Try to find options with the particular option code in the main
- // storage. If found, remove these options because they will be
- // replaced with new one.
- Subnet::OptionContainerTypeRange range =
- idx.equal_range(opt_type);
- if (std::distance(range.first, range.second) > 0) {
- idx.erase(range.first, range.second);
- }
- // Append new option to the main storage.
- options_->addItem(option_descriptor_, option_space_);
- }
-
- /// @brief Set storage for the parser.
- ///
- /// Sets storage for the parser. This storage points to the
- /// vector of options and is used by multiple instances of
- /// OptionDataParser. Each instance creates exactly one object
- /// of dhcp::Option or derived type and appends it to this
- /// storage.
- ///
- /// @param storage pointer to the options storage
- void setStorage(OptionStorage* storage) {
- options_ = storage;
- }
-
-private:
-
- /// @brief Create option instance.
- ///
- /// Creates an instance of an option and adds it to the provided
- /// options storage. If the option data parsed by \ref build function
- /// are invalid or insufficient this function emits an exception.
- ///
- /// @warning this function does not check if options_ storage pointer
- /// is initialized but this check is not needed here because it is done
- /// in the \ref build function.
- ///
- /// @throw DhcpConfigError if parameters provided in the configuration
- /// are invalid.
- void createOption() {
- // Option code is held in the uint32_t storage but is supposed to
- // be uint16_t value. We need to check that value in the configuration
- // does not exceed range of uint8_t and is not zero.
- uint32_t option_code = uint32_values_.getParam("code");
- if (option_code == 0) {
- isc_throw(DhcpConfigError, "option code must not be zero."
- << " Option code '0' is reserved in DHCPv4.");
- } else if (option_code > std::numeric_limits<uint8_t>::max()) {
- isc_throw(DhcpConfigError, "invalid option code '" << option_code
- << "', it must not exceed '"
- << std::numeric_limits<uint8_t>::max() << "'");
- }
-
- // Check that the option name has been specified, is non-empty and does not
- // contain spaces
- std::string option_name = string_values_.getParam("name");
- if (option_name.empty()) {
- isc_throw(DhcpConfigError, "name of the option with code '"
- << option_code << "' is empty");
- } else if (option_name.find(" ") != std::string::npos) {
- isc_throw(DhcpConfigError, "invalid option name '" << option_name
- << "', space character is not allowed");
- }
-
- std::string option_space = string_values_.getParam("space");
- if (!OptionSpace::validateName(option_space)) {
- isc_throw(DhcpConfigError, "invalid option space name '"
- << option_space << "' specified for option '"
- << option_name << "' (code '" << option_code
- << "')");
- }
-
+ /// @param dummy first param, option names are always "Dhcp4/option-data[n]"
+ /// @param options is the option storage in which to store the parsed option
+ /// upon "commit".
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ Dhcp4OptionDataParser(const std::string&,
+ OptionStoragePtr options, ParserContextPtr global_context)
+ :OptionDataParser("", options, global_context) {
+ }
+
+ /// @brief static factory method for instantiating Dhcp4OptionDataParsers
+ ///
+ /// @param param_name name of the parameter to be parsed.
+ /// @param options storage where the parameter value is to be stored.
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ /// @return returns a pointer to a new OptionDataParser. Caller is
+ /// is responsible for deleting it when it is no longer needed.
+ static OptionDataParser* factory(const std::string& param_name,
+ OptionStoragePtr options, ParserContextPtr global_context) {
+ return (new Dhcp4OptionDataParser(param_name, options, global_context));
+ }
+
+protected:
+ /// @brief Finds an option definition within the server's option space
+ ///
+ /// Given an option space and an option code, find the correpsonding
+ /// option defintion within the server's option defintion storage.
+ ///
+ /// @param option_space name of the parameter option space
+ /// @param option_code numeric value of the parameter to find
+ /// @return OptionDefintionPtr of the option defintion or an
+ /// empty OptionDefinitionPtr if not found.
+ /// @throw DhcpConfigError if the option space requested is not valid
+ /// for this server.
+ virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
+ std::string& option_space, uint32_t option_code) {
OptionDefinitionPtr def;
if (option_space == "dhcp4" &&
LibDHCP::isStandardOption(Option::V4, option_code)) {
def = LibDHCP::getOptionDef(Option::V4, option_code);
-
} else if (option_space == "dhcp6") {
isc_throw(DhcpConfigError, "'dhcp6' option space name is reserved"
- << " for DHCPv6 server");
+ << " for DHCPv6 server");
} else {
- // If we are not dealing with a standard option then we
- // need to search for its definition among user-configured
- // options. They are expected to be in the global storage
- // already.
- OptionDefContainerPtr defs = option_def_intermediate.getItems(option_space);
- // The getItems() should never return the NULL pointer. If there are
- // no option definitions for the particular option space a pointer
- // to an empty container should be returned.
- assert(defs);
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- OptionDefContainerTypeRange range = idx.equal_range(option_code);
- if (std::distance(range.first, range.second) > 0) {
- def = *range.first;
- }
- if (!def) {
- isc_throw(DhcpConfigError, "definition for the option '"
- << option_space << "." << option_name
- << "' having code '" << option_code
- << "' does not exist");
- }
-
- }
-
- // Get option data from the configuration database ('data' field).
- const std::string option_data = string_values_.getParam("data");
- const bool csv_format = boolean_values_.getParam("csv-format");
-
- // Transform string of hexadecimal digits into binary format.
- std::vector<uint8_t> binary;
- std::vector<std::string> data_tokens;
-
- if (csv_format) {
- // If the option data is specified as a string of comma
- // separated values then we need to split this string into
- // individual values - each value will be used to initialize
- // one data field of an option.
- data_tokens = isc::util::str::tokens(option_data, ",");
- } else {
- // Otherwise, the option data is specified as a string of
- // hexadecimal digits that we have to turn into binary format.
- try {
- util::encode::decodeHex(option_data, binary);
- } catch (...) {
- isc_throw(DhcpConfigError, "option data is not a valid"
- << " string of hexadecimal digits: " << option_data);
+ // Check if this is a vendor-option. If it is, get vendor-specific
+ // definition.
+ uint32_t vendor_id = SubnetConfigParser::optionSpaceToVendorId(option_space);
+ if (vendor_id) {
+ def = LibDHCP::getVendorOptionDef(Option::V4, vendor_id, option_code);
}
}
- OptionPtr option;
- if (!def) {
- if (csv_format) {
- isc_throw(DhcpConfigError, "the CSV option data format can be"
- " used to specify values for an option that has a"
- " definition. The option with code " << option_code
- << " does not have a definition.");
- }
-
- // @todo We have a limited set of option definitions initialized at the moment.
- // In the future we want to initialize option definitions for all options.
- // Consequently an error will be issued if an option definition does not exist
- // for a particular option code. For now it is ok to create generic option
- // if definition does not exist.
- OptionPtr option(new Option(Option::V4, static_cast<uint16_t>(option_code),
- binary));
- // The created option is stored in option_descriptor_ class member until the
- // commit stage when it is inserted into the main storage. If an option with the
- // same code exists in main storage already the old option is replaced.
- option_descriptor_.option = option;
- option_descriptor_.persistent = false;
- } else {
-
- // Option name should match the definition. The option name
- // may seem to be redundant but in the future we may want
- // to reference options and definitions using their names
- // and/or option codes so keeping the option name in the
- // definition of option value makes sense.
- if (def->getName() != option_name) {
- isc_throw(DhcpConfigError, "specified option name '"
- << option_name << "' does not match the "
- << "option definition: '" << option_space
- << "." << def->getName() << "'");
- }
-
- // Option definition has been found so let's use it to create
- // an instance of our option.
- try {
- OptionPtr option = csv_format ?
- def->optionFactory(Option::V4, option_code, data_tokens) :
- def->optionFactory(Option::V4, option_code, binary);
- Subnet::OptionDescriptor desc(option, false);
- option_descriptor_.option = option;
- option_descriptor_.persistent = false;
- } catch (const isc::Exception& ex) {
- isc_throw(DhcpConfigError, "option data does not match"
- << " option definition (space: " << option_space
- << ", code: " << option_code << "): "
- << ex.what());
- }
-
- }
- // All went good, so we can set the option space name.
- option_space_ = option_space;
+ return (def);
}
-
- /// Storage for uint32 values (e.g. option code).
- Uint32Storage uint32_values_;
- /// Storage for string values (e.g. option name or data).
- StringStorage string_values_;
- /// Storage for boolean values.
- BooleanStorage boolean_values_;
- /// Pointer to options storage. This storage is provided by
- /// the calling class and is shared by all OptionDataParser objects.
- OptionStorage* options_;
- /// Option descriptor holds newly configured option.
- Subnet::OptionDescriptor option_descriptor_;
- /// Option space name where the option belongs to.
- std::string option_space_;
};
-/// @brief Parser for option data values within a subnet.
+/// @brief Parser for IPv4 pool definitions.
+///
+/// This is the IPv4 derivation of the PoolParser class and handles pool
+/// definitions, i.e. a list of entries of one of two syntaxes: min-max and
+/// prefix/len for IPv4 pools. Pool4 objects are created and stored in chosen
+/// PoolStorage container.
///
-/// This parser iterates over all entries that define options
-/// data for a particular subnet and creates a collection of options.
-/// If parsing is successful, all these options are added to the Subnet
-/// object.
-class OptionDataListParser : public DhcpConfigParser {
+/// It is useful for parsing Dhcp4/subnet4[X]/pool parameters.
+class Pool4Parser : public PoolParser {
public:
/// @brief Constructor.
///
- /// Unless otherwise specified, parsed options will be stored in
- /// a global option container (option_default). That storage location
- /// is overriden on a subnet basis.
- OptionDataListParser(const std::string&)
- : options_(&option_defaults), local_options_() { }
-
- /// @brief Parses entries that define options' data for a subnet.
- ///
- /// This method iterates over all entries that define option data
- /// for options within a single subnet and creates options' instances.
- ///
- /// @param option_data_list pointer to a list of options' data sets.
- /// @throw DhcpConfigError if option parsing failed.
- void build(ConstElementPtr option_data_list) {
- BOOST_FOREACH(ConstElementPtr option_value, option_data_list->listValue()) {
- boost::shared_ptr<OptionDataParser> parser(new OptionDataParser("option-data"));
- // options_ member will hold instances of all options thus
- // each OptionDataParser takes it as a storage.
- parser->setStorage(&local_options_);
- // Build the instance of a single option.
- parser->build(option_value);
- // Store a parser as it will be used to commit.
- parsers_.push_back(parser);
- }
+ /// @param param_name name of the parameter. Note, it is passed through
+ /// but unused, parameter is currently always "Dhcp4/subnet4[X]/pool"
+ /// @param pools storage container in which to store the parsed pool
+ /// upon "commit"
+ Pool4Parser(const std::string& param_name, PoolStoragePtr pools)
+ :PoolParser(param_name, pools) {
}
- /// @brief Set storage for option instances.
+protected:
+ /// @brief Creates a Pool4 object given a IPv4 prefix and the prefix length.
///
- /// @param storage pointer to options storage.
- void setStorage(OptionStorage* storage) {
- options_ = storage;
+ /// @param addr is the IPv4 prefix of the pool.
+ /// @param len is the prefix length.
+ /// @param ignored dummy parameter to provide symmetry between the
+ /// PoolParser derivations. The V6 derivation requires a third value.
+ /// @return returns a PoolPtr to the new Pool4 object.
+ PoolPtr poolMaker (IOAddress &addr, uint32_t len, int32_t) {
+ return (PoolPtr(new Pool4(addr, len)));
}
-
- /// @brief Commit all option values.
+ /// @brief Creates a Pool4 object given starting and ending IPv4 addresses.
///
- /// This function invokes commit for all option values.
- void commit() {
- BOOST_FOREACH(ParserPtr parser, parsers_) {
- parser->commit();
- }
- // Parsing was successful and we have all configured
- // options in local storage. We can now replace old values
- // with new values.
- std::swap(local_options_, *options_);
- }
-
- /// @brief Create DhcpDataListParser object
- ///
- /// @param param_name param name.
- ///
- /// @return DhcpConfigParser object.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new OptionDataListParser(param_name));
+ /// @param min is the first IPv4 address in the pool.
+ /// @param max is the last IPv4 address in the pool.
+ /// @param ignored dummy parameter to provide symmetry between the
+ /// PoolParser derivations. The V6 derivation requires a third value.
+ /// @return returns a PoolPtr to the new Pool4 object.
+ PoolPtr poolMaker (IOAddress &min, IOAddress &max, int32_t) {
+ return (PoolPtr(new Pool4(min, max)));
}
-
- /// Pointer to options instances storage.
- OptionStorage* options_;
- /// Intermediate option storage. This storage is used by
- /// lower level parsers to add new options. Values held
- /// in this storage are assigned to main storage (options_)
- /// if overall parsing was successful.
- OptionStorage local_options_;
- /// Collection of parsers;
- ParserCollection parsers_;
};
-/// @brief Parser for a single option definition.
+/// @brief This class parses a single IPv4 subnet.
///
-/// This parser creates an instance of a single option definition.
-class OptionDefParser : public DhcpConfigParser {
+/// This is the IPv4 derivation of the SubnetConfigParser class and it parses
+/// the whole subnet definition. It creates parsersfor received configuration
+/// parameters as needed.
+class Subnet4ConfigParser : public SubnetConfigParser {
public:
-
- /// @brief Constructor.
- ///
- /// This constructor sets the pointer to the option definitions
- /// storage to NULL. It must be set to point to the actual storage
- /// before \ref build is called.
- OptionDefParser(const std::string&)
- : storage_(NULL) {
- }
-
- /// @brief Parses an entry that describes single option definition.
- ///
- /// @param option_def a configuration entry to be parsed.
+ /// @brief Constructor
///
- /// @throw DhcpConfigError if parsing was unsuccessful.
- void build(ConstElementPtr option_def) {
- if (storage_ == NULL) {
- isc_throw(DhcpConfigError, "parser logic error: storage must be set"
- " before parsing option definition data");
- }
- // Parse the elements that make up the option definition.
- BOOST_FOREACH(ConfigPair param, option_def->mapValue()) {
- std::string entry(param.first);
- ParserPtr parser;
- if (entry == "name" || entry == "type" ||
- entry == "record-types" || entry == "space" ||
- entry == "encapsulate") {
- StringParserPtr
- str_parser(dynamic_cast<StringParser*>(StringParser::factory(entry)));
- if (str_parser) {
- str_parser->setStorage(&string_values_);
- parser = str_parser;
- }
- } else if (entry == "code") {
- Uint32ParserPtr
- code_parser(dynamic_cast<Uint32Parser*>(Uint32Parser::factory(entry)));
- if (code_parser) {
- code_parser->setStorage(&uint32_values_);
- parser = code_parser;
- }
- } else if (entry == "array") {
- BooleanParserPtr
- array_parser(dynamic_cast<BooleanParser*>(BooleanParser::factory(entry)));
- if (array_parser) {
- array_parser->setStorage(&boolean_values_);
- parser = array_parser;
- }
- } else {
- isc_throw(DhcpConfigError, "invalid parameter: " << entry);
- }
-
- parser->build(param.second);
- parser->commit();
- }
-
- // Create an instance of option definition.
- createOptionDef();
-
- // Get all items we collected so far for the particular option space.
- OptionDefContainerPtr defs = storage_->getItems(option_space_name_);
- // Check if there are any items with option code the same as the
- // one specified for the definition we are now creating.
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- const OptionDefContainerTypeRange& range =
- idx.equal_range(option_definition_->getCode());
- // If there are any items with this option code already we need
- // to issue an error because we don't allow duplicates for
- // option definitions within an option space.
- if (std::distance(range.first, range.second) > 0) {
- isc_throw(DhcpConfigError, "duplicated option definition for"
- << " code '" << option_definition_->getCode() << "'");
- }
+ /// @param ignored first parameter
+ /// stores global scope parameters, options, option defintions.
+ Subnet4ConfigParser(const std::string&)
+ :SubnetConfigParser("", globalContext()) {
}
- /// @brief Stores the parsed option definition in a storage.
+ /// @brief Adds the created subnet to a server's configuration.
+ /// @throw throws Unexpected if dynamic cast fails.
void commit() {
- if (storage_ && option_definition_ &&
- OptionSpace::validateName(option_space_name_)) {
- storage_->addItem(option_definition_, option_space_name_);
- }
- }
-
- /// @brief Sets a pointer to the data store.
- ///
- /// The newly created instance of an option definition will be
- /// added to the data store given by the argument.
- ///
- /// @param storage pointer to the data store where the option definition
- /// will be added to.
- void setStorage(OptionDefStorage* storage) {
- storage_ = storage;
- }
-
-private:
-
- /// @brief Create option definition from the parsed parameters.
- void createOptionDef() {
-
- // Get the option space name and validate it.
- std::string space = string_values_.getParam("space");
- if (!OptionSpace::validateName(space)) {
- isc_throw(DhcpConfigError, "invalid option space name '"
- << space << "'");
- }
-
- // Get other parameters that are needed to create the
- // option definition.
- std::string name = string_values_.getParam("name");
- uint32_t code = uint32_values_.getParam("code");
- std::string type = string_values_.getParam("type");
- bool array_type = boolean_values_.getParam("array");
- std::string encapsulates = string_values_.getParam("encapsulate");
-
- // Create option definition.
- OptionDefinitionPtr def;
- // We need to check if user has set encapsulated option space
- // name. If so, different constructor will be used.
- if (!encapsulates.empty()) {
- // Arrays can't be used together with sub-options.
- if (array_type) {
- isc_throw(DhcpConfigError, "option '" << space << "."
- << "name" << "', comprising an array of data"
- << " fields may not encapsulate any option space");
-
- } else if (encapsulates == space) {
- isc_throw(DhcpConfigError, "option must not encapsulate"
- << " an option space it belongs to: '"
- << space << "." << name << "' is set to"
- << " encapsulate '" << space << "'");
-
- } else {
- def.reset(new OptionDefinition(name, code, type,
- encapsulates.c_str()));
- }
-
- } else {
- def.reset(new OptionDefinition(name, code, type, array_type));
-
- }
- // The record-types field may carry a list of comma separated names
- // of data types that form a record.
- std::string record_types = string_values_.getParam("record-types");
-
- // Split the list of record types into tokens.
- std::vector<std::string> record_tokens =
- isc::util::str::tokens(record_types, ",");
- // Iterate over each token and add a record type into
- // option definition.
- BOOST_FOREACH(std::string record_type, record_tokens) {
- try {
- boost::trim(record_type);
- if (!record_type.empty()) {
- def->addRecordField(record_type);
- }
- } catch (const Exception& ex) {
- isc_throw(DhcpConfigError, "invalid record type values"
- << " specified for the option definition: "
- << ex.what());
+ if (subnet_) {
+ Subnet4Ptr sub4ptr = boost::dynamic_pointer_cast<Subnet4>(subnet_);
+ if (!sub4ptr) {
+ // If we hit this, it is a programming error.
+ isc_throw(Unexpected,
+ "Invalid cast in Subnet4ConfigParser::commit");
}
- }
-
- // Check the option definition parameters are valid.
- try {
- def->validate();
- } catch (const isc::Exception& ex) {
- isc_throw(DhcpConfigError, "invalid option definition"
- << " parameters: " << ex.what());
- }
- // Option definition has been created successfully.
- option_space_name_ = space;
- option_definition_ = def;
- }
-
- /// Instance of option definition being created by this parser.
- OptionDefinitionPtr option_definition_;
- /// Name of the space the option definition belongs to.
- std::string option_space_name_;
-
- /// Pointer to a storage where the option definition will be
- /// added when \ref commit is called.
- OptionDefStorage* storage_;
-
- /// Storage for boolean values.
- BooleanStorage boolean_values_;
- /// Storage for string values.
- StringStorage string_values_;
- /// Storage for uint32 values.
- Uint32Storage uint32_values_;
-};
-
-/// @brief Parser for a list of option definitions.
-///
-/// This parser iterates over all configuration entries that define
-/// option definitions and creates instances of these definitions.
-/// If the parsing is successful, the collection of created definitions
-/// is put into the provided storage.
-class OptionDefListParser : DhcpConfigParser {
-public:
-
- /// @brief Constructor.
- ///
- /// This constructor initializes the pointer to option definitions
- /// storage to NULL value. This pointer has to be set to point to
- /// the actual storage before the \ref build function is called.
- OptionDefListParser(const std::string&) {
- }
-
- /// @brief Parse configuration entries.
- ///
- /// This function parses configuration entries and creates instances
- /// of option definitions.
- ///
- /// @param option_def_list pointer to an element that holds entries
- /// that define option definitions.
- /// @throw DhcpConfigError if configuration parsing fails.
- void build(ConstElementPtr option_def_list) {
- // Clear existing items in the global storage.
- // We are going to replace all of them.
- option_def_intermediate.clearItems();
-
- if (!option_def_list) {
- isc_throw(DhcpConfigError, "parser error: a pointer to a list of"
- << " option definitions is NULL");
- }
- BOOST_FOREACH(ConstElementPtr option_def, option_def_list->listValue()) {
- boost::shared_ptr<OptionDefParser>
- parser(new OptionDefParser("single-option-def"));
- parser->setStorage(&option_def_intermediate);
- parser->build(option_def);
- parser->commit();
+ isc::dhcp::CfgMgr::instance().addSubnet4(sub4ptr);
}
}
- /// @brief Stores option definitions in the CfgMgr.
- void commit() {
-
- CfgMgr& cfg_mgr = CfgMgr::instance();
-
- cfg_mgr.deleteOptionDefs();
-
- // We need to move option definitions from the temporary
- // storage to the global storage.
- std::list<std::string> space_names =
- option_def_intermediate.getOptionSpaceNames();
- BOOST_FOREACH(std::string space_name, space_names) {
+protected:
- BOOST_FOREACH(OptionDefinitionPtr def,
- *option_def_intermediate.getItems(space_name)) {
- // All option definitions should be initialized to non-NULL
- // values. The validation is expected to be made by the
- // OptionDefParser when creating an option definition.
- assert(def);
- cfg_mgr.addOptionDef(def, space_name);
- }
- }
- }
-
- /// @brief Create an OptionDefListParser object.
+ /// @brief Creates parsers for entries in subnet definition.
///
- /// @param param_name configuration entry holding option definitions.
+ /// @param config_id name of the entry
///
- /// @return OptionDefListParser object.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new OptionDefListParser(param_name));
- }
-
-};
-
-/// @brief this class parses a single subnet
-///
-/// This class parses the whole subnet definition. It creates parsers
-/// for received configuration parameters as needed.
-class Subnet4ConfigParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor
- Subnet4ConfigParser(const std::string& ) {
- // The parameter should always be "subnet", but we don't check here
- // against it in case someone wants to reuse this parser somewhere.
- }
-
- /// @brief parses parameter value
- ///
- /// @param subnet pointer to the content of subnet definition
- void build(ConstElementPtr subnet) {
-
- BOOST_FOREACH(ConfigPair param, subnet->mapValue()) {
- ParserPtr parser(createSubnet4ConfigParser(param.first));
- // The actual type of the parser is unknown here. We have to discover
- // the parser type here to invoke the corresponding setStorage function
- // on it. We discover parser type by trying to cast the parser to various
- // parser types and checking which one was successful. For this one
- // a setStorage and build methods are invoked.
-
- // Try uint32 type parser.
- if (!buildParser<Uint32Parser, Uint32Storage >(parser, uint32_values_,
- param.second) &&
- // Try string type parser.
- !buildParser<StringParser, StringStorage >(parser, string_values_,
- param.second) &&
- // Try pool parser.
- !buildParser<PoolParser, PoolStorage >(parser, pools_,
- param.second) &&
- // Try option data parser.
- !buildParser<OptionDataListParser, OptionStorage >(parser, options_,
- param.second)) {
- // Appropriate parsers are created in the createSubnet6ConfigParser
- // and they should be limited to those that we check here for. Thus,
- // if we fail to find a matching parser here it is a programming error.
- isc_throw(DhcpConfigError, "failed to find suitable parser");
- }
- }
- // In order to create new subnet we need to get the data out
- // of the child parsers first. The only way to do it is to
- // invoke commit on them because it will make them write
- // parsed data into storages we have supplied.
- // Note that triggering commits on child parsers does not
- // affect global data because we supplied pointers to storages
- // local to this object. Thus, even if this method fails
- // later on, the configuration remains consistent.
- BOOST_FOREACH(ParserPtr parser, parsers_) {
- parser->commit();
+ /// @return parser object for specified entry name. Note the caller is
+ /// responsible for deleting the parser created.
+ /// @throw isc::dhcp::DhcpConfigError if trying to create a parser
+ /// for unknown config element
+ DhcpConfigParser* createSubnetConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ if ((config_id.compare("valid-lifetime") == 0) ||
+ (config_id.compare("renew-timer") == 0) ||
+ (config_id.compare("rebind-timer") == 0)) {
+ parser = new Uint32Parser(config_id, uint32_values_);
+ } else if ((config_id.compare("subnet") == 0) ||
+ (config_id.compare("interface") == 0) ||
+ (config_id.compare("next-server") == 0)) {
+ parser = new StringParser(config_id, string_values_);
+ } else if (config_id.compare("pool") == 0) {
+ parser = new Pool4Parser(config_id, pools_);
+ } else if (config_id.compare("option-data") == 0) {
+ parser = new OptionDataListParser(config_id, options_,
+ global_context_,
+ Dhcp4OptionDataParser::factory);
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: Subnet4 parameter not supported: " << config_id);
}
- // Create a subnet.
- createSubnet();
+ return (parser);
}
- /// @brief commits received configuration.
+ /// @brief Determines if the given option space name and code describe
+ /// a standard option for the DCHP4 server.
///
- /// This method does most of the configuration. Many other parsers are just
- /// storing the values that are actually consumed here. Pool definitions
- /// created in other parsers are used here and added to newly created Subnet4
- /// objects. Subnet4 are then added to DHCP CfgMgr.
- /// @throw DhcpConfigError if there are any issues encountered during commit
- void commit() {
- if (subnet_) {
- CfgMgr::instance().addSubnet4(subnet_);
- }
+ /// @param option_space is the name of the option space to consider
+ /// @param code is the numeric option code to consider
+ /// @return returns true if the space and code are part of the server's
+ /// standard options.
+ bool isServerStdOption(std::string option_space, uint32_t code) {
+ return ((option_space.compare("dhcp4") == 0)
+ && LibDHCP::isStandardOption(Option::V4, code));
}
-private:
-
- /// @brief Set storage for a parser and invoke build.
- ///
- /// This helper method casts the provided parser pointer to the specified
- /// type. If the cast is successful it sets the corresponding storage for
- /// this parser, invokes build on it and saves the parser.
- ///
- /// @tparam T parser type to which parser argument should be cast.
- /// @tparam Y storage type for the specified parser type.
- /// @param parser parser on which build must be invoked.
- /// @param storage reference to a storage that will be set for a parser.
- /// @param subnet subnet element read from the configuration and being parsed.
- /// @return true if parser pointer was successfully cast to specialized
- /// parser type provided as Y.
- template<typename T, typename Y>
- bool buildParser(const ParserPtr& parser, Y& storage, const ConstElementPtr& subnet) {
- // We need to cast to T in order to set storage for the parser.
- boost::shared_ptr<T> cast_parser = boost::dynamic_pointer_cast<T>(parser);
- // It is common that this cast is not successful because we try to cast to all
- // supported parser types as we don't know the type of a parser in advance.
- if (cast_parser) {
- // Cast, successful so we go ahead with setting storage and actual parse.
- cast_parser->setStorage(&storage);
- parser->build(subnet);
- parsers_.push_back(parser);
- // We indicate that cast was successful so as the calling function
- // may skip attempts to cast to other parser types and proceed to
- // next element.
- return (true);
- }
- // It was not successful. Indicate that another parser type
- // should be tried.
- return (false);
+ /// @brief Returns the option definition for a given option code from
+ /// the DHCP4 server's standard set of options.
+ /// @param code is the numeric option code of the desired option definition.
+ /// @return returns a pointer the option definition
+ OptionDefinitionPtr getServerStdOptionDefinition (uint32_t code) {
+ return (LibDHCP::getOptionDef(Option::V4, code));
}
- /// @brief Append sub-options to an option.
+ /// @brief Issues a DHCP4 server specific warning regarding duplicate subnet
+ /// options.
///
- /// @param option_space a name of the encapsulated option space.
- /// @param option option instance to append sub-options to.
- void appendSubOptions(const std::string& option_space, OptionPtr& option) {
- // Only non-NULL options are stored in option container.
- // If this option pointer is NULL this is a serious error.
- assert(option);
-
- OptionDefinitionPtr def;
- if (option_space == "dhcp4" &&
- LibDHCP::isStandardOption(Option::V4, option->getType())) {
- def = LibDHCP::getOptionDef(Option::V4, option->getType());
- // Definitions for some of the standard options hasn't been
- // implemented so it is ok to leave here.
- if (!def) {
- return;
- }
- } else {
- const OptionDefContainerPtr defs =
- option_def_intermediate.getItems(option_space);
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- const OptionDefContainerTypeRange& range =
- idx.equal_range(option->getType());
- // There is no definition so we have to leave.
- if (std::distance(range.first, range.second) == 0) {
- return;
- }
-
- def = *range.first;
-
- // If the definition exists, it must be non-NULL.
- // Otherwise it is a programming error.
- assert(def);
- }
-
- // We need to get option definition for the particular option space
- // and code. This definition holds the information whether our
- // option encapsulates any option space.
- // Get the encapsulated option space name.
- std::string encapsulated_space = def->getEncapsulatedSpace();
- // If option space name is empty it means that our option does not
- // encapsulate any option space (does not include sub-options).
- if (!encapsulated_space.empty()) {
- // Get the sub-options that belong to the encapsulated
- // option space.
- const Subnet::OptionContainerPtr sub_opts =
- option_defaults.getItems(encapsulated_space);
- // Append sub-options to the option.
- BOOST_FOREACH(Subnet::OptionDescriptor desc, *sub_opts) {
- if (desc.option) {
- option->addOption(desc.option);
- }
- }
- }
+ /// @param code is the numeric option code of the duplicate option
+ /// @param addr is the subnet address
+ /// @todo a means to know the correct logger and perhaps a common
+ /// message would allow this method to be emitted by the base class.
+ virtual void duplicate_option_warning(uint32_t code,
+ isc::asiolink::IOAddress& addr) {
+ LOG_WARN(dhcp4_logger, DHCP4_CONFIG_OPTION_DUPLICATE)
+ .arg(code).arg(addr.toText());
}
- /// @brief Create a new subnet using a data from child parsers.
+ /// @brief Instantiates the IPv4 Subnet based on a given IPv4 address
+ /// and prefix length.
///
- /// @throw isc::dhcp::DhcpConfigError if subnet configuration parsing failed.
- void createSubnet() {
- std::string subnet_txt;
- try {
- subnet_txt = string_values_.getParam("subnet");
- } catch (DhcpConfigError) {
- // Rethrow with precise error.
- isc_throw(DhcpConfigError,
- "Mandatory subnet definition in subnet missing");
- }
-
- // Remove any spaces or tabs.
- boost::erase_all(subnet_txt, " ");
- boost::erase_all(subnet_txt, "\t");
-
- // The subnet format is prefix/len. We are going to extract
- // the prefix portion of a subnet string to create IOAddress
- // object from it. IOAddress will be passed to the Subnet's
- // constructor later on. In order to extract the prefix we
- // need to get all characters preceding "/".
- size_t pos = subnet_txt.find("/");
- if (pos == string::npos) {
- isc_throw(DhcpConfigError,
- "Invalid subnet syntax (prefix/len expected):" << subnet_txt);
- }
-
- // Try to create the address object. It also validates that
- // the address syntax is ok.
- IOAddress addr(subnet_txt.substr(0, pos));
- uint8_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
-
+ /// @param addr is IPv4 address of the subnet.
+ /// @param len is the prefix length
+ void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) {
// Get all 'time' parameters using inheritance.
// If the subnet-specific value is defined then use it, else
// use the global value. The global value must always be
@@ -1452,156 +264,38 @@ private:
Triplet<uint32_t> t2 = getParam("rebind-timer");
Triplet<uint32_t> valid = getParam("valid-lifetime");
- /// @todo: Convert this to logger once the parser is working reliably
stringstream tmp;
- tmp << addr.toText() << "/" << (int)len
+ tmp << addr << "/" << (int)len
<< " with params t1=" << t1 << ", t2=" << t2 << ", valid=" << valid;
LOG_INFO(dhcp4_logger, DHCP4_CONFIG_NEW_SUBNET).arg(tmp.str());
- subnet_.reset(new Subnet4(addr, len, t1, t2, valid));
+ Subnet4Ptr subnet4(new Subnet4(addr, len, t1, t2, valid));
+ subnet_ = subnet4;
- for (PoolStorage::iterator it = pools_.begin(); it != pools_.end(); ++it) {
- subnet_->addPool(*it);
- }
-
- // We are going to move configured options to the Subnet object.
- // Configured options reside in the container where options
- // are grouped by space names. Thus we need to get all space names
- // and iterate over all options that belong to them.
- std::list<std::string> space_names = options_.getOptionSpaceNames();
- BOOST_FOREACH(std::string option_space, space_names) {
- // Get all options within a particular option space.
- BOOST_FOREACH(Subnet::OptionDescriptor desc,
- *options_.getItems(option_space)) {
- // The pointer should be non-NULL. The validation is expected
- // to be performed by the OptionDataParser before adding an
- // option descriptor to the container.
- assert(desc.option);
- // We want to check whether an option with the particular
- // option code has been already added. If so, we want
- // to issue a warning.
- Subnet::OptionDescriptor existing_desc =
- subnet_->getOptionDescriptor("option_space",
- desc.option->getType());
- if (existing_desc.option) {
- LOG_WARN(dhcp4_logger, DHCP4_CONFIG_OPTION_DUPLICATE)
- .arg(desc.option->getType()).arg(addr.toText());
- }
- // Add sub-options (if any).
- appendSubOptions(option_space, desc.option);
- // In any case, we add the option to the subnet.
- subnet_->addOption(desc.option, false, option_space);
- }
- }
-
- // Check all global options and add them to the subnet object if
- // they have been configured in the global scope. If they have been
- // configured in the subnet scope we don't add global option because
- // the one configured in the subnet scope always takes precedence.
- space_names = option_defaults.getOptionSpaceNames();
- BOOST_FOREACH(std::string option_space, space_names) {
- // Get all global options for the particular option space.
- BOOST_FOREACH(Subnet::OptionDescriptor desc,
- *option_defaults.getItems(option_space)) {
- // The pointer should be non-NULL. The validation is expected
- // to be performed by the OptionDataParser before adding an
- // option descriptor to the container.
- assert(desc.option);
- // Check if the particular option has been already added.
- // This would mean that it has been configured in the
- // subnet scope. Since option values configured in the
- // subnet scope take precedence over globally configured
- // values we don't add option from the global storage
- // if there is one already.
- Subnet::OptionDescriptor existing_desc =
- subnet_->getOptionDescriptor(option_space, desc.option->getType());
- if (!existing_desc.option) {
- // Add sub-options (if any).
- appendSubOptions(option_space, desc.option);
-
- subnet_->addOption(desc.option, false, option_space);
- }
+ // Try global value first
+ try {
+ string next_server = globalContext()->string_values_->getParam("next-server");
+ if (!next_server.empty()) {
+ subnet4->setSiaddr(IOAddress(next_server));
}
+ } catch (const DhcpConfigError&) {
+ // Don't care. next_server is optional. We can live without it
}
- }
- /// @brief creates parsers for entries in subnet definition
- ///
- /// @todo Add subnet-specific things here (e.g. subnet-specific options)
- ///
- /// @param config_id name od the entry
- /// @return parser object for specified entry name
- /// @throw NotImplemented if trying to create a parser for unknown config element
- DhcpConfigParser* createSubnet4ConfigParser(const std::string& config_id) {
- FactoryMap factories;
- factories["valid-lifetime"] = Uint32Parser::factory;
- factories["renew-timer"] = Uint32Parser::factory;
- factories["rebind-timer"] = Uint32Parser::factory;
- factories["subnet"] = StringParser::factory;
- factories["pool"] = PoolParser::factory;
- factories["option-data"] = OptionDataListParser::factory;
-
- FactoryMap::iterator f = factories.find(config_id);
- if (f == factories.end()) {
- // Used for debugging only.
- // return new DebugParser(config_id);
-
- isc_throw(NotImplemented,
- "parser error: Subnet4 parameter not supported: "
- << config_id);
- }
- return (f->second(config_id));
- }
-
- /// @brief Returns value for a given parameter (after using inheritance)
- ///
- /// This method implements inheritance. For a given parameter name, it first
- /// checks if there is a global value for it and overwrites it with specific
- /// value if such value was defined in subnet.
- ///
- /// @param name name of the parameter
- /// @return triplet with the parameter name
- /// @throw DhcpConfigError when requested parameter is not present
- Triplet<uint32_t> getParam(const std::string& name) {
- uint32_t value = 0;
+ // Try subnet specific value if it's available
try {
- // look for local value
- value = uint32_values_.getParam(name);
- } catch (DhcpConfigError) {
- try {
- // no local, use global value
- value = uint32_defaults.getParam(name);
- } catch (DhcpConfigError) {
- isc_throw(DhcpConfigError, "Mandatory parameter " << name
- << " missing (no global default and no subnet-"
- << "specific value)");
+ string next_server = string_values_->getParam("next-server");
+ if (!next_server.empty()) {
+ subnet4->setSiaddr(IOAddress(next_server));
}
+ } catch (const DhcpConfigError&) {
+ // Don't care. next_server is optional. We can live without it
}
-
- return (Triplet<uint32_t>(value));
}
-
- /// storage for subnet-specific uint32 values
- Uint32Storage uint32_values_;
-
- /// storage for subnet-specific integer values
- StringStorage string_values_;
-
- /// storage for pools belonging to this subnet
- PoolStorage pools_;
-
- /// storage for options belonging to this subnet
- OptionStorage options_;
-
- /// parsers are stored here
- ParserCollection parsers_;
-
- /// @brief Pointer to the created subnet object.
- isc::dhcp::Subnet4Ptr subnet_;
};
-/// @brief this class parses list of subnets
+/// @brief this class parses list of DHCP4 subnets
///
/// This is a wrapper parser that handles the whole list of Subnet4
/// definitions. It iterates over all entries and creates Subnet4ConfigParser
@@ -1611,8 +305,9 @@ public:
/// @brief constructor
///
+ /// @param dummy first argument, always ignored. All parsers accept a
+ /// string parameter "name" as their first argument.
Subnets4ListConfigParser(const std::string&) {
- /// parameter name is ignored
}
/// @brief parses contents of the list
@@ -1622,22 +317,17 @@ public:
///
/// @param subnets_list pointer to a list of IPv4 subnets
void build(ConstElementPtr subnets_list) {
-
- // No need to define FactoryMap here. There's only one type
- // used: Subnet4ConfigParser
-
BOOST_FOREACH(ConstElementPtr subnet, subnets_list->listValue()) {
ParserPtr parser(new Subnet4ConfigParser("subnet"));
parser->build(subnet);
subnets_.push_back(parser);
}
-
}
/// @brief commits subnets definitions.
///
- /// Iterates over all Subnet4 parsers. Each parser contains definitions
- /// of a single subnet and its parameters and commits each subnet separately.
+ /// Iterates over all Subnet4 parsers. Each parser contains definitions of
+ /// a single subnet and its parameters and commits each subnet separately.
void commit() {
// @todo: Implement more subtle reconfiguration than toss
// the old one and replace with the new one.
@@ -1675,44 +365,79 @@ namespace dhcp {
///
/// @param config_id pointer to received global configuration entry
/// @return parser for specified global DHCPv4 parameter
-/// @throw NotImplemented if trying to create a parser for unknown config element
+/// @throw NotImplemented if trying to create a parser for unknown
+/// config element
DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id) {
- FactoryMap factories;
-
- factories["valid-lifetime"] = Uint32Parser::factory;
- factories["renew-timer"] = Uint32Parser::factory;
- factories["rebind-timer"] = Uint32Parser::factory;
- factories["interface"] = InterfaceListConfigParser::factory;
- factories["subnet4"] = Subnets4ListConfigParser::factory;
- factories["option-data"] = OptionDataListParser::factory;
- factories["option-def"] = OptionDefListParser::factory;
- factories["version"] = StringParser::factory;
- factories["lease-database"] = DbAccessParser::factory;
-
- FactoryMap::iterator f = factories.find(config_id);
- if (f == factories.end()) {
- // Used for debugging only.
- // return new DebugParser(config_id);
-
+ DhcpConfigParser* parser = NULL;
+ if ((config_id.compare("valid-lifetime") == 0) ||
+ (config_id.compare("renew-timer") == 0) ||
+ (config_id.compare("rebind-timer") == 0)) {
+ parser = new Uint32Parser(config_id,
+ globalContext()->uint32_values_);
+ } else if (config_id.compare("interfaces") == 0) {
+ parser = new InterfaceListConfigParser(config_id);
+ } else if (config_id.compare("subnet4") == 0) {
+ parser = new Subnets4ListConfigParser(config_id);
+ } else if (config_id.compare("option-data") == 0) {
+ parser = new OptionDataListParser(config_id,
+ globalContext()->options_,
+ globalContext(),
+ Dhcp4OptionDataParser::factory);
+ } else if (config_id.compare("option-def") == 0) {
+ parser = new OptionDefListParser(config_id,
+ globalContext()->option_defs_);
+ } else if ((config_id.compare("version") == 0) ||
+ (config_id.compare("next-server") == 0)) {
+ parser = new StringParser(config_id,
+ globalContext()->string_values_);
+ } else if (config_id.compare("lease-database") == 0) {
+ parser = new DbAccessParser(config_id);
+ } else if (config_id.compare("hooks-libraries") == 0) {
+ parser = new HooksLibrariesParser(config_id);
+ } else if (config_id.compare("echo-client-id") == 0) {
+ parser = new BooleanParser(config_id, globalContext()->boolean_values_);
+ } else if (config_id.compare("dhcp-ddns") == 0) {
+ parser = new D2ClientConfigParser(config_id);
+ } else {
isc_throw(NotImplemented,
- "Parser error: Global configuration parameter not supported: "
- << config_id);
+ "Parser error: Global configuration parameter not supported: "
+ << config_id);
+ }
+
+ return (parser);
+}
+
+void commitGlobalOptions() {
+ // Although the function is modest for now, it is certain that the number
+ // of global switches will increase over time, hence the name.
+
+ // Set whether v4 server is supposed to echo back client-id (yes = RFC6842
+ // compatible, no = backward compatibility)
+ try {
+ bool echo_client_id = globalContext()->boolean_values_->getParam("echo-client-id");
+ CfgMgr::instance().echoClientId(echo_client_id);
+ } catch (...) {
+ // Ignore errors. This flag is optional
}
- return (f->second(config_id));
}
isc::data::ConstElementPtr
-configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
+configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) {
if (!config_set) {
ConstElementPtr answer = isc::config::createAnswer(1,
string("Can't parse NULL config"));
return (answer);
}
- /// @todo: append most essential info here (like "2 new subnets configured")
+ /// @todo: Append most essential info here (like "2 new subnets configured")
string config_details;
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND, DHCP4_CONFIG_START).arg(config_set->str());
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_COMMAND,
+ DHCP4_CONFIG_START).arg(config_set->str());
+
+ // Before starting any subnet operations, let's reset the subnet-id counter,
+ // so newly recreated configuration starts with first subnet-id equal 1.
+ Subnet::resetSubnetID();
// Some of the values specified in the configuration depend on
// other values. Typically, the values in the subnet4 structure
@@ -1724,6 +449,12 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
ParserCollection independent_parsers;
ParserPtr subnet_parser;
ParserPtr option_parser;
+ ParserPtr iface_parser;
+
+ // Some of the parsers alter the state of the system in a way that can't
+ // easily be undone. (Or alter it in a way such that undoing the change has
+ // the same risk of failure as doing the change.)
+ ParserPtr hooks_parser;
// The subnet parsers implement data inheritance by directly
// accessing global storage. For this reason the global data
@@ -1732,14 +463,11 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
// parsing operation fails after the global storage has been
// modified. We need to preserve the original global data here
// so as we can rollback changes when an error occurs.
- Uint32Storage uint32_local(uint32_defaults);
- StringStorage string_local(string_defaults);
- OptionStorage option_local(option_defaults);
- OptionDefStorage option_def_local(option_def_intermediate);
+ ParserContext original_context(*globalContext());
- // answer will hold the result.
+ // Answer will hold the result.
ConstElementPtr answer;
- // rollback informs whether error occured and original data
+ // Rollback informs whether error occured and original data
// have to be restored to global storages.
bool rollback = false;
// config_pair holds the details of the current parser when iterating over
@@ -1749,17 +477,26 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
try {
// Make parsers grouping.
const std::map<std::string, ConstElementPtr>& values_map =
- config_set->mapValue();
+ config_set->mapValue();
BOOST_FOREACH(config_pair, values_map) {
ParserPtr parser(createGlobalDhcp4ConfigParser(config_pair.first));
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PARSER_CREATED)
.arg(config_pair.first);
if (config_pair.first == "subnet4") {
subnet_parser = parser;
-
} else if (config_pair.first == "option-data") {
option_parser = parser;
-
+ } else if (config_pair.first == "interfaces") {
+ // The interface parser is independent from any other
+ // parser and can be run here before any other parsers.
+ iface_parser = parser;
+ parser->build(config_pair.second);
+ } else if (config_pair.first == "hooks-libraries") {
+ // Executing commit will alter currently-loaded hooks
+ // libraries. Check if the supplied libraries are valid,
+ // but defer the commit until everything else has committed.
+ hooks_parser = parser;
+ parser->build(config_pair.second);
} else {
// Those parsers should be started before other
// parsers so we can call build straight away.
@@ -1797,7 +534,7 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
rollback = true;
} catch (...) {
- // for things like bad_cast in boost::lexical_cast
+ // For things like bad_cast in boost::lexical_cast
LOG_ERROR(dhcp4_logger, DHCP4_PARSER_EXCEPTION).arg(config_pair.first);
answer = isc::config::createAnswer(1,
string("Configuration parsing failed"));
@@ -1815,29 +552,38 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
if (subnet_parser) {
subnet_parser->commit();
}
+
+ if (iface_parser) {
+ iface_parser->commit();
+ }
+
+ // Apply global options
+ commitGlobalOptions();
+
+ // This occurs last as if it succeeds, there is no easy way
+ // revert it. As a result, the failure to commit a subsequent
+ // change causes problems when trying to roll back.
+ if (hooks_parser) {
+ hooks_parser->commit();
+ }
}
catch (const isc::Exception& ex) {
LOG_ERROR(dhcp4_logger, DHCP4_PARSER_COMMIT_FAIL).arg(ex.what());
answer = isc::config::createAnswer(2,
string("Configuration commit failed: ") + ex.what());
rollback = true;
-
} catch (...) {
- // for things like bad_cast in boost::lexical_cast
+ // For things like bad_cast in boost::lexical_cast
LOG_ERROR(dhcp4_logger, DHCP4_PARSER_COMMIT_EXCEPTION);
answer = isc::config::createAnswer(2,
string("Configuration commit failed"));
rollback = true;
-
}
}
// Rollback changes as the configuration parsing failed.
if (rollback) {
- std::swap(uint32_defaults, uint32_local);
- std::swap(string_defaults, string_local);
- std::swap(option_defaults, option_local);
- std::swap(option_def_intermediate, option_def_local);
+ globalContext().reset(new ParserContext(original_context));
return (answer);
}
@@ -1848,9 +594,12 @@ configureDhcp4Server(Dhcpv4Srv&, ConstElementPtr config_set) {
return (answer);
}
-const Uint32Storage& getUint32Defaults() {
- return (uint32_defaults);
+ParserContextPtr& globalContext() {
+ static ParserContextPtr global_context_ptr(new ParserContext(Option::V4));
+ return (global_context_ptr);
}
+
+
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/bin/dhcp4/config_parser.h b/src/bin/dhcp4/config_parser.h
index 51a7556..3af9911 100644
--- a/src/bin/dhcp4/config_parser.h
+++ b/src/bin/dhcp4/config_parser.h
@@ -12,9 +12,10 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-#include <exceptions/exceptions.h>
-#include <dhcpsrv/dhcp_config_parser.h>
#include <cc/data.h>
+#include <exceptions/exceptions.h>
+#include <dhcpsrv/dhcp_parsers.h>
+
#include <stdint.h>
#include <string>
@@ -29,7 +30,8 @@ namespace dhcp {
class Dhcpv4Srv;
-/// @brief Configure DHCPv4 server (@c Dhcpv4Srv) with a set of configuration values.
+/// @brief Configure DHCPv4 server (@c Dhcpv4Srv) with a set of configuration
+/// values.
///
/// This function parses configuration information stored in @c config_set
/// and configures the @c server by applying the configuration to it.
@@ -42,9 +44,9 @@ class Dhcpv4Srv;
/// (such as malformed configuration or invalid configuration parameter),
/// this function returns appropriate error code.
///
-/// This function is called every time a new configuration is received. The extra
-/// parameter is a reference to DHCPv4 server component. It is currently not used
-/// and CfgMgr::instance() is accessed instead.
+/// This function is called every time a new configuration is received. The
+/// extra parameter is a reference to DHCPv4 server component. It is currently
+/// not used and CfgMgr::instance() is accessed instead.
///
/// This method does not throw. It catches all exceptions and returns them as
/// reconfiguration statuses. It may return the following response codes:
@@ -59,15 +61,10 @@ isc::data::ConstElementPtr
configureDhcp4Server(Dhcpv4Srv&,
isc::data::ConstElementPtr config_set);
-
-/// @brief Returns the global uint32_t values storage.
-///
-/// This function must be only used by unit tests that need
-/// to access uint32_t global storage to verify that the
-/// Uint32Parser works as expected.
+/// @brief Returns the global context
///
-/// @return a reference to a global uint32 values storage.
-const Uint32Storage& getUint32Defaults();
+/// @return a reference to the global context
+ParserContextPtr& globalContext();
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.cc b/src/bin/dhcp4/ctrl_dhcp4_srv.cc
index eb7d559..43c08c9 100644
--- a/src/bin/dhcp4/ctrl_dhcp4_srv.cc
+++ b/src/bin/dhcp4/ctrl_dhcp4_srv.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -19,24 +19,28 @@
#include <cc/session.h>
#include <config/ccsession.h>
#include <dhcp/iface_mgr.h>
-#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcp4/config_parser.h>
#include <dhcp4/ctrl_dhcp4_srv.h>
#include <dhcp4/dhcp4_log.h>
#include <dhcp4/spec_config.h>
-#include <dhcp4/config_parser.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/dhcp_config_parser.h>
#include <exceptions/exceptions.h>
+#include <hooks/hooks_manager.h>
#include <util/buffer.h>
-#include <cassert>
-#include <iostream>
#include <cassert>
#include <iostream>
+#include <sstream>
+#include <string>
+#include <vector>
using namespace isc::asiolink;
using namespace isc::cc;
using namespace isc::config;
using namespace isc::data;
using namespace isc::dhcp;
+using namespace isc::hooks;
using namespace isc::log;
using namespace isc::util;
using namespace std;
@@ -101,7 +105,27 @@ ControlledDhcpv4Srv::dhcp4ConfigHandler(ConstElementPtr new_config) {
}
// Configure the server.
- return (configureDhcp4Server(*server_, merged_config));
+ ConstElementPtr answer = configureDhcp4Server(*server_, merged_config);
+
+ // Check that configuration was successful. If not, do not reopen sockets.
+ int rcode = 0;
+ parseAnswer(rcode, answer);
+ if (rcode != 0) {
+ return (answer);
+ }
+
+ // Configuration may change active interfaces. Therefore, we have to reopen
+ // sockets according to new configuration. This operation is not exception
+ // safe and we really don't want to emit exceptions to the callback caller.
+ // Instead, catch an exception and create appropriate answer.
+ try {
+ server_->openActiveSockets(server_->getPort(), server_->useBroadcast());
+ } catch (std::exception& ex) {
+ std::ostringstream err;
+ err << "failed to open sockets after server reconfiguration: " << ex.what();
+ answer = isc::config::createAnswer(1, err.str());
+ }
+ return (answer);
}
ConstElementPtr
@@ -121,6 +145,21 @@ ControlledDhcpv4Srv::dhcp4CommandHandler(const string& command, ConstElementPtr
ConstElementPtr answer = isc::config::createAnswer(0,
"Shutting down.");
return (answer);
+
+ } else if (command == "libreload") {
+ // TODO delete any stored CalloutHandles referring to the old libraries
+ // Get list of currently loaded libraries and reload them.
+ vector<string> loaded = HooksManager::getLibraryNames();
+ bool status = HooksManager::loadLibraries(loaded);
+ if (!status) {
+ LOG_ERROR(dhcp4_logger, DHCP4_HOOKS_LIBS_RELOAD_FAIL);
+ ConstElementPtr answer = isc::config::createAnswer(1,
+ "Failed to reload hooks libraries.");
+ return (answer);
+ }
+ ConstElementPtr answer = isc::config::createAnswer(0,
+ "Hooks libraries successfully reloaded.");
+ return (answer);
}
ConstElementPtr answer = isc::config::createAnswer(1,
@@ -172,8 +211,13 @@ void ControlledDhcpv4Srv::establishSession() {
try {
configureDhcp4Server(*this, config_session_->getFullConfig());
+ // Configuration may disable or enable interfaces so we have to
+ // reopen sockets according to new configuration.
+ openActiveSockets(getPort(), useBroadcast());
+
} catch (const DhcpConfigError& ex) {
LOG_ERROR(dhcp4_logger, DHCP4_CONFIG_LOAD_FAIL).arg(ex.what());
+
}
/// Integrate the asynchronous I/O model of BIND 10 configuration
@@ -228,6 +272,5 @@ ControlledDhcpv4Srv::execDhcpv4ServerCommand(const std::string& command_id,
}
}
-
};
};
diff --git a/src/bin/dhcp4/ctrl_dhcp4_srv.h b/src/bin/dhcp4/ctrl_dhcp4_srv.h
index c5204b5..59dde87 100644
--- a/src/bin/dhcp4/ctrl_dhcp4_srv.h
+++ b/src/bin/dhcp4/ctrl_dhcp4_srv.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -131,6 +131,7 @@ protected:
/// when there is a new command or configuration sent over msgq.
static void sessionReader(void);
+
/// @brief IOService object, used for all ASIO operations.
isc::asiolink::IOService io_service_;
diff --git a/src/bin/dhcp4/dhcp4.dox b/src/bin/dhcp4/dhcp4.dox
index 86ae845..aa43ee3 100644
--- a/src/bin/dhcp4/dhcp4.dox
+++ b/src/bin/dhcp4/dhcp4.dox
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -79,4 +79,114 @@ See \ref dhcpv6ConfigParser.
Configuration inheritance in DHCPv4 follows exactly the same logic as its DHCPv6
counterpart. See \ref dhcpv6ConfigInherit.
+ at section dhcpv4OptionsParse Custom functions to parse message options
+
+The DHCPv4 server uses the same logic to supply custom callback function to
+parse message option as DHCPv6 server implementation. See \ref dhcpv6OptionsParse.
+
+ at section dhcpv4DDNSIntegration DHCPv4 Server Support for the Dynamic DNS Updates
+T
+he DHCPv4 server supports processing of the DHCPv4 Client FQDN option (RFC4702)
+and the DHCPv4 Host Name option (RFC2132). Client may send one of these options
+to convey its fully qualified or partial name to the server. The server may use
+this name to perform DNS updates for the client. If server receives both options
+in the same message, the DHCPv4 Client FQDN %Option is processed and the Host
+Name option is ignored. If only Host Name Option is present in the client's
+message, it is used to update DNS.
+
+Server may be configured to use a different name to perform DNS update for the
+client. In this case the server will return one of the DHCPv4 Client FQDN or
+Host Name %Option in its response with the name which was selected for the
+client to indicate that this name will be used to perform DNS update.
+
+The b10-dhcp-ddns process is responsible for the actual communication with the
+DNS, i.e. to send DNS update messages. The b10-dhcp4 module is responsible for
+generating @ref isc::dhcp_ddns::NameChangeRequest and sending it to
+the b10-dhcp-ddns module. The @ref isc::dhcp_ddns::NameChangeRequest object
+represents changes to the DNS bindings, related to acquisition, renewal or
+release of the DHCP lease. The b10-dhcp4 module implements the simple FIFO queue
+of the NameChangeRequest objects. The module logic, which processes the incoming
+DHCPv4 Client FQDN and Host Name Options puts these requests into the FIFO queue.
+
+ at todo Currently the FIFO queue is not processed after the NameChangeRequests are
+generated and added to it. In the future implementation steps it is planned to
+create a code which will check if there are any outstanding requests in the queue
+and send them to the b10-dhcp-ddns module when server is idle waiting for DHCP
+messages.
+
+When client gets an address from the server, a DHCPv4 server may generate 0, 1
+or 2 NameChangeRequests during single message processing. Server generates no
+NameChangeRequests if it is not configured to update DNS or it rejects the DNS
+update for any other reason.
+
+Server may generate 1 NameChangeRequest in a case when client acquired a new
+lease or it releases an existing lease. In the former case, the NameChangeRequest
+type is CHG_ADD, which indicates that the b10-dhcp-ddns module should add a new
+DNS binding for the client, and it is assumed that there is no DNS binding for
+this client already. In the latter case, the NameChangeRequest type is CHG_REMOVE
+to indicate to the b10-dhcp-ddns module that an existing DNS binding should be
+removed from the DNS. The binding consists of the forward and reverse mapping.
+The server may only remove the mapping which it had added. Therefore, the lease
+database holds the information which updates (no update, reverse only update,
+forward only update or both reverse and forward update) have been performed when
+the lease was acquired or renewed. Server checks this information to make a
+decision which mapping it is supposed to remove when lease is released.
+
+Server may generate 2 NameChangeRequests in case a client is renewing a lease and
+it already has a DNS binding for that lease. The DHCPv4 server will check if
+there is an existing lease for the client which has sent a message and if DNS
+Updates had been performed for this lease. If the notion of client's FQDN changes,
+comparing to the information stored in the lease database, the DHCPv4 has to
+remove an existing binding from the DNS and then add a new binding according to
+the new FQDN information received from the client. If the client's FQDN
+information (including the client's name and type of update performed) doesn't
+change comparing to the NameChangeRequest is not generated.
+
+The DHCPv4 Client FQDN %Option comprises flags which communicate to the server
+what updates (if any) client expects the server to perform. Server may be
+configured to obey client's preference or to do FQDN processing in a different way.
+If the server overrides client's preference it will communicate it by sending
+the DHCPv4 Client FQDN %Option in its responses to a client, with the appropriate
+flags set.
+
+ at todo Note, that current implementation doesn't allow configuration of the
+server's behaviour with respect to DNS Updates. This is planned for the future.
+The default behaviour is constituted by the set of constants defined in the
+(upper part of) dhcp4_srv.cc file. Once the configuration is implemented,
+these constants will be removed.
+
+ at section dhcpv4Classifier DHCPv4 Client Classification
+
+Kea DHCPv4 server currently supports simplified client classification. It is called
+"simplified", because the incoming packets are classified based on the content
+of the vendor class (60) option. More flexible classification is planned, but there
+are no specific development dates agreed.
+
+For each incoming packet, @ref isc::dhcp::Dhcpv4Srv::classifyPacket() method is called.
+It attempts to extract content of the vendor class option and interpret as a name
+of the class. For now, the code has been tested with two classes used in cable modem
+networks: eRouter1.0 and docsis3.0, but any other content of the vendor class option will
+be interpreted as a class name.
+
+In principle any given packet can belong to zero or more classes. As the current
+classifier is very modest, there's only one way to assign a class (based on vendor class
+option), the ability to assign more than one class to a packet is not yet exercised.
+Neverthless, there is such a possibility and it will be used in a near future. To
+check whether a packet belongs to given class, isc::dhcp::Pkt4::inClass method should
+be used.
+
+Currently there is a short code section that alternates packet processing depending on
+which class it belongs to. It is planned to move that capability to an external hook
+library. See ticket #3275. The class specific behavior is:
+
+- docsis3.0 packets have siaddr (next server) field set
+- docsis3.0 packets have file field set to the content of the boot-file-name option
+- eRouter1.0 packets have siaddr (next server) field cleared
+
+Aforementioned modifications are conducted in @ref isc::dhcp::Dhcpv4Srv::classSpecificProcessing.
+
+ at section dhcpv4Other Other DHCPv4 topics
+
+ For hooks API support in DHCPv4, see @ref dhcpv4Hooks.
+
*/
diff --git a/src/bin/dhcp4/dhcp4.spec b/src/bin/dhcp4/dhcp4.spec
index 59d727e..c6d2c32 100644
--- a/src/bin/dhcp4/dhcp4.spec
+++ b/src/bin/dhcp4/dhcp4.spec
@@ -3,16 +3,30 @@
"module_name": "Dhcp4",
"module_description": "DHCPv4 server daemon",
"config_data": [
- { "item_name": "interface",
+ {
+ "item_name": "hooks-libraries",
+ "item_type": "list",
+ "item_optional": true,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "hooks-library",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ }
+ },
+
+ { "item_name": "interfaces",
"item_type": "list",
"item_optional": false,
- "item_default": [ "all" ],
+ "item_default": [ "*" ],
"list_item_spec":
{
"item_name": "interface_name",
"item_type": "string",
"item_optional": false,
- "item_default": "all"
+ "item_default": "*"
}
} ,
@@ -34,6 +48,18 @@
"item_default": 4000
},
+ { "item_name": "next-server",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+
+ { "item_name": "echo-client-id",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": true
+ },
+
{ "item_name": "option-def",
"item_type": "list",
"item_optional": false,
@@ -204,6 +230,13 @@
"item_optional": false,
"item_default": 7200
},
+
+ { "item_name": "next-server",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "0.0.0.0"
+ },
+
{ "item_name": "pool",
"item_type": "list",
"item_optional": false,
@@ -259,7 +292,103 @@
}
} ]
}
- }
+ },
+
+ { "item_name": "dhcp-ddns",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {"enable-updates": false},
+ "item_description" : "Contains parameters pertaining DHCP-driven DDNS updates",
+ "map_item_spec": [
+ {
+ "item_name": "enable-updates",
+ "item_type": "boolean",
+ "item_optional": false,
+ "item_default": false,
+ "item_description" : "Enables DDNS update processing"
+ },
+ {
+ "item_name": "server-ip",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "127.0.0.1",
+ "item_description" : "IP address of b10-dhcp-ddns (IPv4 or IPv6)"
+ },
+ {
+ "item_name": "server-port",
+ "item_type": "integer",
+ "item_optional": true,
+ "item_default": 53001,
+ "item_description" : "port number of b10-dhcp-ddns"
+ },
+ {
+ "item_name": "ncr-protocol",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "UDP",
+ "item_description" : "Socket protocol to use with b10-dhcp-ddns"
+ },
+ {
+ "item_name": "ncr-format",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "JSON",
+ "item_description" : "Format of the update request packet"
+ },
+ {
+ "item_name": "remove-on-renew",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": false,
+ "item_description": "Enable requesting a DNS Remove, before a DNS Update on renewals"
+ },
+ {
+
+ "item_name": "always-include-fqdn",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": false,
+ "item_description": "Enable always including the FQDN option in its response"
+ },
+ {
+ "item_name": "override-no-update",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": false,
+ "item_description": "Do update, even if client requested no updates with N flag"
+ },
+ {
+ "item_name": "override-client-update",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": false,
+ "item_description": "Server performs an update even if client requested delegation"
+ },
+ {
+ "item_name": "replace-client-name",
+ "item_type": "boolean",
+ "item_optional": true,
+ "item_default": false,
+ "item_description": "Should server replace the domain-name supplied by the client"
+ },
+ {
+ "item_name": "generated-prefix",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "myhost",
+ "item_description": "Prefix to use when generating the client's name"
+ },
+
+ {
+ "item_name": "qualifying-suffix",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "example.com",
+ "item_description": "Fully qualified domain-name suffix if partial name provided by client"
+ },
+ ]
+ },
+
],
"commands": [
{
@@ -272,7 +401,14 @@
"item_optional": true
}
]
+ },
+
+ {
+ "command_name": "libreload",
+ "command_description": "Reloads the current hooks libraries.",
+ "command_args": []
}
+
]
}
}
diff --git a/src/bin/dhcp4/dhcp4_hooks.dox b/src/bin/dhcp4/dhcp4_hooks.dox
new file mode 100644
index 0000000..ebe2d89
--- /dev/null
+++ b/src/bin/dhcp4/dhcp4_hooks.dox
@@ -0,0 +1,216 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page dhcpv4Hooks The Hooks API for the DHCPv4 Server
+
+ @section dhcpv4HooksIntroduction Introduction
+ BIND10 features an API (the "Hooks" API) that allows user-written code to
+ be integrated into BIND 10 and called at specific points in its processing.
+ An overview of the API and a tutorial for writing such code can be found in
+ the @ref hooksdgDevelopersGuide. Information for BIND 10 maintainers can be
+ found in the @ref hooksComponentDeveloperGuide.
+
+ This manual is more specialised and is aimed at developers of hook
+ code for the DHCPv4 server. It describes each hook point, what the callouts
+ attached to the hook are able to do, and the arguments passed to the
+ callouts. Each entry in this manual has the following information:
+
+ - Name of the hook point.
+ - Arguments for the callout. As well as the argument name and data type, the
+ information includes the direction, which can be one of:
+ - @b in - the server passes values to the callout but ignored any data
+ returned.
+ - @b out - the callout is expected to set this value.
+ - <b>in/out</b> - the server passes a value to the callout and uses whatever
+ value the callout sends back. Note that the callout may choose not to
+ do any modification, in which case the server will use whatever value
+ it sent to the callout.
+ - Description of the hook. This explains where in the processing the hook
+ is located, the possible actions a callout attached to this hook could take,
+ and a description of the data passed to the callouts.
+ - Skip flag action: the action taken by the server if a callout chooses to set
+ the "skip" flag.
+
+ at section dhcpv4HooksHookPoints Hooks in the DHCPv4 Server
+
+The following list is ordered by appearance of specific hook points during
+packet processing. Hook points that are not specific to packet processing
+(e.g. lease expiration) will be added to the end of this list.
+
+ @subsection dhcpv4HooksBuffer4Receive buffer4_receive
+
+ - @b Arguments:
+ - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when an incoming DHCPv4
+ buffer is received, before its content is parsed. The sole argument
+ - query4 - contains a pointer to an isc::dhcp::Pkt4 object that
+ contains raw information regarding incoming packet, including its
+ source and destination addresses, interface over which it was
+ received, and a raw buffer stored in data_ field. None of the
+ packet fields (op_, hlen_, chaddr_, etc.) are set yet. Callouts
+ installed on this hook point can modify the incoming buffer. The
+ server will parse the buffer afterwards.
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+ skip the buffer parsing. In such case there is an expectation that
+ the callout will parse the buffer and create option objects (see
+ isc::dhcp::Pkt4::addOption()). Otherwise the server will find out
+ that some mandatory options are missing (e.g. DHCP Message Type) and
+ will drop the packet. If you want to have the capability to drop
+ a message, it is better to use skip flag in pkt4_receive callout.
+
+ @subsection dhcpv4HooksPkt4Receive pkt4_receive
+
+ - @b Arguments:
+ - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when an incoming DHCPv4
+ packet is received and its content has been parsed. The sole
+ argument - query4 - contains a pointer to an isc::dhcp::Pkt4 object
+ that contains all information regarding incoming packet, including
+ its source and destination addresses, interface over which it was
+ received, a list of all options present within and relay
+ information. All fields of the Pkt4 object can be modified at this
+ time, except data_. (By the time this hook is reached, the contents
+ of the data_ field has been already parsed and stored in other
+ fields. Therefore, the modification in the data_ field has no
+ effect.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+ drop the packet and start processing the next one. The reason for the drop
+ will be logged if logging is set to the appropriate debug level.
+
+ at subsection dhcpv4HooksSubnet4Select subnet4_select
+
+ - @b Arguments:
+ - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+ - name: @b subnet4, type: isc::dhcp::Subnet4Ptr, direction: <b>in/out</b>
+ - name: @b subnet4collection, type: const isc::dhcp::Subnet4Collection *, direction: <b>in</b>
+
+ - @b Description: this callout is executed when a subnet is being
+ selected for the incoming packet. All parameters and addresses
+ will be assigned from that subnet. A callout can select a
+ different subnet if it wishes so, the list of all subnets currently
+ configured being provided as 'subnet4collection'. The list itself must
+ not be modified.
+
+ - <b>Skip flag action</b>: If any callout installed on 'subnet4_select'
+ sets the skip flag, the server will not select any subnet. Packet processing
+ will continue, but will be severely limited (i.e. only global options
+ will be assigned).
+
+ at subsection dhcpv4HooksLeaseSelect lease4_select
+
+ - @b Arguments:
+ - name: @b subnet4, type: isc::dhcp::Subnet4Ptr, direction: <b>in</b>
+ - name: @b fake_allocation, type: bool, direction: <b>in</b>
+ - name: @b lease4, type: isc::dhcp::Lease4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed after the server engine
+ has selected a lease for client's request but before the lease has
+ been inserted into the database. Any modifications made to the
+ isc::dhcp::Lease4 object will be stored in the lease's record in
+ the database. The callout should sanity check all modifications as
+ the server will use that data as is with no further checking.\n\n
+ The server processes lease requests for DISCOVER and REQUEST in a
+ very similar way. The only major difference is that for DISCOVER
+ the lease is just selected, but not inserted into the database. It
+ is possible to distinguish between DISCOVER and REQUEST by checking
+ value of the fake_allocation flag: a value of true indicates that the
+ lease won't be inserted into the database (DISCOVER), a value of
+ false indicates that it will (REQUEST).
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease4_select'
+ sets the skip flag, the server will not assign any lease. Packet
+ processing will continue, but client will not get an address.
+
+ at subsection dhcpv4HooksLeaseRenew lease4_renew
+
+ - @b Arguments:
+ - name: @b subnet4, type: isc::dhcp::Subnet4Ptr, direction: <b>in</b>
+ - name: @b clientid, type: isc::dhcp::ClientId, direction: <b>in</b>
+ - name: @b hwaddr, type: isc::dhcp::HWAddr, direction: <b>in</b>
+ - name: @b lease4, type: isc::dhcp::Lease4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when the server engine
+ is about to renew a lease, as a result of receiving REQUEST/Renewing
+ packet. The lease4 argument points to Lease4 object that contains
+ the updated values. Callout can modify those values. Care should be taken
+ as the server will attempt to update the lease in the database without
+ any additional checks.
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease4_renew'
+ sets the skip flag, the server will not update the lease and will
+ use old values instead.
+
+ at subsection dhcpv4HooksLeaseRelease lease4_release
+
+ - @b Arguments:
+ - name: @b query4, type: isc::dhcp::Pkt4Ptr, direction: <b>in</b>
+ - name: @b lease4, type: isc::dhcp::Lease4Ptr, direction: <b>in</b>
+
+ - @b Description: this callout is executed when the server engine
+ is about to release a lease, as a result of receiving RELEASE packet.
+ The lease4 argument points to Lease4 object that contains the lease to
+ be released. It doesn't make sense to modify it at this time.
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease4_release'
+ sets the skip flag, the server will not delete the lease. It will be
+ kept in the database and will go through the regular expiration/reuse
+ process.
+
+ at subsection dhcpv4HooksPkt4Send pkt4_send
+
+ - @b Arguments:
+ - name: @b response4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when server's response
+ is about to be sent back to the client. The sole argument - response4 -
+ contains a pointer to an isc::dhcp::Pkt4 object that contains the
+ packet, with source and destination addresses set, interface over which
+ it will be sent, and a list of all options and relay information. All fields
+ of the Pkt4 object can be modified at this time, except buffer_out_.
+ (This is scratch space used for constructing the packet after all
+ pkt4_send callouts are complete, so any changes to that field will
+ be overwritten.)
+
+ - <b>Skip flag action</b>: if any callout sets the skip flag, the server
+ will not construct raw buffer. The expectation is that if the callout
+ set skip flag, it is responsible for constructing raw form on its own.
+ Otherwise the output packet will be sent with zero length.
+
+ at subsection dhcpv4HooksBuffer4Send buffer4_send
+
+ - @b Arguments:
+ - name: @b response4, type: isc::dhcp::Pkt4Ptr, direction: <b>in/out</b>
+
+ - @b Description: this callout is executed when server's response
+ is about to be sent back to the client. The sole argument - response4 -
+ contains a pointer to an isc::dhcp::Pkt4 object that contains the
+ packet, with source and destination addresses set, interface over which
+ it will be sent, and a list of all options and relay information. The raw
+ on-wire form is already prepared in buffer_out_ (see isc::dhcp::Pkt4::getBuffer())
+ It doesn't make any sense to modify packet fields or options content
+ at this time, because they were already used to construct on-wire buffer.
+
+ - <b>Skip flag action</b>: if any callout sets the skip flag, the server
+ will drop this response packet. However, the original request packet
+ from a client was processed, so server's state was most likely changed
+ (e.g. lease was allocated). Setting this flag merely stops the change
+ being communicated to the client.
+
+
+*/
diff --git a/src/bin/dhcp4/dhcp4_log.h b/src/bin/dhcp4/dhcp4_log.h
index 07d009a..54dbb40 100644
--- a/src/bin/dhcp4/dhcp4_log.h
+++ b/src/bin/dhcp4/dhcp4_log.h
@@ -38,6 +38,9 @@ const int DBG_DHCP4_COMMAND = DBGLVL_COMMAND;
// Trace basic operations within the code.
const int DBG_DHCP4_BASIC = DBGLVL_TRACE_BASIC;
+// Trace hook related operations
+const int DBG_DHCP4_HOOKS = DBGLVL_TRACE_BASIC;
+
// Trace detailed operations, including errors raised when processing invalid
// packets. (These are not logged at severities of WARN or higher for fear
// that a set of deliberately invalid packets set to the server could overwhelm
diff --git a/src/bin/dhcp4/dhcp4_messages.mes b/src/bin/dhcp4/dhcp4_messages.mes
index 8b3e255..297acfd 100644
--- a/src/bin/dhcp4/dhcp4_messages.mes
+++ b/src/bin/dhcp4/dhcp4_messages.mes
@@ -1,4 +1,4 @@
-# Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -14,6 +14,11 @@
$NAMESPACE isc::dhcp
+% DHCP4_ACTIVATE_INTERFACE activating interface %1
+This message is printed when DHCPv4 server enabled an interface to be used
+to receive DHCPv4 traffic. IPv4 socket on this interface will be opened once
+Interface Manager starts up procedure of opening sockets.
+
% DHCP4_CCSESSION_STARTED control channel session started on socket %1
A debug message issued during startup after the IPv4 DHCP server has
successfully established a session with the BIND 10 control channel.
@@ -22,6 +27,19 @@ successfully established a session with the BIND 10 control channel.
This debug message is issued just before the IPv4 DHCP server attempts
to establish a session with the BIND 10 control channel.
+% DHCP4_CLIENT_NAME_PROC_FAIL failed to process the fqdn or hostname sent by a client: %1
+This debug message is issued when the DHCP server was unable to process the
+FQDN or Hostname option sent by a client. This is likely because the client's
+name was malformed or due to internal server error.
+
+% DHCP4_CLASS_PROCESSING_FAILED client class specific processing failed
+This debug message means that the server processing that is unique for each
+client class has reported a failure. The response packet will not be sent.
+
+% DHCP4_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
+This debug message informs that incoming packet has been assigned to specified
+class or classes. This is a norma
+
% DHCP4_COMMAND_RECEIVED received command %1, arguments: %2
A debug message listing the command (and possible arguments) received
from the BIND 10 control system by the IPv4 DHCP server.
@@ -37,7 +55,7 @@ This critical error message indicates that the initial DHCPv4
configuration has failed. The server will start, but nothing will be
served until the configuration has been corrected.
-% DHCP4_CONFIG_NEW_SUBNET A new subnet has been added to configuration: %1
+% DHCP4_CONFIG_NEW_SUBNET a new subnet has been added to configuration: %1
This is an informational message reporting that the configuration has
been extended to include the specified IPv4 subnet.
@@ -60,13 +78,70 @@ This informational message is printed every time DHCPv4 server is started
and gives both the type and name of the database being used to store
lease and other information.
+% DHCP4_DEACTIVATE_INTERFACE deactivate interface %1
+This message is printed when DHCPv4 server disables an interface from being
+used to receive DHCPv4 traffic. Sockets on this interface will not be opened
+by the Interface Manager until interface is enabled.
+
+% DHCP4_DHCID_COMPUTE_ERROR failed to compute the DHCID for lease: %1, reason: %2
+This error message is logged when the attempt to compute DHCID for a specified
+lease has failed. The lease details and reason for failure is logged in the
+message.
+
+% DHCP4_EMPTY_HOSTNAME received empty hostname from the client, skipping processing of this option
+This debug message is issued when the server received an empty Hostname option
+from a client. Server does not process empty Hostname options and therefore
+option is skipped.
+
+% DHCP4_HOOK_BUFFER_RCVD_SKIP received DHCPv4 buffer was dropped because a callout set the skip flag.
+This debug message is printed when a callout installed on buffer4_receive
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+
+% DHCP4_HOOK_BUFFER_SEND_SKIP prepared DHCPv4 response was dropped because a callout set the skip flag.
+This debug message is printed when a callout installed on buffer4_send
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+Server completed all the processing (e.g. may have assigned, updated
+or released leases), but the response will not be send to the client.
+
+% DHCP4_HOOK_LEASE4_RELEASE_SKIP DHCPv4 lease was not released because a callout set the skip flag.
+This debug message is printed when a callout installed on lease4_release
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to not release
+a lease.
+
+% DHCP4_HOOK_PACKET_RCVD_SKIP received DHCPv4 packet was dropped, because a callout set the skip flag.
+This debug message is printed when a callout installed on the pkt4_receive
+hook point sets the skip flag. For this particular hook point, the
+setting of the flag instructs the server to drop the packet.
+
+% DHCP4_HOOK_PACKET_SEND_SKIP prepared DHCPv6 response was not sent, because a callout set skip flag.
+This debug message is printed when a callout installed on the pkt4_send
+hook point sets the skip flag. For this particular hook point, the setting
+of the flag instructs the server to drop the packet. This means that
+the client will not get any response, even though the server processed
+client's request and acted on it (e.g. possibly allocated a lease).
+
+% DHCP4_HOOK_SUBNET4_SELECT_SKIP no subnet was selected, because a callout set skip flag.
+This debug message is printed when a callout installed on the
+subnet4_select hook point sets the skip flag. For this particular hook
+point, the setting of the flag instructs the server not to choose a
+subnet, an action that severely limits further processing; the server
+will be only able to offer global options - no addresses will be assigned.
+
+% DHCP4_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
+A "libreload" command was issued to reload the hooks libraries but for
+some reason the reload failed. Other error messages issued from the
+hooks framework will indicate the nature of the problem.
+
% DHCP4_LEASE_ADVERT lease %1 advertised (client client-id %2, hwaddr %3)
This debug message indicates that the server successfully advertised
a lease. It is up to the client to choose one server out of othe advertised
and continue allocation with that server. This is a normal behavior and
indicates successful operation.
-% DHCP4_LEASE_ADVERT_FAIL failed to advertise a lease for client client-id %1, hwaddr %2
+% DHCP4_LEASE_ADVERT_FAIL failed to advertise a lease for client client-id %1, hwaddr %2, client sent yiaddr %3
This message indicates that the server has failed to offer a lease to
the specified client after receiving a DISCOVER message from it. There are
many possible reasons for such a failure.
@@ -76,12 +151,30 @@ This debug message indicates that the server successfully granted a lease
in response to client's REQUEST message. This is a normal behavior and
indicates successful operation.
-% DHCP4_LEASE_ALLOC_FAIL failed to grant a lease for client-id %1, hwaddr %2
+% DHCP4_LEASE_ALLOC_FAIL failed to grant a lease for client-id %1, hwaddr %2, client sent yiaddr %3
This message indicates that the server failed to grant a lease to the
specified client after receiving a REQUEST message from it. There are many
possible reasons for such a failure. Additional messages will indicate the
reason.
+% DHCP4_NAME_GEN_UPDATE_FAIL failed to update the lease after generating name for a client: %1
+This message indicates the failure when trying to update the lease and/or
+options in the server's response with the hostname generated by the server
+from the acquired address. The message argument indicates the reason for the
+failure.
+
+% DHCP4_NCR_CREATION_FAILED failed to generate name change requests for DNS: %1
+This message indicates that server was unable to generate NameChangeRequests
+which should be sent to the b10-dhcp_ddns module to create
+new DNS records for the lease being acquired or to update existing records
+for the renewed lease. The reason for the failure is printed in the logged
+message.
+
+% DHCP4_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
+This warning message is issued when current server configuration specifies
+no interfaces that server should listen on, or specified interfaces are not
+configured to receive the traffic.
+
% DHCP4_NOT_RUNNING IPv4 DHCP server is not running
A warning message is issued when an attempt is made to shut down the
IPv4 DHCP server but it is not running.
@@ -90,6 +183,17 @@ IPv4 DHCP server but it is not running.
A debug message issued during startup, this indicates that the IPv4 DHCP
server is about to open sockets on the specified port.
+% DHCP4_PACKET_NOT_FOR_US received DHCPv4 message (transid=%1, iface=%2) dropped because it contains foreign server identifier
+This debug message is issued when received DHCPv4 message is dropped because
+it is addressed to a different server, i.e. a server identifier held by
+this message doesn't match the identifier used by our server. The arguments
+of this message hold the name of the transaction id and interface on which
+the message has been received.
+
+% DHCP4_OPEN_SOCKET_FAIL failed to create socket: %1
+A warning message issued when IfaceMgr fails to open and bind a socket. The reason
+for the failure is appended as an argument of the log message.
+
% DHCP4_PACKET_PARSE_FAIL failed to parse incoming packet: %1
The IPv4 DHCP server has received a packet that it is unable to
interpret. The reason why the packet is invalid is included in the message.
@@ -99,6 +203,10 @@ This is a general catch-all message indicating that the processing of a
received packet failed. The reason is given in the message. The server
will not send a response but will instead ignore the packet.
+% DHCP4_PACKET_DROP_NO_TYPE packet received on interface %1 dropped, because of missing msg-type option
+This is a debug message informing that incoming DHCPv4 packet did not
+have mandatory DHCP message type option and thus was dropped.
+
% DHCP4_PACKET_RECEIVED %1 (type %2) packet received on interface %3
A debug message noting that the server has received the specified type of
packet on the specified interface. Note that a packet marked as UNKNOWN
@@ -115,11 +223,6 @@ This error is output if the IPv4 DHCP server fails to send an assembled
DHCP message to a client. The reason for the error is included in the
message.
-% DHCP4_PACK_FAIL failed to assemble response correctly
-This error is output if the server failed to assemble the data to be
-returned to the client into a valid packet. The cause is most likely
-to be a programming error: please raise a bug report.
-
% DHCP4_PARSER_COMMIT_EXCEPTION parser failed to commit changes
On receipt of message containing details to a change of the IPv4 DHCP
server configuration, a set of parsers were successfully created, but one
@@ -155,6 +258,12 @@ failure is given in the message.
% DHCP4_QUERY_DATA received packet type %1, data is <%2>
A debug message listing the data received from the client.
+% DHCP4_QUEUE_NCR name change request to %1 DNS entry queued: %2
+A debug message which is logged when the NameChangeRequest to add or remove
+a DNS entries for a particular lease has been queued. The first parameter of
+this log message indicates whether the DNS entry is to be added or removed.
+The second parameter carries the details of the NameChangeRequest.
+
% DHCP4_RELEASE address %1 belonging to client-id %2, hwaddr %3 was released properly.
This debug message indicates that an address was released properly. It
is a normal operation during client shutdown.
@@ -195,26 +304,6 @@ both clones use the same client-id.
% DHCP4_RESPONSE_DATA responding with packet type %1, data is <%2>
A debug message listing the data returned to the client.
-% DHCP4_SERVERID_GENERATED server-id %1 has been generated and will be stored in %2
-This informational messages indicates that the server was not able to
-read its server identifier and has generated a new one. This server-id
-will be stored in a file and will be read (and used) whenever the server
-is restarted. This is normal behavior when the server is started for the
-first time. If this message is printed every time the server is started,
-please check that the server has sufficient permission to write its
-server-id file and that the file is not corrupt.
-
-% DHCP4_SERVERID_LOADED server-id %1 has been loaded from file %2
-This debug message indicates that the server loaded its server identifier.
-That value is sent in all server responses and clients use it to
-discriminate between servers. This is a part of normal startup or
-reconfiguration procedure.
-
-% DHCP4_SERVERID_WRITE_FAIL server was not able to write its ID to file %1
-This warning message indicates that server was not able to write its
-server identifier to a file. The most likely cause is is that the server
-does not have permissions to write the server id file.
-
% DHCP4_SERVER_FAILED server failed: %1
The IPv4 DHCP server has encountered a fatal error and is terminating.
The reason for the failure is included in the message.
diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc
index 6f119ed..5fc5598 100644
--- a/src/bin/dhcp4/dhcp4_srv.cc
+++ b/src/bin/dhcp4/dhcp4_srv.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -14,78 +14,130 @@
#include <asiolink/io_address.h>
#include <dhcp/dhcp4.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/option_string.h>
#include <dhcp/pkt4.h>
-#include <dhcp/duid.h>
-#include <dhcp/hwaddr.h>
+#include <dhcp/docsis3_option_defs.h>
#include <dhcp4/dhcp4_log.h>
#include <dhcp4/dhcp4_srv.h>
-#include <dhcpsrv/utils.h>
+#include <dhcpsrv/addr_utilities.h>
+#include <dhcpsrv/callout_handle_store.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/utils.h>
-#include <dhcpsrv/addr_utilities.h>
+#include <dhcpsrv/utils.h>
+#include <hooks/callout_handle.h>
+#include <hooks/hooks_manager.h>
+#include <util/strutil.h>
-#include <boost/algorithm/string/erase.hpp>
+#include <boost/bind.hpp>
+#include <boost/foreach.hpp>
#include <iomanip>
-#include <fstream>
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
+using namespace isc::dhcp_ddns;
+using namespace isc::hooks;
using namespace isc::log;
using namespace std;
+/// Structure that holds registered hook indexes
+struct Dhcp4Hooks {
+ int hook_index_buffer4_receive_;///< index for "buffer4_receive" hook point
+ int hook_index_pkt4_receive_; ///< index for "pkt4_receive" hook point
+ int hook_index_subnet4_select_; ///< index for "subnet4_select" hook point
+ int hook_index_lease4_release_; ///< index for "lease4_release" hook point
+ int hook_index_pkt4_send_; ///< index for "pkt4_send" hook point
+ int hook_index_buffer4_send_; ///< index for "buffer4_send" hook point
+
+ /// Constructor that registers hook points for DHCPv4 engine
+ Dhcp4Hooks() {
+ hook_index_buffer4_receive_= HooksManager::registerHook("buffer4_receive");
+ hook_index_pkt4_receive_ = HooksManager::registerHook("pkt4_receive");
+ hook_index_subnet4_select_ = HooksManager::registerHook("subnet4_select");
+ hook_index_pkt4_send_ = HooksManager::registerHook("pkt4_send");
+ hook_index_lease4_release_ = HooksManager::registerHook("lease4_release");
+ hook_index_buffer4_send_ = HooksManager::registerHook("buffer4_send");
+ }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp4Hooks Hooks;
+
namespace isc {
namespace dhcp {
-/// @brief file name of a server-id file
-///
-/// Server must store its server identifier in persistent storage that must not
-/// change between restarts. This is name of the file that is created in dataDir
-/// (see isc::dhcp::CfgMgr::getDataDir()). It is a text file that uses
-/// regular IPv4 address, e.g. 192.0.2.1. Server will create it during
-/// first run and then use it afterwards.
-static const char* SERVER_ID_FILE = "b10-dhcp4-serverid";
+namespace {
+
+// @todo The following constants describe server's behavior with respect to the
+// DHCPv4 Client FQDN Option sent by a client. They will be removed
+// when DDNS parameters for DHCPv4 are implemented with the ticket #3033.
+
+// @todo Additional configuration parameter which we may consider is the one
+// that controls whether the DHCP server sends the removal NameChangeRequest
+// if it discovers that the entry for the particular client exists or that
+// it always updates the DNS.
+
+// Should server always include the FQDN option in its response, regardless
+// if it has been requested in Parameter Request List Option (Disabled).
+const bool FQDN_ALWAYS_INCLUDE = false;
+// Enable A RR update delegation to the client (Disabled).
+const bool FQDN_ALLOW_CLIENT_UPDATE = false;
+// Globally enable updates (Enabled).
+const bool FQDN_ENABLE_UPDATE = true;
+// Do update, even if client requested no updates with N flag (Disabled).
+const bool FQDN_OVERRIDE_NO_UPDATE = false;
+// Server performs an update when client requested delegation (Enabled).
+const bool FQDN_OVERRIDE_CLIENT_UPDATE = true;
+// The fully qualified domain-name suffix if partial name provided by
+// a client.
+const char* FQDN_PARTIAL_SUFFIX = "example.com";
+// Should server replace the domain-name supplied by the client (Disabled).
+const bool FQDN_REPLACE_CLIENT_NAME = false;
-// These are hardcoded parameters. Currently this is a skeleton server that only
-// grants those options and a single, fixed, hardcoded lease.
+}
+
+Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast,
+ const bool direct_response_desired)
+: shutdown_(true), alloc_engine_(), port_(port),
+ use_bcast_(use_bcast), hook_index_pkt4_receive_(-1),
+ hook_index_subnet4_select_(-1), hook_index_pkt4_send_(-1) {
-Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast) {
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_OPEN_SOCKET).arg(port);
try {
- // First call to instance() will create IfaceMgr (it's a singleton)
- // it may throw something if things go wrong
- IfaceMgr::instance();
-
+ // Open sockets only if port is non-zero. Port 0 is used for testing
+ // purposes in two cases:
+ // - when non-socket related testing is performed
+ // - when the particular test supplies its own packet filtering class.
if (port) {
- // open sockets only if port is non-zero. Port 0 is used
- // for non-socket related testing.
- IfaceMgr::instance().openSockets4(port, use_bcast);
- }
-
- string srvid_file = CfgMgr::instance().getDataDir() + "/" + string(SERVER_ID_FILE);
- if (loadServerID(srvid_file)) {
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_START, DHCP4_SERVERID_LOADED)
- .arg(srvidToString(getServerID()))
- .arg(srvid_file);
- } else {
- generateServerID();
- LOG_INFO(dhcp4_logger, DHCP4_SERVERID_GENERATED)
- .arg(srvidToString(getServerID()))
- .arg(srvid_file);
-
- if (!writeServerID(srvid_file)) {
- LOG_WARN(dhcp4_logger, DHCP4_SERVERID_WRITE_FAIL)
- .arg(srvid_file);
- }
-
+ // First call to instance() will create IfaceMgr (it's a singleton)
+ // it may throw something if things go wrong.
+ // The 'true' value of the call to setMatchingPacketFilter imposes
+ // that IfaceMgr will try to use the mechanism to respond directly
+ // to the client which doesn't have address assigned. This capability
+ // may be lacking on some OSes, so there is no guarantee that server
+ // will be able to respond directly.
+ IfaceMgr::instance().setMatchingPacketFilter(direct_response_desired);
+
+ // Create error handler. This handler will be called every time
+ // the socket opening operation fails. We use this handler to
+ // log a warning.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&Dhcpv4Srv::ifaceMgrSocket4ErrorHandler, _1);
+ IfaceMgr::instance().openSockets4(port_, use_bcast_, error_handler);
}
// Instantiate LeaseMgr
@@ -95,7 +147,15 @@ Dhcpv4Srv::Dhcpv4Srv(uint16_t port, const char* dbconfig, const bool use_bcast)
.arg(LeaseMgrFactory::instance().getName());
// Instantiate allocation engine
- alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
+ alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100,
+ false /* false = IPv4 */));
+
+ // Register hook points
+ hook_index_pkt4_receive_ = Hooks.hook_index_pkt4_receive_;
+ hook_index_subnet4_select_ = Hooks.hook_index_subnet4_select_;
+ hook_index_pkt4_send_ = Hooks.hook_index_pkt4_send_;
+
+ /// @todo call loadLibraries() when handling configuration changes
} catch (const std::exception &e) {
LOG_ERROR(dhcp4_logger, DHCP4_SRV_CONSTRUCT_ERROR).arg(e.what());
@@ -116,204 +176,296 @@ Dhcpv4Srv::shutdown() {
shutdown_ = true;
}
+Pkt4Ptr
+Dhcpv4Srv::receivePacket(int timeout) {
+ return (IfaceMgr::instance().receive4(timeout));
+}
+
+void
+Dhcpv4Srv::sendPacket(const Pkt4Ptr& packet) {
+ IfaceMgr::instance().send(packet);
+}
+
bool
Dhcpv4Srv::run() {
while (!shutdown_) {
/// @todo: calculate actual timeout once we have lease database
- int timeout = 1000;
+ //cppcheck-suppress variableScope This is temporary anyway
+ const int timeout = 1000;
// client's message and server's response
Pkt4Ptr query;
Pkt4Ptr rsp;
try {
- query = IfaceMgr::instance().receive4(timeout);
+ query = receivePacket(timeout);
} catch (const std::exception& e) {
LOG_ERROR(dhcp4_logger, DHCP4_PACKET_RECEIVE_FAIL).arg(e.what());
}
- if (query) {
+ // Timeout may be reached or signal received, which breaks select()
+ // with no reception ocurred
+ if (!query) {
+ continue;
+ }
+
+ // In order to parse the DHCP options, the server needs to use some
+ // configuration information such as: existing option spaces, option
+ // definitions etc. This is the kind of information which is not
+ // available in the libdhcp, so we need to supply our own implementation
+ // of the option parsing function here, which would rely on the
+ // configuration data.
+ query->setCallback(boost::bind(&Dhcpv4Srv::unpackOptions, this,
+ _1, _2, _3));
+
+ bool skip_unpack = false;
+
+ // The packet has just been received so contains the uninterpreted wire
+ // data; execute callouts registered for buffer4_receive.
+ if (HooksManager::calloutsPresent(Hooks.hook_index_buffer4_receive_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("query4", query);
+
+ // Call callouts
+ HooksManager::callCallouts(Hooks.hook_index_buffer4_receive_,
+ *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to parse the packet, so skip at this
+ // stage means that callouts did the parsing already, so server
+ // should skip parsing.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_BUFFER_RCVD_SKIP);
+ skip_unpack = true;
+ }
+
+ callout_handle->getArgument("query4", query);
+ }
+
+ // Unpack the packet information unless the buffer4_receive callouts
+ // indicated they did it
+ if (!skip_unpack) {
try {
query->unpack();
-
} catch (const std::exception& e) {
// Failed to parse the packet.
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL,
DHCP4_PACKET_PARSE_FAIL).arg(e.what());
continue;
}
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PACKET_RECEIVED)
- .arg(serverReceivedPacketName(query->getType()))
- .arg(query->getType())
- .arg(query->getIface());
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUERY_DATA)
- .arg(query->toText());
-
- try {
- switch (query->getType()) {
- case DHCPDISCOVER:
- rsp = processDiscover(query);
- break;
-
- case DHCPREQUEST:
- rsp = processRequest(query);
- break;
-
- case DHCPRELEASE:
- processRelease(query);
- break;
-
- case DHCPDECLINE:
- processDecline(query);
- break;
-
- case DHCPINFORM:
- processInform(query);
- break;
-
- default:
- // Only action is to output a message if debug is enabled,
- // and that is covered by the debug statement before the
- // "switch" statement.
- ;
- }
- } catch (const isc::Exception& e) {
-
- // Catch-all exception (at least for ones based on the isc
- // Exception class, which covers more or less all that
- // are explicitly raised in the BIND 10 code). Just log
- // the problem and ignore the packet. (The problem is logged
- // as a debug message because debug is disabled by default -
- // it prevents a DDOS attack based on the sending of problem
- // packets.)
- if (dhcp4_logger.isDebugEnabled(DBG_DHCP4_BASIC)) {
- std::string source = "unknown";
- HWAddrPtr hwptr = query->getHWAddr();
- if (hwptr) {
- source = hwptr->toText();
- }
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC,
- DHCP4_PACKET_PROCESS_FAIL)
- .arg(source).arg(e.what());
- }
- }
+ }
- if (rsp) {
- if (rsp->getRemoteAddr().toText() == "0.0.0.0") {
- rsp->setRemoteAddr(query->getRemoteAddr());
- }
- if (!rsp->getHops()) {
- rsp->setRemotePort(DHCP4_CLIENT_PORT);
- } else {
- rsp->setRemotePort(DHCP4_SERVER_PORT);
- }
+ // Check if the DHCPv4 packet has been sent to us or to someone else.
+ // If it hasn't been sent to us, drop it!
+ if (!acceptServerId(query)) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PACKET_NOT_FOR_US)
+ .arg(query->getTransid())
+ .arg(query->getIface());
+ continue;
+ }
- rsp->setLocalAddr(query->getLocalAddr());
- rsp->setLocalPort(DHCP4_SERVER_PORT);
- rsp->setIface(query->getIface());
- rsp->setIndex(query->getIndex());
-
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA,
- DHCP4_RESPONSE_DATA)
- .arg(rsp->getType()).arg(rsp->toText());
-
- if (rsp->pack()) {
- try {
- IfaceMgr::instance().send(rsp);
- } catch (const std::exception& e) {
- LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL).arg(e.what());
- }
- } else {
- LOG_ERROR(dhcp4_logger, DHCP4_PACK_FAIL);
- }
- }
+ // When receiving a packet without message type option, getType() will
+ // throw. Let's set type to -1 as default error indicator.
+ int type = -1;
+ try {
+ type = query->getType();
+ } catch (...) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PACKET_DROP_NO_TYPE)
+ .arg(query->getIface());
+ continue;
}
- }
- return (true);
-}
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PACKET_RECEIVED)
+ .arg(serverReceivedPacketName(type))
+ .arg(type)
+ .arg(query->getIface());
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUERY_DATA)
+ .arg(type)
+ .arg(query->toText());
+
+ // Let's execute all callouts registered for pkt4_receive
+ if (HooksManager::calloutsPresent(hook_index_pkt4_receive_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("query4", query);
+
+ // Call callouts
+ HooksManager::callCallouts(hook_index_pkt4_receive_,
+ *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to process the packet, so skip at this
+ // stage means drop.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_PACKET_RCVD_SKIP);
+ continue;
+ }
-bool
-Dhcpv4Srv::loadServerID(const std::string& file_name) {
+ callout_handle->getArgument("query4", query);
+ }
- // load content of the file into a string
- fstream f(file_name.c_str(), ios::in);
- if (!f.is_open()) {
- return (false);
- }
+ // Assign this packet to one or more classes if needed
+ classifyPacket(query);
- string hex_string;
- f >> hex_string;
- f.close();
+ try {
+ switch (query->getType()) {
+ case DHCPDISCOVER:
+ rsp = processDiscover(query);
+ break;
+
+ case DHCPREQUEST:
+ // Note that REQUEST is used for many things in DHCPv4: for
+ // requesting new leases, renewing existing ones and even
+ // for rebinding.
+ rsp = processRequest(query);
+ break;
+
+ case DHCPRELEASE:
+ processRelease(query);
+ break;
+
+ case DHCPDECLINE:
+ processDecline(query);
+ break;
+
+ case DHCPINFORM:
+ processInform(query);
+ break;
+
+ default:
+ // Only action is to output a message if debug is enabled,
+ // and that is covered by the debug statement before the
+ // "switch" statement.
+ ;
+ }
+ } catch (const isc::Exception& e) {
+
+ // Catch-all exception (at least for ones based on the isc
+ // Exception class, which covers more or less all that
+ // are explicitly raised in the BIND 10 code). Just log
+ // the problem and ignore the packet. (The problem is logged
+ // as a debug message because debug is disabled by default -
+ // it prevents a DDOS attack based on the sending of problem
+ // packets.)
+ if (dhcp4_logger.isDebugEnabled(DBG_DHCP4_BASIC)) {
+ std::string source = "unknown";
+ HWAddrPtr hwptr = query->getHWAddr();
+ if (hwptr) {
+ source = hwptr->toText();
+ }
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC,
+ DHCP4_PACKET_PROCESS_FAIL)
+ .arg(source).arg(e.what());
+ }
+ }
- // remove any spaces
- boost::algorithm::erase_all(hex_string, " ");
+ if (!rsp) {
+ continue;
+ }
- try {
- IOAddress addr(hex_string);
+ // Let's do class specific processing. This is done before
+ // pkt4_send.
+ //
+ /// @todo: decide whether we want to add a new hook point for
+ /// doing class specific processing.
+ if (!classSpecificProcessing(query, rsp)) {
+ /// @todo add more verbosity here
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_PROCESSING_FAILED);
- if (!addr.isV4()) {
- return (false);
+ continue;
}
- // Now create server-id option
- serverid_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER, addr));
+ // Specifies if server should do the packing
+ bool skip_pack = false;
- } catch(...) {
- // any kind of malformed input (empty string, IPv6 address, complete
- // garbate etc.)
- return (false);
- }
+ // Execute all callouts registered for pkt4_send
+ if (HooksManager::calloutsPresent(hook_index_pkt4_send_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
- return (true);
-}
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
-void
-Dhcpv4Srv::generateServerID() {
+ // Clear skip flag if it was set in previous callouts
+ callout_handle->setSkip(false);
- const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+ // Set our response
+ callout_handle->setArgument("response4", rsp);
- // Let's find suitable interface.
- for (IfaceMgr::IfaceCollection::const_iterator iface = ifaces.begin();
- iface != ifaces.end(); ++iface) {
+ // Call all installed callouts
+ HooksManager::callCallouts(hook_index_pkt4_send_,
+ *callout_handle);
- // Let's don't use loopback.
- if (iface->flag_loopback_) {
- continue;
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to send the packet, so skip at this
+ // stage means "drop response".
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS, DHCP4_HOOK_PACKET_SEND_SKIP);
+ skip_pack = true;
+ }
}
- // Let's skip downed interfaces. It is better to use working ones.
- if (!iface->flag_up_) {
- continue;
+ if (!skip_pack) {
+ try {
+ rsp->pack();
+ } catch (const std::exception& e) {
+ LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL)
+ .arg(e.what());
+ }
}
- const Iface::AddressCollection addrs = iface->getAddresses();
+ try {
+ // Now all fields and options are constructed into output wire buffer.
+ // Option objects modification does not make sense anymore. Hooks
+ // can only manipulate wire buffer at this stage.
+ // Let's execute all callouts registered for buffer4_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_buffer4_send_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("response4", rsp);
+
+ // Call callouts
+ HooksManager::callCallouts(Hooks.hook_index_buffer4_send_,
+ *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to parse the packet, so skip at this
+ // stage means drop.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS,
+ DHCP4_HOOK_BUFFER_SEND_SKIP);
+ continue;
+ }
- for (Iface::AddressCollection::const_iterator addr = addrs.begin();
- addr != addrs.end(); ++addr) {
- if (addr->getFamily() != AF_INET) {
- continue;
+ callout_handle->getArgument("response4", rsp);
}
- serverid_ = OptionPtr(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
- *addr));
- return;
- }
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA,
+ DHCP4_RESPONSE_DATA)
+ .arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
+ sendPacket(rsp);
+ } catch (const std::exception& e) {
+ LOG_ERROR(dhcp4_logger, DHCP4_PACKET_SEND_FAIL)
+ .arg(e.what());
+ }
+ // Send NameChangeRequests to the b10-dhcp_ddns module.
+ sendNameChangeRequests();
}
- isc_throw(BadValue, "No suitable interfaces for server-identifier found");
-}
-
-bool
-Dhcpv4Srv::writeServerID(const std::string& file_name) {
- fstream f(file_name.c_str(), ios::out | ios::trunc);
- if (!f.good()) {
- return (false);
- }
- f << srvidToString(getServerID());
- f.close();
return (true);
}
@@ -337,6 +489,52 @@ Dhcpv4Srv::srvidToString(const OptionPtr& srvid) {
return (addrs[0].toText());
}
+isc::dhcp_ddns::D2Dhcid
+Dhcpv4Srv::computeDhcid(const Lease4Ptr& lease) {
+ if (!lease) {
+ isc_throw(DhcidComputeError, "a pointer to the lease must be not"
+ " NULL to compute DHCID");
+
+ } else if (lease->hostname_.empty()) {
+ isc_throw(DhcidComputeError, "unable to compute the DHCID for the"
+ " lease which has empty hostname set");
+
+ }
+
+ // In order to compute DHCID the client's hostname must be encoded in
+ // canonical wire format. It is unlikely that the FQDN is malformed
+ // because it is validated by the classes which encapsulate options
+ // carrying client FQDN. However, if the client name was carried in the
+ // Hostname option it is more likely as it carries the hostname as a
+ // regular string.
+ std::vector<uint8_t> fqdn_wire;
+ try {
+ OptionDataTypeUtil::writeFqdn(lease->hostname_, fqdn_wire, true);
+
+ } catch (const Exception& ex) {
+ isc_throw(DhcidComputeError, "unable to compute DHCID because the"
+ " hostname: " << lease->hostname_ << " is invalid");
+
+ }
+
+ // Prefer client id to HW address to compute DHCID. If Client Id is
+ // NULL, use HW address.
+ try {
+ if (lease->client_id_) {
+ return (D2Dhcid(lease->client_id_->getClientId(), fqdn_wire));
+
+ } else {
+ HWAddrPtr hwaddr(new HWAddr(lease->hwaddr_, HTYPE_ETHER));
+ return (D2Dhcid(hwaddr, fqdn_wire));
+ }
+ } catch (const Exception& ex) {
+ isc_throw(DhcidComputeError, "unable to compute DHCID: "
+ << ex.what());
+
+ }
+
+}
+
void
Dhcpv4Srv::copyDefaultFields(const Pkt4Ptr& question, Pkt4Ptr& answer) {
answer->setIface(question->getIface());
@@ -352,19 +550,35 @@ Dhcpv4Srv::copyDefaultFields(const Pkt4Ptr& question, Pkt4Ptr& answer) {
// relay address
answer->setGiaddr(question->getGiaddr());
- if (question->getGiaddr().toText() != "0.0.0.0") {
- // relayed traffic
- answer->setRemoteAddr(question->getGiaddr());
- } else {
- // direct traffic
- answer->setRemoteAddr(question->getRemoteAddr());
- }
-
// Let's copy client-id to response. See RFC6842.
+ // It is possible to disable RFC6842 to keep backward compatibility
+ bool echo = CfgMgr::instance().echoClientId();
OptionPtr client_id = question->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
- if (client_id) {
+ if (client_id && echo) {
answer->addOption(client_id);
}
+
+ // If src/dest HW addresses are used by the packet filtering class
+ // we need to copy them as well. There is a need to check that the
+ // address being set is not-NULL because an attempt to set the NULL
+ // HW would result in exception. If these values are not set, the
+ // the default HW addresses (zeroed) should be generated by the
+ // packet filtering class when creating Ethernet header for
+ // outgoing packet.
+ HWAddrPtr src_hw_addr = question->getLocalHWAddr();
+ if (src_hw_addr) {
+ answer->setLocalHWAddr(src_hw_addr);
+ }
+ HWAddrPtr dst_hw_addr = question->getRemoteHWAddr();
+ if (dst_hw_addr) {
+ answer->setRemoteHWAddr(dst_hw_addr);
+ }
+
+ // If this packet is relayed, we want to copy Relay Agent Info option
+ OptionPtr rai = question->getOption(DHO_DHCP_AGENT_OPTIONS);
+ if (rai) {
+ answer->addOption(rai);
+ }
}
void
@@ -374,13 +588,22 @@ Dhcpv4Srv::appendDefaultOptions(Pkt4Ptr& msg, uint8_t msg_type) {
// add Message Type Option (type 53)
msg->setType(msg_type);
- // DHCP Server Identifier (type 54)
- msg->addOption(getServerID());
-
// more options will be added here later
}
void
+Dhcpv4Srv::appendServerID(const Pkt4Ptr& response) {
+ // The source address for the outbound message should have been set already.
+ // This is the address that to the best of the server's knowledge will be
+ // available from the client.
+ // @todo: perhaps we should consider some more sophisticated server id
+ // generation, but for the current use cases, it should be ok.
+ response->addOption(OptionPtr(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
+ response->getLocalAddr()))
+ );
+}
+
+void
Dhcpv4Srv::appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
// Get the subnet relevant for the client. We will need it
@@ -410,20 +633,78 @@ Dhcpv4Srv::appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
// to be returned to the client.
for (std::vector<uint8_t>::const_iterator opt = requested_opts.begin();
opt != requested_opts.end(); ++opt) {
- Subnet::OptionDescriptor desc =
- subnet->getOptionDescriptor("dhcp4", *opt);
- if (desc.option) {
- msg->addOption(desc.option);
+ if (!msg->getOption(*opt)) {
+ Subnet::OptionDescriptor desc =
+ subnet->getOptionDescriptor("dhcp4", *opt);
+ if (desc.option && !msg->getOption(*opt)) {
+ msg->addOption(desc.option);
+ }
+ }
+ }
+}
+
+void
+Dhcpv4Srv::appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer) {
+ // Get the configured subnet suitable for the incoming packet.
+ Subnet4Ptr subnet = selectSubnet(question);
+ // Leave if there is no subnet matching the incoming packet.
+ // There is no need to log the error message here because
+ // it will be logged in the assignLease() when it fails to
+ // pick the suitable subnet. We don't want to duplicate
+ // error messages in such case.
+ if (!subnet) {
+ return;
+ }
+
+ // Try to get the vendor option
+ boost::shared_ptr<OptionVendor> vendor_req =
+ boost::dynamic_pointer_cast<OptionVendor>(question->getOption(DHO_VIVSO_SUBOPTIONS));
+ if (!vendor_req) {
+ return;
+ }
+
+ uint32_t vendor_id = vendor_req->getVendorId();
+
+ // Let's try to get ORO within that vendor-option
+ /// @todo This is very specific to vendor-id=4491 (Cable Labs). Other vendors
+ /// may have different policies.
+ OptionUint8ArrayPtr oro =
+ boost::dynamic_pointer_cast<OptionUint8Array>(vendor_req->getOption(DOCSIS3_V4_ORO));
+
+ // Option ORO not found. Don't do anything then.
+ if (!oro) {
+ return;
+ }
+
+ boost::shared_ptr<OptionVendor> vendor_rsp(new OptionVendor(Option::V4, vendor_id));
+
+ // Get the list of options that client requested.
+ bool added = false;
+ const std::vector<uint8_t>& requested_opts = oro->getValues();
+
+ for (std::vector<uint8_t>::const_iterator code = requested_opts.begin();
+ code != requested_opts.end(); ++code) {
+ if (!vendor_rsp->getOption(*code)) {
+ Subnet::OptionDescriptor desc = subnet->getVendorOptionDescriptor(vendor_id,
+ *code);
+ if (desc.option) {
+ vendor_rsp->addOption(desc.option);
+ added = true;
+ }
+ }
+
+ if (added) {
+ answer->addOption(vendor_rsp);
}
}
}
+
void
Dhcpv4Srv::appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
// Identify options that we always want to send to the
// client (if they are configured).
static const uint16_t required_options[] = {
- DHO_SUBNET_MASK,
DHO_ROUTERS,
DHO_DOMAIN_NAME_SERVERS,
DHO_DOMAIN_NAME };
@@ -453,6 +734,265 @@ Dhcpv4Srv::appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg) {
}
void
+Dhcpv4Srv::processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer) {
+ // It is possible that client has sent both Client FQDN and Hostname
+ // option. In such case, server should prefer Client FQDN option and
+ // ignore the Hostname option.
+ try {
+ Option4ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<Option4ClientFqdn>
+ (query->getOption(DHO_FQDN));
+ if (fqdn) {
+ processClientFqdnOption(fqdn, answer);
+
+ } else {
+ OptionCustomPtr hostname = boost::dynamic_pointer_cast<OptionCustom>
+ (query->getOption(DHO_HOST_NAME));
+ if (hostname) {
+ processHostnameOption(hostname, answer);
+ }
+
+ }
+ } catch (const Exception& ex) {
+ // In some rare cases it is possible that the client's name processing
+ // fails. For example, the Hostname option may be malformed, or there
+ // may be an error in the server's logic which would cause multiple
+ // attempts to add the same option to the response message. This
+ // error message aggregates all these errors so they can be diagnosed
+ // from the log. We don't want to throw an exception here because,
+ // it will impact the processing of the whole packet. We rather want
+ // the processing to continue, even if the client's name is wrong.
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_CLIENT_NAME_PROC_FAIL)
+ .arg(ex.what());
+ }
+}
+
+void
+Dhcpv4Srv::processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
+ Pkt4Ptr& answer) {
+ // Create the DHCPv4 Client FQDN Option to be included in the server's
+ // response to a client.
+ Option4ClientFqdnPtr fqdn_resp(new Option4ClientFqdn(*fqdn));
+
+ // RFC4702, section 4 - set 'NOS' flags to 0.
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_S, 0);
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_O, 0);
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_N, 0);
+
+ // Conditions when N flag has to be set to indicate that server will not
+ // perform DNS updates:
+ // 1. Updates are globally disabled,
+ // 2. Client requested no update and server respects it,
+ // 3. Client requested that the forward DNS update is delegated to the
+ // client but server neither respects requests for forward update
+ // delegation nor it is configured to send update on its own when
+ // client requested delegation.
+ if (!FQDN_ENABLE_UPDATE ||
+ (fqdn->getFlag(Option4ClientFqdn::FLAG_N) &&
+ !FQDN_OVERRIDE_NO_UPDATE) ||
+ (!fqdn->getFlag(Option4ClientFqdn::FLAG_S) &&
+ !FQDN_ALLOW_CLIENT_UPDATE && !FQDN_OVERRIDE_CLIENT_UPDATE)) {
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_N, true);
+
+ // Conditions when S flag is set to indicate that server will perform DNS
+ // update on its own:
+ // 1. Client requested that server performs DNS update and DNS updates are
+ // globally enabled.
+ // 2. Client requested that server delegates forward update to the client
+ // but server doesn't respect requests for delegation and it is
+ // configured to perform an update on its own when client requested the
+ // delegation.
+ } else if (fqdn->getFlag(Option4ClientFqdn::FLAG_S) ||
+ (!fqdn->getFlag(Option4ClientFqdn::FLAG_S) &&
+ !FQDN_ALLOW_CLIENT_UPDATE && FQDN_OVERRIDE_CLIENT_UPDATE)) {
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_S, true);
+ }
+
+ // Server MUST set the O flag if it has overriden the client's setting
+ // of S flag.
+ if (fqdn->getFlag(Option4ClientFqdn::FLAG_S) !=
+ fqdn_resp->getFlag(Option4ClientFqdn::FLAG_S)) {
+ fqdn_resp->setFlag(Option4ClientFqdn::FLAG_O, true);
+ }
+
+ // If client suppled partial or empty domain-name, server should generate
+ // one.
+ if (fqdn->getDomainNameType() == Option4ClientFqdn::PARTIAL) {
+ std::ostringstream name;
+ if (fqdn->getDomainName().empty() || FQDN_REPLACE_CLIENT_NAME) {
+ fqdn_resp->setDomainName("", Option4ClientFqdn::PARTIAL);
+
+ } else {
+ name << fqdn->getDomainName();
+ name << "." << FQDN_PARTIAL_SUFFIX;
+ fqdn_resp->setDomainName(name.str(), Option4ClientFqdn::FULL);
+
+ }
+
+ // Server may be configured to replace a name supplied by a client, even if
+ // client supplied fully qualified domain-name. The empty domain-name is
+ // is set to indicate that the name must be generated when the new lease
+ // is acquired.
+ } else if(FQDN_REPLACE_CLIENT_NAME) {
+ fqdn_resp->setDomainName("", Option4ClientFqdn::PARTIAL);
+ }
+
+ // Add FQDN option to the response message. Note that, there may be some
+ // cases when server may choose not to include the FQDN option in a
+ // response to a client. In such cases, the FQDN should be removed from the
+ // outgoing message. In theory we could cease to include the FQDN option
+ // in this function until it is confirmed that it should be included.
+ // However, we include it here for simplicity. Functions used to acquire
+ // lease for a client will scan the response message for FQDN and if it
+ // is found they will take necessary actions to store the FQDN information
+ // in the lease database as well as to generate NameChangeRequests to DNS.
+ // If we don't store the option in the reponse message, we will have to
+ // propagate it in the different way to the functions which acquire the
+ // lease. This would require modifications to the API of this class.
+ answer->addOption(fqdn_resp);
+}
+
+void
+Dhcpv4Srv::processHostnameOption(const OptionCustomPtr& opt_hostname,
+ Pkt4Ptr& answer) {
+ // Do nothing if the DNS updates are disabled.
+ if (!FQDN_ENABLE_UPDATE) {
+ return;
+ }
+
+ std::string hostname = isc::util::str::trim(opt_hostname->readString());
+ unsigned int label_count = OptionDataTypeUtil::getLabelCount(hostname);
+ // The hostname option sent by the client should be at least 1 octet long.
+ // If it isn't we ignore this option.
+ if (label_count == 0) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_EMPTY_HOSTNAME);
+ return;
+ }
+ // Copy construct the hostname provided by the client. It is entirely
+ // possible that we will use the hostname option provided by the client
+ // to perform the DNS update and we will send the same option to him to
+ // indicate that we accepted this hostname.
+ OptionCustomPtr opt_hostname_resp(new OptionCustom(*opt_hostname));
+
+ // The hostname option may be unqualified or fully qualified. The lab_count
+ // holds the number of labels for the name. The number of 1 means that
+ // there is only root label "." (even for unqualified names, as the
+ // getLabelCount function treats each name as a fully qualified one).
+ // By checking the number of labels present in the hostname we may infer
+ // whether client has sent the fully qualified or unqualified hostname.
+
+ // @todo We may want to reconsider whether it is appropriate for the
+ // client to send a root domain name as a Hostname. There are
+ // also extensions to the auto generation of the client's name,
+ // e.g. conversion to the puny code which may be considered at some point.
+ // For now, we just remain liberal and expect that the DNS will handle
+ // conversion if needed and possible.
+ if (FQDN_REPLACE_CLIENT_NAME || (label_count < 2)) {
+ opt_hostname_resp->writeString("");
+ // If there are two labels, it means that the client has specified
+ // the unqualified name. We have to concatenate the unqalified name
+ // with the domain name.
+ } else if (label_count == 2) {
+ std::ostringstream resp_hostname;
+ resp_hostname << hostname << "." << FQDN_PARTIAL_SUFFIX << ".";
+ opt_hostname_resp->writeString(resp_hostname.str());
+ }
+
+ answer->addOption(opt_hostname_resp);
+}
+
+void
+Dhcpv4Srv::createNameChangeRequests(const Lease4Ptr& lease,
+ const Lease4Ptr& old_lease) {
+ if (!lease) {
+ isc_throw(isc::Unexpected,
+ "NULL lease specified when creating NameChangeRequest");
+ }
+
+ // If old lease is not NULL, it is an indication that the lease has
+ // just been renewed. In such case we may need to generate the
+ // additional NameChangeRequest to remove an existing entry before
+ // we create a NameChangeRequest to add the entry for an updated lease.
+ // We may also decide not to generate any requests at all. This is when
+ // we discover that nothing has changed in the client's FQDN data.
+ if (old_lease) {
+ if (!lease->matches(*old_lease)) {
+ isc_throw(isc::Unexpected,
+ "there is no match between the current instance of the"
+ " lease: " << lease->toText() << ", and the previous"
+ " instance: " << lease->toText());
+ } else {
+ // There will be a NameChangeRequest generated to remove existing
+ // DNS entries if the following conditions are met:
+ // - The hostname is set for the existing lease, we can't generate
+ // removal request for non-existent hostname.
+ // - A server has performed reverse, forward or both updates.
+ // - FQDN data between the new and old lease do not match.
+ if ((lease->hostname_ != old_lease->hostname_) ||
+ (lease->fqdn_fwd_ != old_lease->fqdn_fwd_) ||
+ (lease->fqdn_rev_ != old_lease->fqdn_rev_)) {
+ queueNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE,
+ old_lease);
+
+ // If FQDN data from both leases match, there is no need to update.
+ } else if ((lease->hostname_ == old_lease->hostname_) &&
+ (lease->fqdn_fwd_ == old_lease->fqdn_fwd_) &&
+ (lease->fqdn_rev_ == old_lease->fqdn_rev_)) {
+ return;
+ }
+
+ }
+ }
+
+ // We may need to generate the NameChangeRequest for the new lease. It
+ // will be generated only if hostname is set and if forward or reverse
+ // update has been requested.
+ queueNameChangeRequest(isc::dhcp_ddns::CHG_ADD, lease);
+}
+
+void
+Dhcpv4Srv::
+queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
+ const Lease4Ptr& lease) {
+ // The hostname must not be empty, and at least one type of update
+ // should be requested.
+ if (!lease || lease->hostname_.empty() ||
+ (!lease->fqdn_rev_ && !lease->fqdn_fwd_)) {
+ return;
+ }
+
+ // Create the DHCID for the NameChangeRequest.
+ D2Dhcid dhcid;
+ try {
+ dhcid = computeDhcid(lease);
+ } catch (const DhcidComputeError& ex) {
+ LOG_ERROR(dhcp4_logger, DHCP4_DHCID_COMPUTE_ERROR)
+ .arg(lease->toText())
+ .arg(ex.what());
+ return;
+ }
+ // Create NameChangeRequest
+ NameChangeRequest ncr(chg_type, lease->fqdn_fwd_, lease->fqdn_rev_,
+ lease->hostname_, lease->addr_.toText(),
+ dhcid, lease->cltt_ + lease->valid_lft_,
+ lease->valid_lft_);
+ // And queue it.
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_QUEUE_NCR)
+ .arg(chg_type == CHG_ADD ? "add" : "remove")
+ .arg(ncr.toText());
+ name_change_reqs_.push(ncr);
+}
+
+void
+Dhcpv4Srv::sendNameChangeRequests() {
+ while (!name_change_reqs_.empty()) {
+ // @todo Once next NameChangeRequest is picked from the queue
+ // we should send it to the b10-dhcp_ddns module. Currently we
+ // just drop it.
+ name_change_reqs_.pop();
+ }
+}
+
+void
Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
// We need to select a subnet the client is connected in.
@@ -476,6 +1016,13 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
return;
}
+ // Set up siaddr. Perhaps assignLease is not the best place to call this
+ // as siaddr has nothing to do with a lease, but otherwise we would have
+ // to select subnet twice (performance hit) or update too many functions
+ // at once.
+ // @todo: move subnet selection to a common code
+ answer->setSiaddr(subnet->getSiaddr());
+
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL_DATA, DHCP4_SUBNET_SELECTED)
.arg(subnet->toText());
@@ -499,12 +1046,42 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
// allocation.
bool fake_allocation = (question->getType() == DHCPDISCOVER);
+ CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+ std::string hostname;
+ bool fqdn_fwd = false;
+ bool fqdn_rev = false;
+ OptionCustomPtr opt_hostname;
+ Option4ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
+ Option4ClientFqdn>(answer->getOption(DHO_FQDN));
+ if (fqdn) {
+ hostname = fqdn->getDomainName();
+ fqdn_fwd = fqdn->getFlag(Option4ClientFqdn::FLAG_S);
+ fqdn_rev = !fqdn->getFlag(Option4ClientFqdn::FLAG_N);
+ } else {
+ opt_hostname = boost::dynamic_pointer_cast<OptionCustom>
+ (answer->getOption(DHO_HOST_NAME));
+ if (opt_hostname) {
+ hostname = opt_hostname->readString();
+ // @todo It could be configurable what sort of updates the server
+ // is doing when Hostname option was sent.
+ fqdn_fwd = true;
+ fqdn_rev = true;
+ }
+ }
+
// Use allocation engine to pick a lease for this client. Allocation engine
// will try to honour the hint, but it is just a hint - some other address
// may be used instead. If fake_allocation is set to false, the lease will
// be inserted into the LeaseMgr as well.
- Lease4Ptr lease = alloc_engine_->allocateAddress4(subnet, client_id, hwaddr,
- hint, fake_allocation);
+ // @todo pass the actual FQDN data.
+ Lease4Ptr old_lease;
+ Lease4Ptr lease = alloc_engine_->allocateLease4(subnet, client_id, hwaddr,
+ hint, fqdn_fwd, fqdn_rev,
+ hostname,
+ fake_allocation,
+ callout_handle,
+ old_lease);
if (lease) {
// We have a lease! Let's set it in the packet and send it back to
@@ -517,25 +1094,39 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
answer->setYiaddr(lease->addr_);
- // If remote address is not set, we are dealing with a directly
- // connected client requesting new lease. We can send response to
- // the address assigned in the lease, but first we have to make sure
- // that IfaceMgr supports responding directly to the client when
- // client doesn't have address assigned to its interface yet.
- if (answer->getRemoteAddr().toText() == "0.0.0.0") {
- if (IfaceMgr::instance().isDirectResponseSupported()) {
- answer->setRemoteAddr(lease->addr_);
- } else {
- // Since IfaceMgr does not support direct responses to
- // clients not having IP addresses, we have to send response
- // to broadcast. We don't check whether the use_bcast flag
- // was set in the constructor, because this flag is only used
- // by unit tests to prevent opening broadcast sockets, as
- // it requires root privileges. If this function is invoked by
- // unit tests, we expect that it sets broadcast address if
- // direct response is not supported, so as a test can verify
- // function's behavior, regardless of the use_bcast flag's value.
- answer->setRemoteAddr(IOAddress("255.255.255.255"));
+ // If there has been Client FQDN or Hostname option sent, but the
+ // hostname is empty, it means that server is responsible for
+ // generating the entire hostname for the client. The example of the
+ // client's name, generated from the IP address is: host-192-0-2-3.
+ if ((fqdn || opt_hostname) && lease->hostname_.empty()) {
+ hostname = lease->addr_.toText();
+ // Replace dots with hyphens.
+ std::replace(hostname.begin(), hostname.end(), '.', '-');
+ ostringstream stream;
+ // The partial suffix will need to be replaced with the actual
+ // domain-name for the client when configuration is implemented.
+ stream << "host-" << hostname << "." << FQDN_PARTIAL_SUFFIX << ".";
+ lease->hostname_ = stream.str();
+ // The operations below are rather safe, but we want to catch
+ // any potential exceptions (e.g. invalid lease database backend
+ // implementation) and log an error.
+ try {
+ // The lease update should be safe, because the lease should
+ // be already in the database. In most cases the exception
+ // would be thrown if the lease was missing.
+ LeaseMgrFactory::instance().updateLease4(lease);
+ // The name update in the option should be also safe,
+ // because the generated name is well formed.
+ if (fqdn) {
+ fqdn->setDomainName(lease->hostname_,
+ Option4ClientFqdn::FULL);
+ } else if (opt_hostname) {
+ opt_hostname->writeString(lease->hostname_);
+ }
+
+ } catch (const Exception& ex) {
+ LOG_ERROR(dhcp4_logger, DHCP4_NAME_GEN_UPDATE_FAIL)
+ .arg(ex.what());
}
}
@@ -544,19 +1135,27 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
opt->setUint32(lease->valid_lft_);
answer->addOption(opt);
- // Router (type 3)
- Subnet::OptionDescriptor opt_routers =
- subnet->getOptionDescriptor("dhcp4", DHO_ROUTERS);
- if (opt_routers.option) {
- answer->addOption(opt_routers.option);
- }
-
// Subnet mask (type 1)
answer->addOption(getNetmaskOption(subnet));
// @todo: send renew timer option (T1, option 58)
// @todo: send rebind timer option (T2, option 59)
+ // @todo Currently the NameChangeRequests are always generated if
+ // real (not fake) allocation is being performed. Should we have
+ // control switch to enable/disable NameChangeRequest creation?
+ // Perhaps we need a way to detect whether the b10-dhcp-ddns module
+ // is up an running?
+ if (!fake_allocation) {
+ try {
+ createNameChangeRequests(lease, old_lease);
+ } catch (const Exception& ex) {
+ LOG_ERROR(dhcp4_logger, DHCP4_NCR_CREATION_FAILED)
+ .arg(ex.what());
+ }
+
+ }
+
} else {
// Allocation engine did not allocate a lease. The engine logged
// cause of that failure. The only thing left is to insert
@@ -570,9 +1169,95 @@ Dhcpv4Srv::assignLease(const Pkt4Ptr& question, Pkt4Ptr& answer) {
answer->setType(DHCPNAK);
answer->setYiaddr(IOAddress("0.0.0.0"));
+
+ answer->delOption(DHO_FQDN);
+ answer->delOption(DHO_HOST_NAME);
}
}
+void
+Dhcpv4Srv::adjustIfaceData(const Pkt4Ptr& query, const Pkt4Ptr& response) {
+ adjustRemoteAddr(query, response);
+
+ // For the non-relayed message, the destination port is the client's port.
+ // For the relayed message, the server/relay port is a destination.
+ // Note that the call to this function may throw if invalid combination
+ // of hops and giaddr is found (hops = 0 if giaddr = 0 and hops != 0 if
+ // giaddr != 0). The exception will propagate down and eventually cause the
+ // packet to be discarded.
+ response->setRemotePort(query->isRelayed() ? DHCP4_SERVER_PORT :
+ DHCP4_CLIENT_PORT);
+
+ // In many cases the query is sent to a broadcast address. This address
+ // appears as a local address in the query message. Therefore we can't
+ // simply copy local address from the query and use it as a source
+ // address for the response. Instead, we have to check what address our
+ // socket is bound to and use it as a source address. This operation
+ // may throw if for some reason the socket is closed.
+ // @todo Consider an optimization that we use local address from
+ // the query if this address is not broadcast.
+ SocketInfo sock_info = IfaceMgr::instance().getSocket(*query);
+ // Set local adddress, port and interface.
+ response->setLocalAddr(sock_info.addr_);
+ response->setLocalPort(DHCP4_SERVER_PORT);
+ response->setIface(query->getIface());
+ response->setIndex(query->getIndex());
+}
+
+void
+Dhcpv4Srv::adjustRemoteAddr(const Pkt4Ptr& question, const Pkt4Ptr& response) {
+ // Let's create static objects representing zeroed and broadcast
+ // addresses. We will use them further in this function to test
+ // other addresses against them. Since they are static, they will
+ // be created only once.
+ static const IOAddress zero_addr("0.0.0.0");
+ static const IOAddress bcast_addr("255.255.255.255");
+
+ // If received relayed message, server responds to the relay address.
+ if (question->isRelayed()) {
+ response->setRemoteAddr(question->getGiaddr());
+
+ // If giaddr is 0 but client set ciaddr, server should unicast the
+ // response to ciaddr.
+ } else if (question->getCiaddr() != zero_addr) {
+ response->setRemoteAddr(question->getCiaddr());
+
+ // We can't unicast the response to the client when sending NAK,
+ // because we haven't allocated address for him. Therefore,
+ // NAK is broadcast.
+ } else if (response->getType() == DHCPNAK) {
+ response->setRemoteAddr(bcast_addr);
+
+ // If yiaddr is set it means that we have created a lease for a client.
+ } else if (response->getYiaddr() != zero_addr) {
+ // If the broadcast bit is set in the flags field, we have to
+ // send the response to broadcast address. Client may have requested it
+ // because it doesn't support reception of messages on the interface
+ // which doesn't have an address assigned. The other case when response
+ // must be broadcasted is when our server does not support responding
+ // directly to a client without address assigned.
+ const bool bcast_flag = ((question->getFlags() & Pkt4::FLAG_BROADCAST_MASK) != 0);
+ if (!IfaceMgr::instance().isDirectResponseSupported() || bcast_flag) {
+ response->setRemoteAddr(bcast_addr);
+
+ // Client cleared the broadcast bit and we support direct responses
+ // so we should unicast the response to a newly allocated address -
+ // yiaddr.
+ } else {
+ response->setRemoteAddr(response ->getYiaddr());
+
+ }
+
+ // In most cases, we should have the remote address found already. If we
+ // found ourselves at this point, the rational thing to do is to respond
+ // to the address we got the query from.
+ } else {
+ response->setRemoteAddr(question->getRemoteAddr());
+
+ }
+}
+
+
OptionPtr
Dhcpv4Srv::getNetmaskOption(const Subnet4Ptr& subnet) {
uint32_t netmask = getNetmask4(subnet->get().second);
@@ -585,38 +1270,80 @@ Dhcpv4Srv::getNetmaskOption(const Subnet4Ptr& subnet) {
Pkt4Ptr
Dhcpv4Srv::processDiscover(Pkt4Ptr& discover) {
+
+ sanityCheck(discover, FORBIDDEN);
+
Pkt4Ptr offer = Pkt4Ptr
(new Pkt4(DHCPOFFER, discover->getTransid()));
copyDefaultFields(discover, offer);
appendDefaultOptions(offer, DHCPOFFER);
- appendRequestedOptions(discover, offer);
+
+ // If DISCOVER message contains the FQDN or Hostname option, server
+ // may respond to the client with the appropriate FQDN or Hostname
+ // option to indicate that whether it will take responsibility for
+ // updating DNS when the client sends REQUEST message.
+ processClientName(discover, offer);
assignLease(discover, offer);
- // There are a few basic options that we always want to
- // include in the response. If client did not request
- // them we append them for him.
- appendBasicOptions(discover, offer);
+ // Adding any other options makes sense only when we got the lease.
+ if (offer->getYiaddr() != IOAddress("0.0.0.0")) {
+ appendRequestedOptions(discover, offer);
+ appendRequestedVendorOptions(discover, offer);
+ // There are a few basic options that we always want to
+ // include in the response. If client did not request
+ // them we append them for him.
+ appendBasicOptions(discover, offer);
+ }
+
+ // Set the src/dest IP address, port and interface for the outgoing
+ // packet.
+ adjustIfaceData(discover, offer);
+
+ appendServerID(offer);
return (offer);
}
Pkt4Ptr
Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
+
+ /// @todo Uncomment this (see ticket #3116)
+ // sanityCheck(request, MANDATORY);
+
Pkt4Ptr ack = Pkt4Ptr
(new Pkt4(DHCPACK, request->getTransid()));
copyDefaultFields(request, ack);
appendDefaultOptions(ack, DHCPACK);
- appendRequestedOptions(request, ack);
+ // If REQUEST message contains the FQDN or Hostname option, server
+ // should respond to the client with the appropriate FQDN or Hostname
+ // option to indicate if it takes responsibility for the DNS updates.
+ // This is performed by the function below.
+ processClientName(request, ack);
+
+ // Note that we treat REQUEST message uniformly, regardless if this is a
+ // first request (requesting for new address), renewing existing address
+ // or even rebinding.
assignLease(request, ack);
- // There are a few basic options that we always want to
- // include in the response. If client did not request
- // them we append them for him.
- appendBasicOptions(request, ack);
+ // Adding any other options makes sense only when we got the lease.
+ if (ack->getYiaddr() != IOAddress("0.0.0.0")) {
+ appendRequestedOptions(request, ack);
+ appendRequestedVendorOptions(request, ack);
+ // There are a few basic options that we always want to
+ // include in the response. If client did not request
+ // them we append them for him.
+ appendBasicOptions(request, ack);
+ }
+
+ // Set the src/dest IP address, port and interface for the outgoing
+ // packet.
+ adjustIfaceData(request, ack);
+
+ appendServerID(ack);
return (ack);
}
@@ -624,6 +1351,9 @@ Dhcpv4Srv::processRequest(Pkt4Ptr& request) {
void
Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
+ /// @todo Uncomment this (see ticket #3116)
+ // sanityCheck(release, MANDATORY);
+
// Try to find client-id
ClientIdPtr client_id;
OptionPtr opt = release->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
@@ -633,12 +1363,12 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
try {
// Do we have a lease for that particular address?
- Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(release->getYiaddr());
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(release->getCiaddr());
if (!lease) {
// No such lease - bogus release
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_NO_LEASE)
- .arg(release->getYiaddr().toText())
+ .arg(release->getCiaddr().toText())
.arg(release->getHWAddr()->toText())
.arg(client_id ? client_id->toText() : "(no client-id)");
return;
@@ -649,7 +1379,7 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
if (lease->hwaddr_ != release->getHWAddr()->hwaddr_) {
// @todo: Print hwaddr from lease as part of ticket #2589
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_WRONG_HWADDR)
- .arg(release->getYiaddr().toText())
+ .arg(release->getCiaddr().toText())
.arg(client_id ? client_id->toText() : "(no client-id)")
.arg(release->getHWAddr()->toText());
return;
@@ -659,27 +1389,62 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
// the client sent us.
if (lease->client_id_ && client_id && *lease->client_id_ != *client_id) {
LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE_FAIL_WRONG_CLIENT_ID)
- .arg(release->getYiaddr().toText())
+ .arg(release->getCiaddr().toText())
.arg(client_id->toText())
.arg(lease->client_id_->toText());
return;
}
+ bool skip = false;
+
+ // Execute all callouts registered for lease4_release
+ if (HooksManager::calloutsPresent(Hooks.hook_index_lease4_release_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(release);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass the original packet
+ callout_handle->setArgument("query4", release);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease4", lease);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease4_release_,
+ *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to send the packet, so skip at this
+ // stage means "drop response".
+ if (callout_handle->getSkip()) {
+ skip = true;
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS,
+ DHCP4_HOOK_LEASE4_RELEASE_SKIP);
+ }
+ }
+
// Ok, hw and client-id match - let's release the lease.
- if (LeaseMgrFactory::instance().deleteLease(lease->addr_)) {
+ if (!skip) {
+ bool success = LeaseMgrFactory::instance().deleteLease(lease->addr_);
- // Release successful - we're done here
- LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE)
- .arg(lease->addr_.toText())
- .arg(client_id ? client_id->toText() : "(no client-id)")
- .arg(release->getHWAddr()->toText());
- } else {
+ if (success) {
+ // Release successful
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_RELEASE)
+ .arg(lease->addr_.toText())
+ .arg(client_id ? client_id->toText() : "(no client-id)")
+ .arg(release->getHWAddr()->toText());
- // Release failed -
- LOG_ERROR(dhcp4_logger, DHCP4_RELEASE_FAIL)
- .arg(lease->addr_.toText())
+ // Remove existing DNS entries for the lease, if any.
+ queueNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, lease);
+
+ } else {
+ // Release failed -
+ LOG_ERROR(dhcp4_logger, DHCP4_RELEASE_FAIL)
+ .arg(lease->addr_.toText())
.arg(client_id ? client_id->toText() : "(no client-id)")
- .arg(release->getHWAddr()->toText());
+ .arg(release->getHWAddr()->toText());
+ }
}
} catch (const isc::Exception& ex) {
// Rethrow the exception with a bit more data.
@@ -692,12 +1457,13 @@ Dhcpv4Srv::processRelease(Pkt4Ptr& release) {
void
Dhcpv4Srv::processDecline(Pkt4Ptr& /* decline */) {
- /// TODO: Implement this.
+ /// @todo Implement this (also see ticket #3116)
}
Pkt4Ptr
Dhcpv4Srv::processInform(Pkt4Ptr& inform) {
- /// TODO: Currently implemented echo mode. Implement this for real
+
+ /// @todo Implement this for real. (also see ticket #3116)
return (inform);
}
@@ -735,17 +1501,104 @@ Dhcpv4Srv::serverReceivedPacketName(uint8_t type) {
Subnet4Ptr
Dhcpv4Srv::selectSubnet(const Pkt4Ptr& question) {
+ Subnet4Ptr subnet;
// Is this relayed message?
IOAddress relay = question->getGiaddr();
- if (relay.toText() == "0.0.0.0") {
+ static const IOAddress notset("0.0.0.0");
+ if (relay != notset) {
// Yes: Use relay address to select subnet
- return (CfgMgr::instance().getSubnet4(relay));
+ subnet = CfgMgr::instance().getSubnet4(relay);
} else {
// No: Use client's address to select subnet
- return (CfgMgr::instance().getSubnet4(question->getRemoteAddr()));
+ subnet = CfgMgr::instance().getSubnet4(question->getRemoteAddr());
}
+
+ /// @todo Implement getSubnet4(interface-name)
+
+ // Let's execute all callouts registered for subnet4_select
+ if (HooksManager::calloutsPresent(hook_index_subnet4_select_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+ // We're reusing callout_handle from previous calls
+ callout_handle->deleteAllArguments();
+
+ // Set new arguments
+ callout_handle->setArgument("query4", question);
+ callout_handle->setArgument("subnet4", subnet);
+ callout_handle->setArgument("subnet4collection",
+ CfgMgr::instance().getSubnets4());
+
+ // Call user (and server-side) callouts
+ HooksManager::callCallouts(hook_index_subnet4_select_,
+ *callout_handle);
+
+ // Callouts decided to skip this step. This means that no subnet will be
+ // selected. Packet processing will continue, but it will be severly
+ // limited (i.e. only global options will be assigned)
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_HOOKS,
+ DHCP4_HOOK_SUBNET4_SELECT_SKIP);
+ return (Subnet4Ptr());
+ }
+
+ // Use whatever subnet was specified by the callout
+ callout_handle->getArgument("subnet4", subnet);
+ }
+
+ return (subnet);
+}
+
+bool
+Dhcpv4Srv::acceptServerId(const Pkt4Ptr& pkt) const {
+ // This function is meant to be called internally by the server class, so
+ // we rely on the caller to sanity check the pointer and we don't check
+ // it here.
+
+ // Check if server identifier option is present. If it is not present
+ // we accept the message because it is targetted to all servers.
+ // Note that we don't check cases that server identifier is mandatory
+ // but not present. This is meant to be sanity checked in other
+ // functions.
+ OptionPtr option = pkt->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ if (!option) {
+ return (true);
+ }
+ // Server identifier is present. Let's convert it to 4-byte address
+ // and try to match with server identifiers used by the server.
+ OptionCustomPtr option_custom =
+ boost::dynamic_pointer_cast<OptionCustom>(option);
+ // Unable to convert the option to the option type which encapsulates it.
+ // We treat this as non-matching server id.
+ if (!option_custom) {
+ return (false);
+ }
+ // The server identifier option should carry exactly one IPv4 address.
+ // If the option definition for the server identifier doesn't change,
+ // the OptionCustom object should have exactly one IPv4 address and
+ // this check is somewhat redundant. On the other hand, if someone
+ // breaks option it may be better to check that here.
+ if (option_custom->getDataFieldsNum() != 1) {
+ return (false);
+ }
+
+ // The server identifier MUST be an IPv4 address. If given address is
+ // v6, it is wrong.
+ IOAddress server_id = option_custom->readAddress();
+ if (!server_id.isV4()) {
+ return (false);
+ }
+
+ // This function iterates over all interfaces on which the
+ // server is listening to find the one which has a socket bound
+ // to the address carried in the server identifier option.
+ // This has some performance implications. However, given that
+ // typically there will be just a few active interfaces the
+ // performance hit should be acceptable. If it turns out to
+ // be significant, we will have to cache server identifiers
+ // when sockets are opened.
+ return (IfaceMgr::instance().hasOpenSocket(server_id));
}
void
@@ -771,6 +1624,243 @@ Dhcpv4Srv::sanityCheck(const Pkt4Ptr& pkt, RequirementLevel serverid) {
// do nothing here
;
}
+
+ // If there is HWAddress set and it is non-empty, then we're good
+ if (pkt->getHWAddr() && !pkt->getHWAddr()->hwaddr_.empty()) {
+ return;
+ }
+
+ // There has to be something to uniquely identify the client:
+ // either non-zero MAC address or client-id option present (or both)
+ OptionPtr client_id = pkt->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+ // If there's no client-id (or a useless one is provided, i.e. 0 length)
+ if (!client_id || client_id->len() == client_id->getHeaderLen()) {
+ isc_throw(RFCViolation, "Missing or useless client-id and no HW address "
+ " provided in message "
+ << serverReceivedPacketName(pkt->getType()));
+ }
+}
+
+void
+Dhcpv4Srv::openActiveSockets(const uint16_t port,
+ const bool use_bcast) {
+ IfaceMgr::instance().closeSockets();
+
+ // Get the reference to the collection of interfaces. This reference should
+ // be valid as long as the program is run because IfaceMgr is a singleton.
+ // Therefore we can safely iterate over instances of all interfaces and
+ // modify their flags. Here we modify flags which indicate whether socket
+ // should be open for a particular interface or not.
+ const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+ for (IfaceMgr::IfaceCollection::const_iterator iface = ifaces.begin();
+ iface != ifaces.end(); ++iface) {
+ Iface* iface_ptr = IfaceMgr::instance().getIface(iface->getName());
+ if (iface_ptr == NULL) {
+ isc_throw(isc::Unexpected, "Interface Manager returned NULL"
+ << " instance of the interface when DHCPv4 server was"
+ << " trying to reopen sockets after reconfiguration");
+ }
+ if (CfgMgr::instance().isActiveIface(iface->getName())) {
+ iface_ptr->inactive4_ = false;
+ LOG_INFO(dhcp4_logger, DHCP4_ACTIVATE_INTERFACE)
+ .arg(iface->getFullName());
+
+ } else {
+ // For deactivating interface, it should be sufficient to log it
+ // on the debug level because it is more useful to know what
+ // interface is activated which is logged on the info level.
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC,
+ DHCP4_DEACTIVATE_INTERFACE).arg(iface->getName());
+ iface_ptr->inactive4_ = true;
+
+ }
+ }
+ // Let's reopen active sockets. openSockets4 will check internally whether
+ // sockets are marked active or inactive.
+ // @todo Optimization: we should not reopen all sockets but rather select
+ // those that have been affected by the new configuration.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&Dhcpv4Srv::ifaceMgrSocket4ErrorHandler, _1);
+ if (!IfaceMgr::instance().openSockets4(port, use_bcast, error_handler)) {
+ LOG_WARN(dhcp4_logger, DHCP4_NO_SOCKETS_OPEN);
+ }
+}
+
+size_t
+Dhcpv4Srv::unpackOptions(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options) {
+ size_t offset = 0;
+
+ OptionDefContainer option_defs;
+ if (option_space == "dhcp4") {
+ // Get the list of stdandard option definitions.
+ option_defs = LibDHCP::getOptionDefs(Option::V4);
+ } else if (!option_space.empty()) {
+ OptionDefContainerPtr option_defs_ptr =
+ CfgMgr::instance().getOptionDefs(option_space);
+ if (option_defs_ptr != NULL) {
+ option_defs = *option_defs_ptr;
+ }
+ }
+ // Get the search index #1. It allows to search for option definitions
+ // using option code.
+ const OptionDefContainerTypeIndex& idx = option_defs.get<1>();
+
+ // The buffer being read comprises a set of options, each starting with
+ // a one-byte type code and a one-byte length field.
+ while (offset + 1 <= buf.size()) {
+ uint8_t opt_type = buf[offset++];
+
+ // DHO_END is a special, one octet long option
+ if (opt_type == DHO_END)
+ return (offset); // just return. Don't need to add DHO_END option
+
+ // DHO_PAD is just a padding after DHO_END. Let's continue parsing
+ // in case we receive a message without DHO_END.
+ if (opt_type == DHO_PAD)
+ continue;
+
+ if (offset + 1 >= buf.size()) {
+ // opt_type must be cast to integer so as it is not treated as
+ // unsigned char value (a number is presented in error message).
+ isc_throw(OutOfRange, "Attempt to parse truncated option "
+ << static_cast<int>(opt_type));
+ }
+
+ uint8_t opt_len = buf[offset++];
+ if (offset + opt_len > buf.size()) {
+ isc_throw(OutOfRange, "Option parse failed. Tried to parse "
+ << offset + opt_len << " bytes from " << buf.size()
+ << "-byte long buffer.");
+ }
+
+ // Get all definitions with the particular option code. Note that option code
+ // is non-unique within this container however at this point we expect
+ // to get one option definition with the particular code. If more are
+ // returned we report an error.
+ const OptionDefContainerTypeRange& range = idx.equal_range(opt_type);
+ // Get the number of returned option definitions for the option code.
+ size_t num_defs = distance(range.first, range.second);
+
+ OptionPtr opt;
+ if (num_defs > 1) {
+ // Multiple options of the same code are not supported right now!
+ isc_throw(isc::Unexpected, "Internal error: multiple option definitions"
+ " for option type " << static_cast<int>(opt_type)
+ << " returned. Currently it is not supported to initialize"
+ << " multiple option definitions for the same option code."
+ << " This will be supported once support for option spaces"
+ << " is implemented");
+ } else if (num_defs == 0) {
+ opt = OptionPtr(new Option(Option::V4, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len));
+ opt->setEncapsulatedSpace("dhcp4");
+ } else {
+ // The option definition has been found. Use it to create
+ // the option instance from the provided buffer chunk.
+ const OptionDefinitionPtr& def = *(range.first);
+ assert(def);
+ opt = def->optionFactory(Option::V4, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len,
+ boost::bind(&Dhcpv4Srv::unpackOptions,
+ this, _1, _2, _3));
+ }
+
+ options.insert(std::make_pair(opt_type, opt));
+ offset += opt_len;
+ }
+ return (offset);
+}
+
+void
+Dhcpv4Srv::ifaceMgrSocket4ErrorHandler(const std::string& errmsg) {
+ // Log the reason for socket opening failure and return.
+ LOG_WARN(dhcp4_logger, DHCP4_OPEN_SOCKET_FAIL).arg(errmsg);
+}
+
+void Dhcpv4Srv::classifyPacket(const Pkt4Ptr& pkt) {
+ boost::shared_ptr<OptionString> vendor_class =
+ boost::dynamic_pointer_cast<OptionString>(pkt->getOption(DHO_VENDOR_CLASS_IDENTIFIER));
+
+ string classes = "";
+
+ if (!vendor_class) {
+ return;
+ }
+
+ // DOCSIS specific section
+
+ // Let's keep this as a series of checks. So far we're supporting only
+ // docsis3.0, but there are also docsis2.0, docsis1.1 and docsis1.0. We
+ // may come up with adding several classes, e.g. for docsis2.0 we would
+ // add classes docsis2.0, docsis1.1 and docsis1.0.
+
+ // Also we are using find, because we have at least one traffic capture
+ // where the user class was followed by a space ("docsis3.0 ").
+
+ // For now, the code is very simple, but it is expected to get much more
+ // complex soon. One specific case is that the vendor class is an option
+ // sent by the client, so we should not trust it. To confirm that the device
+ // is indeed a modem, John B. suggested to check whether chaddr field
+ // quals subscriber-id option that was inserted by the relay (CMTS).
+ // This kind of logic will appear here soon.
+ if (vendor_class->getValue().find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
+ pkt->addClass(DOCSIS3_CLASS_MODEM);
+ classes += string(DOCSIS3_CLASS_MODEM) + " ";
+ } else
+ if (vendor_class->getValue().find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
+ pkt->addClass(DOCSIS3_CLASS_EROUTER);
+ classes += string(DOCSIS3_CLASS_EROUTER) + " ";
+ } else {
+ classes += vendor_class->getValue();
+ pkt->addClass(vendor_class->getValue());
+ }
+
+ if (!classes.empty()) {
+ LOG_DEBUG(dhcp4_logger, DBG_DHCP4_BASIC, DHCP4_CLASS_ASSIGNED)
+ .arg(classes);
+ }
+}
+
+bool Dhcpv4Srv::classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp) {
+
+ Subnet4Ptr subnet = selectSubnet(query);
+ if (!subnet) {
+ return (true);
+ }
+
+ if (query->inClass(DOCSIS3_CLASS_MODEM)) {
+
+ // Set next-server. This is TFTP server address. Cable modems will
+ // download their configuration from that server.
+ rsp->setSiaddr(subnet->getSiaddr());
+
+ // Now try to set up file field in DHCPv4 packet. We will just copy
+ // content of the boot-file option, which contains the same information.
+ Subnet::OptionDescriptor desc =
+ subnet->getOptionDescriptor("dhcp4", DHO_BOOT_FILE_NAME);
+
+ if (desc.option) {
+ boost::shared_ptr<OptionString> boot =
+ boost::dynamic_pointer_cast<OptionString>(desc.option);
+ if (boot) {
+ std::string filename = boot->getValue();
+ rsp->setFile((const uint8_t*)filename.c_str(), filename.size());
+ }
+ }
+ }
+
+ if (query->inClass(DOCSIS3_CLASS_EROUTER)) {
+
+ // Do not set TFTP server address for eRouter devices.
+ rsp->setSiaddr(IOAddress("0.0.0.0"));
+ }
+
+ return (true);
}
} // namespace dhcp
diff --git a/src/bin/dhcp4/dhcp4_srv.h b/src/bin/dhcp4/dhcp4_srv.h
index bc8851e..ec63e7f 100644
--- a/src/bin/dhcp4/dhcp4_srv.h
+++ b/src/bin/dhcp4/dhcp4_srv.h
@@ -18,16 +18,28 @@
#include <dhcp/dhcp4.h>
#include <dhcp/pkt4.h>
#include <dhcp/option.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option_custom.h>
+#include <dhcp_ddns/ncr_msg.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/alloc_engine.h>
+#include <hooks/callout_handle.h>
#include <boost/noncopyable.hpp>
#include <iostream>
+#include <queue>
namespace isc {
namespace dhcp {
+/// @brief Exception thrown when DHCID computation failed.
+class DhcidComputeError : public isc::Exception {
+public:
+ DhcidComputeError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
/// @brief DHCPv4 server service.
///
/// This singleton class represents DHCPv4 server. It contains all
@@ -45,7 +57,7 @@ namespace dhcp {
/// Dhcpv4Srv and other classes, see \ref dhcpv4Session.
class Dhcpv4Srv : public boost::noncopyable {
- public:
+public:
/// @brief defines if certain option may, must or must not appear
typedef enum {
@@ -61,18 +73,25 @@ class Dhcpv4Srv : public boost::noncopyable {
/// network interaction. Will instantiate lease manager, and load
/// old or create new DUID. It is possible to specify alternate
/// port on which DHCPv4 server will listen on. That is mostly useful
- /// for testing purposes.
+ /// for testing purposes. The Last two arguments of the constructor
+ /// should be left at default values for normal server operation.
+ /// They should be set to 'false' when creating an instance of this
+ /// class for unit testing because features they enable require
+ /// root privileges.
///
/// @param port specifies port number to listen on
/// @param dbconfig Lease manager configuration string. The default
/// of the "memfile" manager is used for testing.
/// @param use_bcast configure sockets to support broadcast messages.
+ /// @param direct_response_desired specifies if it is desired to
+ /// use direct V4 traffic.
Dhcpv4Srv(uint16_t port = DHCP4_SERVER_PORT,
const char* dbconfig = "type=memfile",
- const bool use_bcast = true);
+ const bool use_bcast = true,
+ const bool direct_response_desired = true);
/// @brief Destructor. Used during DHCPv4 service shutdown.
- ~Dhcpv4Srv();
+ virtual ~Dhcpv4Srv();
/// @brief Main server processing loop.
///
@@ -106,8 +125,63 @@ class Dhcpv4Srv : public boost::noncopyable {
/// be freed by the caller.
static const char* serverReceivedPacketName(uint8_t type);
+ ///
+ /// @name Public accessors returning values required to (re)open sockets.
+ ///
+ /// These accessors must be public because sockets are reopened from the
+ /// static configuration callback handler. This callback handler invokes
+ /// @c ControlledDhcpv4Srv::openActiveSockets which requires parameters
+ /// which has to be retrieved from the @c ControlledDhcpv4Srv object.
+ /// They are retrieved using these public functions
+ //@{
+ ///
+ /// @brief Get UDP port on which server should listen.
+ ///
+ /// Typically, server listens on UDP port number 67. Other ports are used
+ /// for testing purposes only.
+ ///
+ /// @return UDP port on which server should listen.
+ uint16_t getPort() const {
+ return (port_);
+ }
+
+ /// @brief Return bool value indicating that broadcast flags should be set
+ /// on sockets.
+ ///
+ /// @return A bool value indicating that broadcast should be used (if true).
+ bool useBroadcast() const {
+ return (use_bcast_);
+ }
+ //@}
+
+ /// @brief Open sockets which are marked as active in @c CfgMgr.
+ ///
+ /// This function reopens sockets according to the current settings in the
+ /// Configuration Manager. It holds the list of the interfaces which server
+ /// should listen on. This function will open sockets on these interfaces
+ /// only. This function is not exception safe.
+ ///
+ /// @param port UDP port on which server should listen.
+ /// @param use_bcast should broadcast flags be set on the sockets.
+ static void openActiveSockets(const uint16_t port, const bool use_bcast);
+
protected:
+ /// @brief Verifies if the server id belongs to our server.
+ ///
+ /// This function checks if the server identifier carried in the specified
+ /// DHCPv4 message belongs to this server. If the server identifier option
+ /// is absent or the value carried by this option is equal to one of the
+ /// server identifiers used by the server, the true is returned. If the
+ /// server identifier option is present, but it doesn't match any server
+ /// identifier used by this server, the false value is returned.
+ ///
+ /// @param pkt DHCPv4 message which server identifier is to be checked.
+ ///
+ /// @return true, if the server identifier is absent or matches one of the
+ /// server identifiers that the server is using; false otherwise.
+ bool acceptServerId(const Pkt4Ptr& pkt) const;
+
/// @brief verifies if specified packet meets RFC requirements
///
/// Checks if mandatory option is really there, that forbidden option
@@ -116,7 +190,7 @@ protected:
/// @param pkt packet to be checked
/// @param serverid expectation regarding server-id option
/// @throw RFCViolation if any issues are detected
- void sanityCheck(const Pkt4Ptr& pkt, RequirementLevel serverid);
+ static void sanityCheck(const Pkt4Ptr& pkt, RequirementLevel serverid);
/// @brief Processes incoming DISCOVER and returns response.
///
@@ -179,6 +253,18 @@ protected:
/// @param msg outgoing message (options will be added here)
void appendRequestedOptions(const Pkt4Ptr& question, Pkt4Ptr& msg);
+ /// @brief Appends requested vendor options as requested by client.
+ ///
+ /// This method is similar to \ref appendRequestedOptions(), but uses
+ /// vendor options. The major difference is that vendor-options use
+ /// its own option spaces (there may be more than one distinct set of vendor
+ /// options, each with unique vendor-id). Vendor options are requested
+ /// using separate options within their respective vendor-option spaces.
+ ///
+ /// @param question DISCOVER or REQUEST message from a client.
+ /// @param answer outgoing message (options will be added here)
+ void appendRequestedVendorOptions(const Pkt4Ptr& question, Pkt4Ptr& answer);
+
/// @brief Assigns a lease and appends corresponding options
///
/// This method chooses the most appropriate lease for reqesting
@@ -202,6 +288,117 @@ protected:
/// @param msg the message to add options to.
void appendBasicOptions(const Pkt4Ptr& question, Pkt4Ptr& msg);
+ /// @brief Processes Client FQDN and Hostname Options sent by a client.
+ ///
+ /// Client may send Client FQDN or Hostname option to communicate its name
+ /// to the server. Server may use this name to perform DNS update for the
+ /// lease being assigned to a client. If server takes responsibility for
+ /// updating DNS for a client it may communicate it by sending the Client
+ /// FQDN or Hostname %Option back to the client. Server select a different
+ /// name than requested by a client to update DNS. In such case, the server
+ /// stores this different name in its response.
+ ///
+ /// Client should not send both Client FQDN and Hostname options. However,
+ /// if client sends both options, server should prefer Client FQDN option
+ /// and ignore the Hostname option. If Client FQDN option is not present,
+ /// the Hostname option is processed.
+ ///
+ /// The Client FQDN %Option is processed by this function as described in
+ /// RFC4702.
+ ///
+ /// In response to a Hostname %Option sent by a client, the server may send
+ /// Hostname option with the same or different hostname. If different
+ /// hostname is sent, it is an indication to the client that server has
+ /// overridden the client's preferred name and will rather use this
+ /// different name to update DNS. However, since Hostname option doesn't
+ /// carry an information whether DNS update will be carried by the server
+ /// or not, the client is responsible for checking whether DNS update
+ /// has been performed.
+ ///
+ /// After successful processing options stored in the first parameter,
+ /// this function may add Client FQDN or Hostname option to the response
+ /// message. In some cases, server may cease to add any options to the
+ /// response, i.e. when server doesn't support DNS updates.
+ ///
+ /// This function does not throw. It simply logs the debug message if the
+ /// processing of the FQDN or Hostname failed.
+ ///
+ /// @param query A DISCOVER or REQUEST message from a cient.
+ /// @param [out] answer A response message to be sent to a client.
+ void processClientName(const Pkt4Ptr& query, Pkt4Ptr& answer);
+
+private:
+ /// @brief Process Client FQDN %Option sent by a client.
+ ///
+ /// This function is called by the @c Dhcpv4Srv::processClientName when
+ /// the client has sent the FQDN option in its message to the server.
+ /// It comprises the actual logic to parse the FQDN option and prepare
+ /// the FQDN option to be sent back to the client in the server's
+ /// response.
+ ///
+ /// @param fqdn An DHCPv4 Client FQDN %Option sent by a client.
+ /// @param [out] answer A response message to be sent to a client.
+ void processClientFqdnOption(const Option4ClientFqdnPtr& fqdn,
+ Pkt4Ptr& answer);
+
+ /// @brief Process Hostname %Option sent by a client.
+ ///
+ /// This function is called by the @c DHcpv4Srv::processClientName when
+ /// the client has sent the Hostname option in its message to the server.
+ /// It comprises the actual logic to parse the Hostname option and
+ /// prepare the Hostname option to be sent back to the client in the
+ /// server's response.
+ ///
+ /// @param opt_hostname An @c OptionCustom object encapsulating the Hostname
+ /// %Option.
+ /// @param [out] answer A response message to be sent to a client.
+ void processHostnameOption(const OptionCustomPtr& opt_hostname,
+ Pkt4Ptr& answer);
+
+protected:
+
+ /// @brief Creates NameChangeRequests which correspond to the lease
+ /// which has been acquired.
+ ///
+ /// If this function is called whe an existing lease is renewed, it
+ /// may generate NameChangeRequest to remove existing DNS entries which
+ /// correspond to the old lease instance. This function may cease to
+ /// generate NameChangeRequests if the notion of the client's FQDN hasn't
+ /// changed between an old and new lease.
+ ///
+ /// @param lease A pointer to the new lease which has been acquired.
+ /// @param old_lease A pointer to the instance of the old lease which has
+ /// been replaced by the new lease passed in the first argument. The NULL
+ /// value indicates that the new lease has been allocated, rather than
+ /// lease being renewed.
+ void createNameChangeRequests(const Lease4Ptr& lease,
+ const Lease4Ptr& old_lease);
+
+ /// @brief Creates the NameChangeRequest and adds to the queue for
+ /// processing.
+ ///
+ /// This function adds the @c isc::dhcp_ddns::NameChangeRequest to the
+ /// queue and emits the debug message which indicates whether the request
+ /// being added is to remove DNS entry or add a new entry. This function
+ /// is exception free.
+ ///
+ /// @param chg_type A type of the NameChangeRequest (ADD or REMOVE).
+ /// @param lease A lease for which the NameChangeRequest is created and
+ /// queued.
+ void queueNameChangeRequest(const isc::dhcp_ddns::NameChangeType chg_type,
+ const Lease4Ptr& lease);
+
+ /// @brief Sends all outstanding NameChangeRequests to b10-dhcp-ddns module.
+ ///
+ /// The purpose of this function is to pick all outstanding
+ /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
+ /// module.
+ ///
+ /// @todo Currently this function simply removes all requests from the
+ /// queue but doesn't send them anywhere. In the future, the
+ /// NameChangeSender will be used to deliver requests to the other module.
+ void sendNameChangeRequests();
+
/// @brief Attempts to renew received addresses
///
/// Attempts to renew existing lease. This typically includes finding a lease that
@@ -214,42 +411,89 @@ protected:
/// @brief Appends default options to a message
///
+ /// Currently it is only a Message Type option. This function does not add
+ /// the Server Identifier option as this option must be added using
+ /// @c Dhcpv4Srv::appendServerID.
+ ///
+ ///
/// @param msg message object (options will be added to it)
/// @param msg_type specifies message type
void appendDefaultOptions(Pkt4Ptr& msg, uint8_t msg_type);
- /// @brief Returns server-identifier option
+ /// @brief Adds server identifier option to the server's response.
///
- /// @return server-id option
- OptionPtr getServerID() { return serverid_; }
-
- /// @brief Sets server-identifier.
+ /// This method adds a server identifier to the DHCPv4 message. It epxects
+ /// that the local (source) address is set for this message. If address is
+ /// not set, it will throw an exception. This method also expects that the
+ /// server identifier option is not present in the specified message.
+ /// Otherwise, it will throw an exception on attempt to add a duplicate
+ /// server identifier option.
+ ///
+ /// @note This method doesn't throw exceptions by itself but the underlying
+ /// classes being used my throw. The reason for this method to not sanity
+ /// check the specified message is that it is meant to be called internally
+ /// by the @c Dhcpv4Srv class.
///
- /// This method attempts to set server-identifier DUID. It tries to
- /// load previously stored IP from configuration. If there is no previously
- /// stored server identifier, it will pick up one address from configured
- /// and supported network interfaces.
+ /// @note This method is static because it is not dependent on the class
+ /// state.
///
- /// @throws isc::Unexpected Failed to obtain server identifier (i.e. no
- // previously stored configuration and no network interfaces available)
- void generateServerID();
+ /// @param [out] response DHCPv4 message to which the server identifier
+ /// option should be added.
+ static void appendServerID(const Pkt4Ptr& response);
- /// @brief attempts to load server-id from a file
+ /// @brief Set IP/UDP and interface parameters for the DHCPv4 response.
+ ///
+ /// This method sets the following parameters for the DHCPv4 message being
+ /// sent to a client:
+ /// - client unicast or a broadcast address,
+ /// - client or relay port,
+ /// - server address,
+ /// - server port,
+ /// - name and index of the interface which is to be used to send the
+ /// message.
+ ///
+ /// Internally it calls the @c Dhcpv4Srv::adjustRemoteAddr to figure
+ /// out the destination address (client unicast address or broadcast
+ /// address).
+ ///
+ /// The destination port is always DHCPv4 client (68) or relay (67) port,
+ /// depending if the response will be sent directly to a client.
///
- /// Tries to load duid from a text file. If the load is successful,
- /// it creates server-id option and stores it in serverid_ (to be used
- /// later by getServerID()).
+ /// The source port is always set to DHCPv4 server port (67).
///
- /// @param file_name name of the server-id file to load
- /// @return true if load was successful, false otherwise
- bool loadServerID(const std::string& file_name);
+ /// The interface selected for the response is always the same as the
+ /// one through which the query has been received.
+ ///
+ /// The source address for the response is the IPv4 address assigned to
+ /// the interface being used to send the response. This function uses
+ /// @c IfaceMgr to get the socket bound to the IPv4 address on the
+ /// particular interface.
+ ///
+ /// @note This method is static because it is not dependent on the class
+ /// state.
+ static void adjustIfaceData(const Pkt4Ptr& query, const Pkt4Ptr& response);
- /// @brief attempts to write server-id to a file
- /// Tries to write server-id content (stored in serverid_) to a text file.
+ /// @brief Sets remote addresses for outgoing packet.
+ ///
+ /// This method sets the local and remote addresses on outgoing packet.
+ /// The addresses being set depend on the following conditions:
+ /// - has incoming packet been relayed,
+ /// - is direct response to a client without address supported,
+ /// - type of the outgoing packet,
+ /// - broadcast flag set in the incoming packet.
+ ///
+ /// @warning This method does not check whether provided packet pointers
+ /// are valid. Make sure that pointers are correct before calling this
+ /// function.
///
- /// @param file_name name of the server-id file to write
- /// @return true if write was successful, false otherwise
- bool writeServerID(const std::string& file_name);
+ /// @note This method is static because it is not dependent on the class
+ /// state.
+ ///
+ /// @param question instance of a packet received by a server.
+ /// @param [out] response response packet which addresses are to be
+ /// adjusted.
+ static void adjustRemoteAddr(const Pkt4Ptr& question,
+ const Pkt4Ptr& response);
/// @brief converts server-id to text
/// Converts content of server-id option to a text representation, e.g.
@@ -259,20 +503,82 @@ protected:
/// @return string representation
static std::string srvidToString(const OptionPtr& opt);
+ /// @brief Computes DHCID from a lease.
+ ///
+ /// This method creates an object which represents DHCID. The DHCID value
+ /// is computed as described in RFC4701. The section 3.3. of RFC4701
+ /// specifies the DHCID RR Identifier Type codes:
+ /// - 0x0000 The 1 octet htype followed by glen octets of chaddr
+ /// - 0x0001 The data octets from the DHCPv4 client's Client Identifier
+ /// option.
+ /// - 0x0002 The client's DUID.
+ ///
+ /// Currently this function supports first two of these identifiers.
+ /// The 0x0001 is preferred over the 0x0000 - if the client identifier
+ /// option is present, the former is used. If the client identifier
+ /// is absent, the latter is used.
+ ///
+ /// @todo Implement support for 0x0002 DHCID identifier type.
+ ///
+ /// @param lease A pointer to the structure describing a lease.
+ /// @return An object encapsulating DHCID to be used for DNS updates.
+ /// @throw DhcidComputeError If the computation of the DHCID failed.
+ static isc::dhcp_ddns::D2Dhcid computeDhcid(const Lease4Ptr& lease);
+
/// @brief Selects a subnet for a given client's packet.
///
/// @param question client's message
/// @return selected subnet (or NULL if no suitable subnet was found)
isc::dhcp::Subnet4Ptr selectSubnet(const Pkt4Ptr& question);
- /// server DUID (to be sent in server-identifier option)
- OptionPtr serverid_;
-
/// indicates if shutdown is in progress. Setting it to true will
/// initiate server shutdown procedure.
volatile bool shutdown_;
- private:
+ /// @brief dummy wrapper around IfaceMgr::receive4
+ ///
+ /// This method is useful for testing purposes, where its replacement
+ /// simulates reception of a packet. For that purpose it is protected.
+ virtual Pkt4Ptr receivePacket(int timeout);
+
+ /// @brief dummy wrapper around IfaceMgr::send()
+ ///
+ /// This method is useful for testing purposes, where its replacement
+ /// simulates transmission of a packet. For that purpose it is protected.
+ virtual void sendPacket(const Pkt4Ptr& pkt);
+
+ /// @brief Implements a callback function to parse options in the message.
+ ///
+ /// @param buf a A buffer holding options in on-wire format.
+ /// @param option_space A name of the option space which holds definitions
+ /// of to be used to parse options in the packets.
+ /// @param [out] options A reference to the collection where parsed options
+ /// will be stored.
+ /// @return An offset to the first byte after last parsed option.
+ size_t unpackOptions(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options);
+
+ /// @brief Assigns incoming packet to zero or more classes.
+ ///
+ /// @note For now, the client classification is very simple. It just uses
+ /// content of the vendor-class-identifier option as a class. The resulting
+ /// class will be stored in packet (see @ref isc::dhcp::Pkt4::classes_ and
+ /// @ref isc::dhcp::Pkt4::inClass).
+ ///
+ /// @param pkt packet to be classified
+ void classifyPacket(const Pkt4Ptr& pkt);
+
+ /// @brief Performs packet processing specific to a class
+ ///
+ /// This processing is a likely candidate to be pushed into hooks.
+ ///
+ /// @param query incoming client's packet
+ /// @param rsp server's response
+ /// @return true if successful, false otherwise (will prevent sending response)
+ bool classSpecificProcessing(const Pkt4Ptr& query, const Pkt4Ptr& rsp);
+
+private:
/// @brief Constructs netmask option based on subnet4
/// @param subnet subnet for which the netmask will be calculated
@@ -280,12 +586,34 @@ protected:
/// @return Option that contains netmask information
static OptionPtr getNetmaskOption(const Subnet4Ptr& subnet);
+ /// @brief Implements the error handler for socket open failure.
+ ///
+ /// This callback function is installed on the @c isc::dhcp::IfaceMgr
+ /// when IPv4 sockets are being open. When socket fails to open for
+ /// any reason, this function is called. It simply logs the error message.
+ ///
+ /// @param errmsg An error message containing a cause of the failure.
+ static void ifaceMgrSocket4ErrorHandler(const std::string& errmsg);
+
/// @brief Allocation Engine.
/// Pointer to the allocation engine that we are currently using
/// It must be a pointer, because we will support changing engines
/// during normal operation (e.g. to use different allocators)
boost::shared_ptr<AllocEngine> alloc_engine_;
+ uint16_t port_; ///< UDP port number on which server listens.
+ bool use_bcast_; ///< Should broadcast be enabled on sockets (if true).
+
+ /// Indexes for registered hook points
+ int hook_index_pkt4_receive_;
+ int hook_index_subnet4_select_;
+ int hook_index_pkt4_send_;
+
+protected:
+
+ /// Holds a list of @c isc::dhcp_ddns::NameChangeRequest objects which
+ /// are waiting for sending to b10-dhcp-ddns module.
+ std::queue<isc::dhcp_ddns::NameChangeRequest> name_change_reqs_;
};
}; // namespace isc::dhcp
diff --git a/src/bin/dhcp4/tests/.gitignore b/src/bin/dhcp4/tests/.gitignore
index 5d14dac..5983892 100644
--- a/src/bin/dhcp4/tests/.gitignore
+++ b/src/bin/dhcp4/tests/.gitignore
@@ -1 +1,4 @@
/dhcp4_unittests
+/marker_file.h
+/test_data_files_config.h
+/test_libraries.h
diff --git a/src/bin/dhcp4/tests/Makefile.am b/src/bin/dhcp4/tests/Makefile.am
index 3e10c3b..c72b8b0 100644
--- a/src/bin/dhcp4/tests/Makefile.am
+++ b/src/bin/dhcp4/tests/Makefile.am
@@ -32,6 +32,7 @@ AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/bin/dhcp6/tests\"
AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
+CLEANFILES += $(builddir)/load_marker.txt $(builddir)/unload_marker.txt
AM_CXXFLAGS = $(B10_CXXFLAGS)
if USE_CLANGPP
@@ -39,26 +40,52 @@ if USE_CLANGPP
AM_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_STATIC_LINK
-AM_LDFLAGS = -static
-endif
-
TESTS_ENVIRONMENT = \
$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
TESTS =
if HAVE_GTEST
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+lib_LTLIBRARIES = libco1.la libco2.la
+
+libco1_la_SOURCES = callout_library_1.cc callout_library_common.h
+libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+libco2_la_SOURCES = callout_library_2.cc callout_library_common.h
+libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
TESTS += dhcp4_unittests
dhcp4_unittests_SOURCES = ../dhcp4_srv.h ../dhcp4_srv.cc ../ctrl_dhcp4_srv.cc
dhcp4_unittests_SOURCES += ../dhcp4_log.h ../dhcp4_log.cc
dhcp4_unittests_SOURCES += ../config_parser.cc ../config_parser.h
+dhcp4_unittests_SOURCES += dhcp4_test_utils.h
dhcp4_unittests_SOURCES += dhcp4_unittests.cc
dhcp4_unittests_SOURCES += dhcp4_srv_unittest.cc
+dhcp4_unittests_SOURCES += dhcp4_test_utils.cc dhcp4_test_utils.h
+dhcp4_unittests_SOURCES += wireshark.cc
dhcp4_unittests_SOURCES += ctrl_dhcp4_srv_unittest.cc
dhcp4_unittests_SOURCES += config_parser_unittest.cc
+dhcp4_unittests_SOURCES += fqdn_unittest.cc
+dhcp4_unittests_SOURCES += marker_file.cc
nodist_dhcp4_unittests_SOURCES = ../dhcp4_messages.h ../dhcp4_messages.cc
+nodist_dhcp4_unittests_SOURCES += marker_file.h test_libraries.h
dhcp4_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
dhcp4_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
@@ -67,10 +94,12 @@ dhcp4_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
dhcp4_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+dhcp4_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
endif
noinst_PROGRAMS = $(TESTS)
diff --git a/src/bin/dhcp4/tests/callout_library_1.cc b/src/bin/dhcp4/tests/callout_library_1.cc
new file mode 100644
index 0000000..471bb6f
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_1.cc
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 1;
+#include "callout_library_common.h"
diff --git a/src/bin/dhcp4/tests/callout_library_2.cc b/src/bin/dhcp4/tests/callout_library_2.cc
new file mode 100644
index 0000000..b0b4637
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_2.cc
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 2;
+#include "callout_library_common.h"
diff --git a/src/bin/dhcp4/tests/callout_library_common.h b/src/bin/dhcp4/tests/callout_library_common.h
new file mode 100644
index 0000000..cbabcda
--- /dev/null
+++ b/src/bin/dhcp4/tests/callout_library_common.h
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests.
+///
+/// To check that they libraries are loaded and unloaded correctly, the load
+/// and unload functions in this library maintain two marker files - the load
+/// marker file and the unload marker file. The functions append a single
+/// line to the file, creating the file if need be. In this way, the test code
+/// can determine whether the load/unload functions have been run and, if so,
+/// in what order.
+///
+/// This file is the common library file for the tests. It will not compile
+/// by itself - it is included into each callout library which specifies the
+/// missing constant LIBRARY_NUMBER before the inclusion.
+
+#include <hooks/hooks.h>
+#include "marker_file.h"
+
+#include <fstream>
+
+using namespace isc::hooks;
+using namespace std;
+
+extern "C" {
+
+/// @brief Append digit to marker file
+///
+/// If the marker file does not exist, create it. Then append the single
+/// digit (given by the constant LIBRARY_NUMBER) defined earlier to it and
+/// close the file.
+///
+/// @param name Name of the file to open
+///
+/// @return 0 on success, non-zero on error.
+int
+appendDigit(const char* name) {
+ // Open the file and check if successful.
+ fstream file(name, fstream::out | fstream::app);
+ if (!file.good()) {
+ return (1);
+ }
+
+ // Add the library number to it and close.
+ file << LIBRARY_NUMBER;
+ file.close();
+
+ return (0);
+}
+
+// Framework functions
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int
+load(LibraryHandle&) {
+ return (appendDigit(LOAD_MARKER_FILE));
+}
+
+int
+unload() {
+ return (appendDigit(UNLOAD_MARKER_FILE));
+}
+
+};
diff --git a/src/bin/dhcp4/tests/config_parser_unittest.cc b/src/bin/dhcp4/tests/config_parser_unittest.cc
index 61e6b61..14c4f80 100644
--- a/src/bin/dhcp4/tests/config_parser_unittest.cc
+++ b/src/bin/dhcp4/tests/config_parser_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -23,8 +23,14 @@
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_int.h>
+#include <dhcp/docsis3_option_defs.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/cfgmgr.h>
+#include <hooks/hooks_manager.h>
+
+#include "marker_file.h"
+#include "test_libraries.h"
+#include "test_data_files_config.h"
#include <boost/foreach.hpp>
#include <boost/scoped_ptr.hpp>
@@ -34,15 +40,34 @@
#include <sstream>
#include <limits.h>
-using namespace std;
using namespace isc;
-using namespace isc::dhcp;
using namespace isc::asiolink;
-using namespace isc::data;
using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace std;
namespace {
+/// @brief Prepends the given name with the DHCP4 source directory
+///
+/// @param name file name of the desired file
+/// @return string containing the absolute path of the file in the DHCP source
+/// directory.
+std::string specfile(const std::string& name) {
+ return (std::string(DHCP4_SRC_DIR) + "/" + name);
+}
+
+/// @brief Tests that the spec file is valid.
+/// Verifies that the BIND10 DHCP-DDNS configuration specification file
+// is valid.
+TEST(Dhcp4SpecTest, basicSpec) {
+ (isc::config::moduleSpecFromFile(specfile("dhcp4.spec")));
+ ASSERT_NO_THROW(isc::config::moduleSpecFromFile(specfile("dhcp4.spec")));
+}
+
class Dhcp4ParserTest : public ::testing::Test {
public:
Dhcp4ParserTest()
@@ -51,13 +76,24 @@ public:
// deal with sockets here, just check if configuration handling
// is sane.
srv_.reset(new Dhcpv4Srv(0));
+ CfgMgr::instance().deleteActiveIfaces();
+ }
+
+ // Check that no hooks libraries are loaded. This is a pre-condition for
+ // a number of tests, so is checked in one place. As this uses an
+ // ASSERT call - and it is not clear from the documentation that Gtest
+ // predicates can be used in a constructor - the check is placed in SetUp.
+ void SetUp() {
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_TRUE(libraries.empty());
}
// Checks if global parameter of name have expected_value
void checkGlobalUint32(string name, uint32_t expected_value) {
- const Uint32Storage& uint32_defaults = getUint32Defaults();
+ const Uint32StoragePtr uint32_defaults =
+ globalContext()->uint32_values_;
try {
- uint32_t actual_value = uint32_defaults.getParam(name);
+ uint32_t actual_value = uint32_defaults->getParam(name);
EXPECT_EQ(expected_value, actual_value);
} catch (DhcpConfigError) {
ADD_FAILURE() << "Expected uint32 with name " << name
@@ -76,6 +112,10 @@ public:
~Dhcp4ParserTest() {
resetConfiguration();
+
+ // ... and delete the hooks library marker files if present
+ unlink(LOAD_MARKER_FILE);
+ unlink(UNLOAD_MARKER_FILE);
};
/// @brief Create the simple configuration with single option.
@@ -137,7 +177,7 @@ public:
/// describing an option.
std::string createConfigWithOption(const std::map<std::string, std::string>& params) {
std::ostringstream stream;
- stream << "{ \"interface\": [ \"all\" ],"
+ stream << "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -234,56 +274,82 @@ public:
expected_data_len));
}
- /// @brief Reset configuration database.
+ /// @brief Parse and Execute configuration
///
- /// This function resets configuration data base by
- /// removing all subnets and option-data. Reset must
- /// be performed after each test to make sure that
- /// contents of the database do not affect result of
- /// subsequent tests.
- void resetConfiguration() {
+ /// Parses a configuration and executes a configuration of the server.
+ /// If the operation fails, the current test will register a failure.
+ ///
+ /// @param config Configuration to parse
+ /// @param operation Operation being performed. In the case of an error,
+ /// the error text will include the string "unable to <operation>.".
+ ///
+ /// @return true if the configuration succeeded, false if not. In the
+ /// latter case, a failure will have been added to the current test.
+ bool
+ executeConfiguration(const std::string& config, const char* operation) {
ConstElementPtr status;
-
- string config = "{ \"interface\": [ \"all\" ],"
- "\"rebind-timer\": 2000, "
- "\"renew-timer\": 1000, "
- "\"valid-lifetime\": 4000, "
- "\"subnet4\": [ ], "
- "\"option-def\": [ ], "
- "\"option-data\": [ ] }";
-
try {
ElementPtr json = Element::fromJSON(config);
status = configureDhcp4Server(*srv_, json);
} catch (const std::exception& ex) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. The following configuration was used"
- << " to reset database: " << std::endl
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The following configuration was used: " << std::endl
<< config << std::endl
<< " and the following error message was returned:"
<< ex.what() << std::endl;
+ return (false);
}
- // status object must not be NULL
+ // The status object must not be NULL
if (!status) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. Configuration function returned"
- << " NULL pointer" << std::endl;
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned a null pointer.";
+ return (false);
}
+ // Store the answer if we need it.
+
+ // Returned value should be 0 (configuration success)
comment_ = parseAnswer(rcode_, status);
- // returned value should be 0 (configuration success)
if (rcode_ != 0) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. Configuration function returned"
- << " error code " << rcode_ << std::endl;
+ string reason = "";
+ if (comment_) {
+ reason = string(" (") + comment_->stringValue() + string(")");
+ }
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned error code "
+ << rcode_ << reason;
+ return (false);
}
+
+ return (true);
+ }
+
+ /// @brief Reset configuration database.
+ ///
+ /// This function resets configuration data base by
+ /// removing all subnets and option-data. Reset must
+ /// be performed after each test to make sure that
+ /// contents of the database do not affect result of
+ /// subsequent tests.
+ void resetConfiguration() {
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"hooks-libraries\": [ ], "
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet4\": [ ], "
+ "\"dhcp-ddns\": { \"enable-updates\" : false }, "
+ "\"option-def\": [ ], "
+ "\"option-data\": [ ] }";
+ static_cast<void>(executeConfiguration(config,
+ "reset configuration database"));
}
- boost::scoped_ptr<Dhcpv4Srv> srv_;
- int rcode_;
- ConstElementPtr comment_;
+ boost::scoped_ptr<Dhcpv4Srv> srv_; // DHCP4 server under test
+ int rcode_; // Return code from element parsing
+ ConstElementPtr comment_; // Reason for parse fail
};
// Goal of this test is a verification if a very simple config update
@@ -321,7 +387,7 @@ TEST_F(Dhcp4ParserTest, emptySubnet) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp4Server(*srv_,
- Element::fromJSON("{ \"interface\": [ \"all\" ],"
+ Element::fromJSON("{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ ], "
@@ -341,7 +407,7 @@ TEST_F(Dhcp4ParserTest, subnetGlobalDefaults) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -363,6 +429,379 @@ TEST_F(Dhcp4ParserTest, subnetGlobalDefaults) {
EXPECT_EQ(1000, subnet->getT1());
EXPECT_EQ(2000, subnet->getT2());
EXPECT_EQ(4000, subnet->getValid());
+
+ // Check that subnet-id is 1
+ EXPECT_EQ(1, subnet->getID());
+}
+
+// Goal of this test is to verify that multiple subnets get unique
+// subnet-ids. Also, test checks that it's possible to do reconfiguration
+// multiple times.
+TEST_F(Dhcp4ParserTest, multipleSubnets) {
+ ConstElementPtr x;
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.3.101 - 192.0.3.150\" ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.4.101 - 192.0.4.150\" ],"
+ " \"subnet\": \"192.0.4.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.5.101 - 192.0.5.150\" ],"
+ " \"subnet\": \"192.0.5.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ int cnt = 0; // Number of reconfigurations
+
+ do {
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Check subnet-ids of each subnet (it should be monotonously increasing)
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ EXPECT_EQ(2, subnets->at(1)->getID());
+ EXPECT_EQ(3, subnets->at(2)->getID());
+ EXPECT_EQ(4, subnets->at(3)->getID());
+
+ // Repeat reconfiguration process 10 times and check that the subnet-id
+ // is set to the same value. Technically, just two iterations would be
+ // sufficient, but it's nice to have a test that exercises reconfiguration
+ // a bit.
+ } while (++cnt < 10);
+}
+
+// Goal of this test is to verify that a previously configured subnet can be
+// deleted in subsequent reconfiguration.
+TEST_F(Dhcp4ParserTest, reconfigureRemoveSubnet) {
+ ConstElementPtr x;
+
+ // All four subnets
+ string config4 = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.3.101 - 192.0.3.150\" ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.4.101 - 192.0.4.150\" ],"
+ " \"subnet\": \"192.0.4.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.5.101 - 192.0.5.150\" ],"
+ " \"subnet\": \"192.0.5.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Three subnets (the last one removed)
+ string config_first3 = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.3.101 - 192.0.3.150\" ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.4.101 - 192.0.4.150\" ],"
+ " \"subnet\": \"192.0.4.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Second subnet removed
+ string config_second_removed = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.4.101 - 192.0.4.150\" ],"
+ " \"subnet\": \"192.0.4.0/24\" "
+ " },"
+ " {"
+ " \"pool\": [ \"192.0.5.101 - 192.0.5.150\" ],"
+ " \"subnet\": \"192.0.5.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // CASE 1: Configure 4 subnets, then reconfigure and remove the
+ // last one.
+
+ ElementPtr json = Element::fromJSON(config4);
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Do the reconfiguration (the last subnet is removed)
+ json = Element::fromJSON(config_first3);
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ subnets = CfgMgr::instance().getSubnets4();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 3 subnets now (4th is removed)
+
+ // Check subnet-ids of each subnet (it should be monotonously increasing)
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ EXPECT_EQ(2, subnets->at(1)->getID());
+ EXPECT_EQ(3, subnets->at(2)->getID());
+
+ /// CASE 2: Configure 4 subnets, then reconfigure and remove one
+ /// from in between (not first, not last)
+
+#if 0
+ /// @todo: Uncomment subnet removal test as part of #3281.
+ json = Element::fromJSON(config4);
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ // Do reconfiguration
+ json = Element::fromJSON(config_second_removed);
+ EXPECT_NO_THROW(x = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ subnets = CfgMgr::instance().getSubnets4();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 4 subnets
+
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ // The second subnet (with subnet-id = 2) is no longer there
+ EXPECT_EQ(3, subnets->at(1)->getID());
+ EXPECT_EQ(4, subnets->at(2)->getID());
+#endif
+
+}
+
+/// @todo: implement subnet removal test as part of #3281.
+
+// Checks if the next-server defined as global parameter is taken into
+// consideration.
+TEST_F(Dhcp4ParserTest, nextServerGlobal) {
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"1.2.3.4\", "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as subnet parameter is taken into
+// consideration.
+TEST_F(Dhcp4ParserTest, nextServerSubnet) {
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
+// Test checks several negative scenarios for next-server configuration: bogus
+// address, IPv6 adddress and empty string.
+TEST_F(Dhcp4ParserTest, nextServerNegative) {
+
+ ConstElementPtr status;
+
+ // Config with junk instead of next-server address
+ string config_bogus1 = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"a.b.c.d\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with IPv6 next server address
+ string config_bogus2 = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"2001:db8::1\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Config with empty next server address
+ string config_bogus3 = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"next-server\": \"\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json1 = Element::fromJSON(config_bogus1);
+ ElementPtr json2 = Element::fromJSON(config_bogus2);
+ ElementPtr json3 = Element::fromJSON(config_bogus3);
+
+ // check if returned status is always a failure
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json1));
+ checkResult(status, 1);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json2));
+ checkResult(status, 1);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json3));
+ checkResult(status, 0);
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value.
+TEST_F(Dhcp4ParserTest, nextServerOverride) {
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Now check if the configuration was indeed handled and we have
+ // expected pool configured.
+ Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.200"));
+ ASSERT_TRUE(subnet);
+ EXPECT_EQ("1.2.3.4", subnet->getSiaddr().toText());
+}
+
+// Check whether it is possible to configure echo-client-id
+TEST_F(Dhcp4ParserTest, echoClientId) {
+
+ ConstElementPtr status;
+
+ string config_false = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"echo-client-id\": false,"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ string config_true = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"echo-client-id\": true,"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json_false = Element::fromJSON(config_false);
+ ElementPtr json_true = Element::fromJSON(config_true);
+
+ // Let's check the default. It should be true
+ ASSERT_TRUE(CfgMgr::instance().echoClientId());
+
+ // Now check that "false" configuration is really applied.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json_false));
+ ASSERT_FALSE(CfgMgr::instance().echoClientId());
+
+ // Now check that "true" configuration is really applied.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json_true));
+ ASSERT_TRUE(CfgMgr::instance().echoClientId());
+
+ // In any case revert back to the default value (true)
+ CfgMgr::instance().echoClientId(true);
}
// This test checks if it is possible to override global values
@@ -371,7 +810,7 @@ TEST_F(Dhcp4ParserTest, subnetLocal) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -402,7 +841,7 @@ TEST_F(Dhcp4ParserTest, poolOutOfSubnet) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -426,7 +865,7 @@ TEST_F(Dhcp4ParserTest, poolPrefixLen) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -948,7 +1387,7 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) {
// configuration does not include options configuration.
TEST_F(Dhcp4ParserTest, optionDataDefaults) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1021,7 +1460,7 @@ TEST_F(Dhcp4ParserTest, optionDataTwoSpaces) {
// The definition is not required for the option that
// belongs to the 'dhcp4' option space as it is the
// standard option.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1099,7 +1538,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
// at the very end (when all other parameters are configured).
// Starting stage 1. Configure sub-options and their definitions.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1148,7 +1587,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
// the configuration from the stage 2 is repeated because BIND
// configuration manager sends whole configuration for the lists
// where at least one element is being modified or added.
- config = "{ \"interface\": [ \"all\" ],"
+ config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1244,7 +1683,7 @@ TEST_F(Dhcp4ParserTest, optionDataEncapsulate) {
// option setting.
TEST_F(Dhcp4ParserTest, optionDataInSingleSubnet) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"option-data\": [ {"
@@ -1316,7 +1755,7 @@ TEST_F(Dhcp4ParserTest, optionDataInSingleSubnet) {
// for multiple subnets.
TEST_F(Dhcp4ParserTest, optionDataInMultipleSubnets) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
"\"subnet4\": [ { "
@@ -1596,7 +2035,7 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
// In the first stahe we create definitions of suboptions
// that we will add to the base option.
// Let's create some dummy options: foo and foo2.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1649,7 +2088,7 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
// We add our dummy options to this option space and thus
// they should be included as sub-options in the 'vendor-opts'
// option.
- config = "{ \"interface\": [ \"all\" ],"
+ config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1748,5 +2187,434 @@ TEST_F(Dhcp4ParserTest, stdOptionDataEncapsulate) {
EXPECT_FALSE(desc.option->getOption(3));
}
+// This test checks if vendor options can be specified in the config file
+// (in hex format), and later retrieved from configured subnet
+TEST_F(Dhcp4ParserTest, vendorOptionsHex) {
-};
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"option-one\","
+ " \"space\": \"vendor-4491\"," // VENDOR_ID_CABLE_LABS = 4491
+ " \"code\": 100," // just a random code
+ " \"data\": \"AB CDEF0105\","
+ " \"csv-format\": False"
+ " },"
+ " {"
+ " \"name\": \"option-two\","
+ " \"space\": \"vendor-1234\","
+ " \"code\": 100,"
+ " \"data\": \"1234\","
+ " \"csv-format\": False"
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1-192.0.2.10\" ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}";
+
+ ConstElementPtr status;
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Options should be now available for the subnet.
+ Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.5"));
+ ASSERT_TRUE(subnet);
+
+ // Try to get the option from the vendor space 4491
+ Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(VENDOR_ID_CABLE_LABS, 100);
+ ASSERT_TRUE(desc1.option);
+ EXPECT_EQ(100, desc1.option->getType());
+ // Try to get the option from the vendor space 1234
+ Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(1234, 100);
+ ASSERT_TRUE(desc2.option);
+ EXPECT_EQ(100, desc1.option->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ Subnet::OptionDescriptor desc3 = subnet->getVendorOptionDescriptor(5678, 100);
+ ASSERT_FALSE(desc3.option);
+}
+
+// This test checks if vendor options can be specified in the config file,
+// (in csv format), and later retrieved from configured subnet
+TEST_F(Dhcp4ParserTest, vendorOptionsCsv) {
+
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 100,"
+ " \"data\": \"this is a string vendor-opt\","
+ " \"csv-format\": True"
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"vendor-4491\","
+ " \"encapsulate\": \"\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" "
+ " } ]"
+ "}";
+
+ ConstElementPtr status;
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Options should be now available for the subnet.
+ Subnet4Ptr subnet = CfgMgr::instance().getSubnet4(IOAddress("192.0.2.5"));
+ ASSERT_TRUE(subnet);
+
+ // Try to get the option from the vendor space 4491
+ Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(VENDOR_ID_CABLE_LABS, 100);
+ ASSERT_TRUE(desc1.option);
+ EXPECT_EQ(100, desc1.option->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(5678, 100);
+ ASSERT_FALSE(desc2.option);
+}
+
+
+
+// Tests of the hooks libraries configuration. All tests have the pre-
+// condition (checked in the test fixture's SetUp() method) that no hooks
+// libraries are loaded at the start of the tests.
+
+// Helper function to return a configuration containing an arbitrary number
+// of hooks libraries.
+std::string
+buildHooksLibrariesConfig(const std::vector<std::string>& libraries) {
+ const string quote("\"");
+
+ // Create the first part of the configuration string.
+ string config =
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"hooks-libraries\": [";
+
+ // Append the libraries (separated by commas if needed)
+ for (int i = 0; i < libraries.size(); ++i) {
+ if (i > 0) {
+ config += string(", ");
+ }
+ config += (quote + libraries[i] + quote);
+ }
+
+ // Append the remainder of the configuration.
+ config += string(
+ "],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"dhcp-message\","
+ " \"space\": \"dhcp4\","
+ " \"code\": 56,"
+ " \"data\": \"AB CDEF0105\","
+ " \"csv-format\": False"
+ " },"
+ " {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"code\": 56,"
+ " \"data\": \"1234\","
+ " \"csv-format\": True"
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 56,"
+ " \"type\": \"uint32\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"\""
+ " } ],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\""
+ " } ]"
+ "}");
+
+ return (config);
+}
+
+// Convenience function for creating hooks library configuration with one or
+// two character string constants.
+std::string
+buildHooksLibrariesConfig(const char* library1 = NULL,
+ const char* library2 = NULL) {
+ std::vector<std::string> libraries;
+ if (library1 != NULL) {
+ libraries.push_back(string(library1));
+ if (library2 != NULL) {
+ libraries.push_back(string(library2));
+ }
+ }
+ return (buildHooksLibrariesConfig(libraries));
+}
+
+
+// The goal of this test is to verify the configuration of hooks libraries if
+// none are specified.
+TEST_F(Dhcp4ParserTest, NoHooksLibraries) {
+ // Parse a configuration containing no names.
+ string config = buildHooksLibrariesConfig();
+ if (!executeConfiguration(config,
+ "set configuration with no hooks libraries")) {
+ FAIL() << "Unable to execute configuration";
+
+ } else {
+ // No libraries should be loaded at the end of the test.
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+ }
+}
+
+// Verify parsing fails with one library that will fail validation.
+TEST_F(Dhcp4ParserTest, InvalidLibrary) {
+ // Parse a configuration containing a failing library.
+ string config = buildHooksLibrariesConfig(NOT_PRESENT_LIBRARY);
+
+ ConstElementPtr status;
+ ElementPtr json = Element::fromJSON(config);
+ ASSERT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+
+ // The status object must not be NULL
+ ASSERT_TRUE(status);
+
+ // Returned value should not be 0
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_NE(0, rcode_);
+}
+
+// Verify the configuration of hooks libraries with two being specified.
+TEST_F(Dhcp4ParserTest, LibrariesSpecified) {
+ // Marker files should not be present.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Set up the configuration with two libraries and load them.
+ string config = buildHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+ ASSERT_TRUE(executeConfiguration(config,
+ "load two valid libraries"));
+
+ // Expect two libraries to be loaded in the correct order (load marker file
+ // is present, no unload marker file).
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Unload the libraries. The load file should not have changed, but
+ // the unload one should indicate the unload() functions have been run.
+ config = buildHooksLibrariesConfig();
+ ASSERT_TRUE(executeConfiguration(config, "unloading libraries"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+
+ // Expect the hooks system to say that none are loaded.
+ libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+
+}
+
+// This test verifies that it is possible to select subset of interfaces
+// on which server should listen.
+TEST_F(Dhcp4ParserTest, selectedInterfaces) {
+ ConstElementPtr x;
+ string config = "{ \"interfaces\": [ \"eth0\", \"eth1\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ ConstElementPtr status;
+
+ // Make sure the config manager is clean and there is no hanging
+ // interface configuration.
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+ // Apply configuration.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // eth0 and eth1 were explicitly selected. eth2 was not.
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+ EXPECT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+// This test verifies that it is possible to configure the server in such a way
+// that it listens on all interfaces.
+TEST_F(Dhcp4ParserTest, allInterfaces) {
+ ConstElementPtr x;
+ // This configuration specifies two interfaces on which server should listen
+ // but it also includes asterisk. The asterisk switches server into the
+ // mode when it listens on all interfaces regardless of what interface names
+ // were specified in the "interfaces" parameter.
+ string config = "{ \"interfaces\": [ \"eth0\", \"*\", \"eth1\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ ConstElementPtr status;
+
+ // Make sure there is no old configuration.
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+ // Apply configuration.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // All interfaces should be now active.
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+// This test checks the ability of the server to parse a configuration
+// containing a full, valid dhcp-ddns (D2ClientConfig) entry.
+TEST_F(Dhcp4ParserTest, d2ClientConfig) {
+ ConstElementPtr status;
+
+ // Verify that the D2 configuraiton can be fetched and is set to disabled.
+ D2ClientConfigPtr d2_client_config = CfgMgr::instance().getD2ClientConfig();
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+ // Verify that the convenience method agrees.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ string config_str = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.168.2.1\", "
+ " \"server-port\" : 777, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"allow-client-update\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ElementPtr config;
+ ASSERT_NO_THROW(config = Element::fromJSON(config_str));
+
+ // Pass the configuration in for parsing.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, config));
+
+ // check if returned status is OK
+ checkResult(status, 0);
+
+ // Verify that DHCP-DDNS updating is enabled.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Verify that the D2 configuration can be retrieved.
+ d2_client_config = CfgMgr::instance().getD2ClientConfig();
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the configuration values are correct.
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ("192.168.2.1", d2_client_config->getServerIp().toText());
+ EXPECT_EQ(777, d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+ EXPECT_TRUE(d2_client_config->getRemoveOnRenew());
+ EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
+ EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
+ EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
+ EXPECT_TRUE(d2_client_config->getReplaceClientName());
+ EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
+ EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
+}
+
+// This test checks the ability of the server to handle a configuration
+// containing an invalid dhcp-ddns (D2ClientConfig) entry.
+TEST_F(Dhcp4ParserTest, invalidD2ClientConfig) {
+ ConstElementPtr status;
+
+ // Configuration string with an invalid D2 client config,
+ // "server-ip" is missing.
+ string config_str = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ " \"dhcp-ddns\" : {"
+ " \"enable-updates\" : true, "
+ " \"server-port\" : 5301, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"allow-client-update\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" },"
+ "\"valid-lifetime\": 4000 }";
+
+ // Convert the JSON string to configuration elements.
+ ElementPtr config;
+ ASSERT_NO_THROW(config = Element::fromJSON(config_str));
+
+ // Configuration should not throw, but should fail.
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, config));
+
+ // check if returned status is failed.
+ checkResult(status, 1);
+
+ // Verify that the D2 configuraiton can be fetched and is set to disabled.
+ D2ClientConfigPtr d2_client_config = CfgMgr::instance().getD2ClientConfig();
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+ // Verify that the convenience method agrees.
+ ASSERT_FALSE(CfgMgr::instance().ddnsEnabled());
+}
+
+}
diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
index 13b57be..e6b500b 100644
--- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
+++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
@@ -17,6 +17,10 @@
#include <config/ccsession.h>
#include <dhcp/dhcp4.h>
#include <dhcp4/ctrl_dhcp4_srv.h>
+#include <hooks/hooks_manager.h>
+
+#include "marker_file.h"
+#include "test_libraries.h"
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
@@ -26,13 +30,16 @@
#include <sstream>
#include <arpa/inet.h>
+#include <unistd.h>
using namespace std;
using namespace isc;
-using namespace isc::dhcp;
using namespace isc::asiolink;
-using namespace isc::data;
using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
namespace {
@@ -45,10 +52,25 @@ public:
class CtrlDhcpv4SrvTest : public ::testing::Test {
public:
CtrlDhcpv4SrvTest() {
+ reset();
}
~CtrlDhcpv4SrvTest() {
+ reset();
};
+
+ /// @brief Reset hooks data
+ ///
+ /// Resets the data for the hooks-related portion of the test by ensuring
+ /// that no libraries are loaded and that any marker files are deleted.
+ void reset() {
+ // Unload any previously-loaded libraries.
+ HooksManager::unloadLibraries();
+
+ // Get rid of any marker files.
+ static_cast<void>(unlink(LOAD_MARKER_FILE));
+ static_cast<void>(unlink(UNLOAD_MARKER_FILE));
+ }
};
TEST_F(CtrlDhcpv4SrvTest, commands) {
@@ -82,4 +104,48 @@ TEST_F(CtrlDhcpv4SrvTest, commands) {
EXPECT_EQ(0, rcode); // expect success
}
+// Check that the "libreload" command will reload libraries
+
+TEST_F(CtrlDhcpv4SrvTest, libreload) {
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Load two libraries
+ std::vector<std::string> libraries;
+ libraries.push_back(CALLOUT_LIBRARY_1);
+ libraries.push_back(CALLOUT_LIBRARY_2);
+ HooksManager::loadLibraries(libraries);
+
+ // Check they are loaded.
+ std::vector<std::string> loaded_libraries =
+ HooksManager::getLibraryNames();
+ ASSERT_TRUE(libraries == loaded_libraries);
+
+ // ... which also included checking that the marker file created by the
+ // load functions exists and holds the correct value (of "12" - the
+ // first library appends "1" to the file, the second appends "2"). Also
+ // check that the unload marker file does not yet exist.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Now execute the "libreload" command. This should cause the libraries
+ // to unload and to reload.
+
+ // Use empty parameters list
+ ElementPtr params(new isc::data::MapElement());
+ int rcode = -1;
+
+ ConstElementPtr result =
+ ControlledDhcpv4Srv::execDhcpv4ServerCommand("libreload", params);
+ ConstElementPtr comment = parseAnswer(rcode, result);
+ EXPECT_EQ(0, rcode); // Expect success
+
+ // Check that the libraries have unloaded and reloaded. The libraries are
+ // unloaded in the reverse order to which they are loaded. When they load,
+ // they should append information to the loading marker file.
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1212"));
+}
+
} // End of anonymous namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
index cabd8b7..e930ba0 100644
--- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
+++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,23 +16,34 @@
#include <sstream>
#include <asiolink/io_address.h>
+#include <config/ccsession.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
#include <dhcp/dhcp4.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/option.h>
+#include <dhcp/option_int.h>
#include <dhcp/option4_addrlst.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_int_array.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/pkt_filter.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcp/docsis3_option_defs.h>
#include <dhcp4/dhcp4_srv.h>
#include <dhcp4/dhcp4_log.h>
+#include <dhcp4/config_parser.h>
+#include <hooks/server_hooks.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcpsrv/utils.h>
#include <gtest/gtest.h>
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+#include <config/ccsession.h>
#include <boost/scoped_ptr.hpp>
-#include <fstream>
#include <iostream>
#include <arpa/inet.h>
@@ -40,453 +51,336 @@
using namespace std;
using namespace isc;
using namespace isc::dhcp;
+using namespace isc::data;
using namespace isc::asiolink;
+using namespace isc::hooks;
+using namespace isc::dhcp::test;
namespace {
-class NakedDhcpv4Srv: public Dhcpv4Srv {
- // "Naked" DHCPv4 server, exposes internal fields
-public:
-
- /// @brief Constructor.
- ///
- /// It disables configuration of broadcast options on
- /// sockets that are opened by the Dhcpv4Srv constructor.
- /// Setting broadcast options requires root privileges
- /// which is not the case when running unit tests.
- NakedDhcpv4Srv(uint16_t port = 0)
- : Dhcpv4Srv(port, "type=memfile", false) {
- }
-
- using Dhcpv4Srv::processDiscover;
- using Dhcpv4Srv::processRequest;
- using Dhcpv4Srv::processRelease;
- using Dhcpv4Srv::processDecline;
- using Dhcpv4Srv::processInform;
- using Dhcpv4Srv::getServerID;
- using Dhcpv4Srv::loadServerID;
- using Dhcpv4Srv::generateServerID;
- using Dhcpv4Srv::writeServerID;
- using Dhcpv4Srv::sanityCheck;
- using Dhcpv4Srv::srvidToString;
-};
-
-static const char* SRVID_FILE = "server-id-test.txt";
-
-class Dhcpv4SrvTest : public ::testing::Test {
-public:
-
- /// @brief Constructor
- ///
- /// Initializes common objects used in many tests.
- /// Also sets up initial configuration in CfgMgr.
- Dhcpv4SrvTest() {
- subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1000,
- 2000, 3000));
- pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"), IOAddress("192.0.2.110")));
- subnet_->addPool(pool_);
-
- CfgMgr::instance().deleteSubnets4();
- CfgMgr::instance().addSubnet4(subnet_);
-
- // Add Router option.
- Option4AddrLstPtr opt_routers(new Option4AddrLst(DHO_ROUTERS));
- opt_routers->setAddress(IOAddress("192.0.2.2"));
- subnet_->addOption(opt_routers, false, "dhcp4");
-
- // it's ok if that fails. There should not be such a file anyway
- unlink(SRVID_FILE);
- }
-
- /// @brief Add 'Parameter Request List' option to the packet.
- ///
- /// This function PRL option comprising the following option codes:
- /// - 5 - Name Server
- /// - 15 - Domain Name
- /// - 7 - Log Server
- /// - 8 - Quotes Server
- /// - 9 - LPR Server
- ///
- /// @param pkt packet to add PRL option to.
- void addPrlOption(Pkt4Ptr& pkt) {
-
- OptionUint8ArrayPtr option_prl =
- OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
- DHO_DHCP_PARAMETER_REQUEST_LIST));
-
- // Let's request options that have been configured for the subnet.
- option_prl->addValue(DHO_DOMAIN_NAME_SERVERS);
- option_prl->addValue(DHO_DOMAIN_NAME);
- option_prl->addValue(DHO_LOG_SERVERS);
- option_prl->addValue(DHO_COOKIE_SERVERS);
- // Let's also request the option that hasn't been configured. In such
- // case server should ignore request for this particular option.
- option_prl->addValue(DHO_LPR_SERVERS);
- // And add 'Parameter Request List' option into the DISCOVER packet.
- pkt->addOption(option_prl);
- }
-
- /// @brief Configures options being requested in the PRL option.
- ///
- /// The lpr-servers option is NOT configured here although it is
- /// added to the 'Parameter Request List' option in the
- /// \ref addPrlOption. When requested option is not configured
- /// the server should not return it in its response. The goal
- /// of not configuring the requested option is to verify that
- /// the server will not return it.
- void configureRequestedOptions() {
- // dns-servers
- Option4AddrLstPtr
- option_dns_servers(new Option4AddrLst(DHO_DOMAIN_NAME_SERVERS));
- option_dns_servers->addAddress(IOAddress("192.0.2.1"));
- option_dns_servers->addAddress(IOAddress("192.0.2.100"));
- ASSERT_NO_THROW(subnet_->addOption(option_dns_servers, false, "dhcp4"));
-
- // domain-name
- OptionDefinition def("domain-name", DHO_DOMAIN_NAME, OPT_FQDN_TYPE);
- OptionCustomPtr option_domain_name(new OptionCustom(def, Option::V4));
- option_domain_name->writeFqdn("example.com");
- subnet_->addOption(option_domain_name, false, "dhcp4");
-
- // log-servers
- Option4AddrLstPtr option_log_servers(new Option4AddrLst(DHO_LOG_SERVERS));
- option_log_servers->addAddress(IOAddress("192.0.2.2"));
- option_log_servers->addAddress(IOAddress("192.0.2.10"));
- ASSERT_NO_THROW(subnet_->addOption(option_log_servers, false, "dhcp4"));
-
- // cookie-servers
- Option4AddrLstPtr option_cookie_servers(new Option4AddrLst(DHO_COOKIE_SERVERS));
- option_cookie_servers->addAddress(IOAddress("192.0.2.1"));
- ASSERT_NO_THROW(subnet_->addOption(option_cookie_servers, false, "dhcp4"));
- }
-
- /// @brief checks that the response matches request
- /// @param q query (client's message)
- /// @param a answer (server's message)
- void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a) {
- ASSERT_TRUE(q);
- ASSERT_TRUE(a);
-
- EXPECT_EQ(q->getHops(), a->getHops());
- EXPECT_EQ(q->getIface(), a->getIface());
- EXPECT_EQ(q->getIndex(), a->getIndex());
- EXPECT_EQ(q->getGiaddr(), a->getGiaddr());
-
- // Check that bare minimum of required options are there.
- // We don't check options requested by a client. Those
- // are checked elsewhere.
- EXPECT_TRUE(a->getOption(DHO_SUBNET_MASK));
- EXPECT_TRUE(a->getOption(DHO_ROUTERS));
- EXPECT_TRUE(a->getOption(DHO_DHCP_SERVER_IDENTIFIER));
- EXPECT_TRUE(a->getOption(DHO_DHCP_LEASE_TIME));
- EXPECT_TRUE(a->getOption(DHO_SUBNET_MASK));
- EXPECT_TRUE(a->getOption(DHO_DOMAIN_NAME));
- EXPECT_TRUE(a->getOption(DHO_DOMAIN_NAME_SERVERS));
-
- // Check that something is offered
- EXPECT_TRUE(a->getYiaddr().toText() != "0.0.0.0");
- }
-
- /// @brief Check that requested options are present.
- ///
- /// @param pkt packet to be checked.
- void optionsCheck(const Pkt4Ptr& pkt) {
- // Check that the requested and configured options are returned
- // in the ACK message.
- EXPECT_TRUE(pkt->getOption(DHO_DOMAIN_NAME))
- << "domain-name not present in the response";
- EXPECT_TRUE(pkt->getOption(DHO_DOMAIN_NAME_SERVERS))
- << "dns-servers not present in the response";
- EXPECT_TRUE(pkt->getOption(DHO_LOG_SERVERS))
- << "log-servers not present in the response";
- EXPECT_TRUE(pkt->getOption(DHO_COOKIE_SERVERS))
- << "cookie-servers not present in the response";
- // Check that the requested but not configured options are not
- // returned in the ACK message.
- EXPECT_FALSE(pkt->getOption(DHO_LPR_SERVERS))
- << "domain-name present in the response but it is"
- << " expected not to be present";
- }
-
- /// @brief generates client-id option
- ///
- /// Generate client-id option of specified length
- /// Ids with different lengths are sufficent to generate
- /// unique ids. If more fine grained control is required,
- /// tests generate client-ids on their own.
- /// Sets client_id_ field.
- /// @param size size of the client-id to be generated
- OptionPtr generateClientId(size_t size = 4) {
-
- OptionBuffer clnt_id(size);
- for (int i = 0; i < size; i++) {
- clnt_id[i] = 100 + i;
- }
-
- client_id_ = ClientIdPtr(new ClientId(clnt_id));
-
- return (OptionPtr(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
- clnt_id.begin(),
- clnt_id.begin() + size)));
- }
-
- /// @brief generate hardware address
- ///
- /// @param size size of the generated MAC address
- /// @param pointer to Hardware Address object
- HWAddrPtr generateHWAddr(size_t size = 6) {
- const uint8_t hw_type = 123; // Just a fake number (typically 6=HTYPE_ETHER, see dhcp4.h)
- OptionBuffer mac(size);
- for (int i = 0; i < size; ++i) {
- mac[i] = 50 + i;
- }
- return (HWAddrPtr(new HWAddr(mac, hw_type)));
- }
-
- /// Check that address was returned from proper range, that its lease
- /// lifetime is correct, that T1 and T2 are returned properly
- /// @param rsp response to be checked
- /// @param subnet subnet that should be used to verify assigned address
- /// and options
- /// @param t1_mandatory is T1 mandatory?
- /// @param t2_mandatory is T2 mandatory?
- void checkAddressParams(const Pkt4Ptr& rsp, const SubnetPtr subnet,
- bool t1_mandatory = false,
- bool t2_mandatory = false) {
-
- // Technically inPool implies inRange, but let's be on the safe
- // side and check both.
- EXPECT_TRUE(subnet->inRange(rsp->getYiaddr()));
- EXPECT_TRUE(subnet->inPool(rsp->getYiaddr()));
-
- // Check lease time
- OptionPtr opt = rsp->getOption(DHO_DHCP_LEASE_TIME);
- if (!opt) {
- ADD_FAILURE() << "Lease time option missing in response";
- } else {
- EXPECT_EQ(opt->getUint32(), subnet->getValid());
- }
-
- // Check T1 timer
- opt = rsp->getOption(DHO_DHCP_RENEWAL_TIME);
- if (opt) {
- EXPECT_EQ(opt->getUint32(), subnet->getT1());
- } else {
- if (t1_mandatory) {
- ADD_FAILURE() << "Required T1 option missing";
- }
- }
-
- // Check T2 timer
- opt = rsp->getOption(DHO_DHCP_REBINDING_TIME);
- if (opt) {
- EXPECT_EQ(opt->getUint32(), subnet->getT2());
- } else {
- if (t2_mandatory) {
- ADD_FAILURE() << "Required T2 option missing";
- }
- }
- }
-
- /// @brief Basic checks for generated response (message type and trans-id).
- ///
- /// @param rsp response packet to be validated
- /// @param expected_message_type expected message type
- /// @param expected_transid expected transaction-id
- void checkResponse(const Pkt4Ptr& rsp, uint8_t expected_message_type,
- uint32_t expected_transid) {
- ASSERT_TRUE(rsp);
- EXPECT_EQ(expected_message_type, rsp->getType());
- EXPECT_EQ(expected_transid, rsp->getTransid());
- }
-
- /// @brief Checks if the lease sent to client is present in the database
- ///
- /// @param rsp response packet to be validated
- /// @param client_id expected client-identifier (or NULL)
- /// @param HWAddr expected hardware address (not used now)
- /// @param expected_addr expected address
- Lease4Ptr checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id,
- const HWAddrPtr&, const IOAddress& expected_addr) {
-
- ClientIdPtr id;
- if (client_id) {
- OptionBuffer data = client_id->getData();
- id.reset(new ClientId(data));
- }
-
- Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(expected_addr);
- if (!lease) {
- cout << "Lease for " << expected_addr.toText()
- << " not found in the database backend.";
- return (Lease4Ptr());
- }
-
- EXPECT_EQ(rsp->getYiaddr().toText(), expected_addr.toText());
-
- EXPECT_EQ(expected_addr.toText(), lease->addr_.toText());
- if (client_id) {
- EXPECT_TRUE(*lease->client_id_ == *id);
- }
- EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
-
- return (lease);
- }
-
- /// @brief Checks if server response (OFFER, ACK, NAK) includes proper server-id
- /// @param rsp response packet to be validated
- /// @param expected_srvid expected value of server-id
- void checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid) {
- // Check that server included its server-id
- OptionPtr opt = rsp->getOption(DHO_DHCP_SERVER_IDENTIFIER);
- ASSERT_TRUE(opt);
- EXPECT_EQ(opt->getType(), expected_srvid->getType() );
- EXPECT_EQ(opt->len(), expected_srvid->len() );
- EXPECT_TRUE(opt->getData() == expected_srvid->getData());
- }
-
- /// @brief Checks if server response (OFFER, ACK, NAK) includes proper client-id
- /// @param rsp response packet to be validated
- /// @param expected_clientid expected value of client-id
- void checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid) {
- // check that server included our own client-id
- OptionPtr opt = rsp->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
- ASSERT_TRUE(opt);
- EXPECT_EQ(expected_clientid->getType(), opt->getType());
- EXPECT_EQ(expected_clientid->len(), opt->len());
- EXPECT_TRUE(expected_clientid->getData() == opt->getData());
- }
-
- /// @brief Tests if Discover or Request message is processed correctly
- ///
- /// @param msg_type DHCPDISCOVER or DHCPREQUEST
- /// @param client_addr client address
- /// @param relay_addr relay address
- void testDiscoverRequest(const uint8_t msg_type,
- const IOAddress& client_addr,
- const IOAddress& relay_addr) {
-
- boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
- vector<uint8_t> mac(6);
- for (int i = 0; i < 6; i++) {
- mac[i] = i*10;
- }
-
- boost::shared_ptr<Pkt4> req(new Pkt4(msg_type, 1234));
- boost::shared_ptr<Pkt4> rsp;
-
- req->setIface("eth0");
- req->setIndex(17);
- req->setHWAddr(1, 6, mac);
- req->setRemoteAddr(IOAddress(client_addr));
- req->setGiaddr(relay_addr);
-
- // We are going to test that certain options are returned
- // in the response message when requested using 'Parameter
- // Request List' option. Let's configure those options that
- // are returned when requested.
- configureRequestedOptions();
-
- if (msg_type == DHCPDISCOVER) {
- ASSERT_NO_THROW(
- rsp = srv->processDiscover(req);
- );
-
- // Should return OFFER
- ASSERT_TRUE(rsp);
- EXPECT_EQ(DHCPOFFER, rsp->getType());
-
- } else {
- ASSERT_NO_THROW(
- rsp = srv->processRequest(req);
- );
-
- // Should return ACK
- ASSERT_TRUE(rsp);
- EXPECT_EQ(DHCPACK, rsp->getType());
-
- }
-
- if (relay_addr.toText() != "0.0.0.0") {
- // This is relayed message. It should be sent brsp to relay address.
- EXPECT_EQ(req->getGiaddr().toText(),
- rsp->getRemoteAddr().toText());
-
- } else if (client_addr.toText() != "0.0.0.0") {
- // This is a message from a client having an IP address.
- EXPECT_EQ(req->getRemoteAddr().toText(),
- rsp->getRemoteAddr().toText());
-
- } else {
- // This is a message from a client having no IP address yet.
- // If IfaceMgr supports direct traffic the response should
- // be sent to the new address assigned to the client.
- if (IfaceMgr::instance().isDirectResponseSupported()) {
- EXPECT_EQ(rsp->getYiaddr(),
- rsp->getRemoteAddr().toText());
-
- // If direct response to the client having no IP address is
- // not supported, response should go to broadcast.
- } else {
- EXPECT_EQ("255.255.255.255", rsp->getRemoteAddr().toText());
-
- }
-
- }
-
- messageCheck(req, rsp);
-
- // We did not request any options so these should not be present
- // in the RSP.
- EXPECT_FALSE(rsp->getOption(DHO_LOG_SERVERS));
- EXPECT_FALSE(rsp->getOption(DHO_COOKIE_SERVERS));
- EXPECT_FALSE(rsp->getOption(DHO_LPR_SERVERS));
-
- // Repeat the test but request some options.
- // Add 'Parameter Request List' option.
- addPrlOption(req);
-
- if (msg_type == DHCPDISCOVER) {
- ASSERT_NO_THROW(
- rsp = srv->processDiscover(req);
- );
-
- // Should return non-NULL packet.
- ASSERT_TRUE(rsp);
- EXPECT_EQ(DHCPOFFER, rsp->getType());
-
- } else {
- ASSERT_NO_THROW(
- rsp = srv->processRequest(req);
- );
-
- // Should return non-NULL packet.
- ASSERT_TRUE(rsp);
- EXPECT_EQ(DHCPACK, rsp->getType());
-
- }
+// This test verifies that the destination address of the response
+// message is set to giaddr, when giaddr is set to non-zero address
+// in the received message.
+TEST_F(Dhcpv4SrvFakeIfaceTest, adjustIfaceDataRelay) {
+ // Create the instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr to non-zero address and hops to non-zero value
+ // as if it was relayed.
+ req->setGiaddr(IOAddress("192.0.2.1"));
+ req->setHops(2);
+ // Set ciaddr to zero. This simulates the client which applies
+ // for the new lease.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // Clear broadcast flag.
+ req->setFlags(0x0000);
+
+ // Set local address, port and interface.
+ req->setLocalAddr(IOAddress("192.0.3.1"));
+ req->setLocalPort(1001);
+ req->setIface("eth0");
+ req->setIndex(1);
+
+ // Create a response packet. Assume that the new lease have
+ // been created and new address allocated. This address is
+ // stored in yiaddr field.
+ boost::shared_ptr<Pkt4> resp(new Pkt4(DHCPOFFER, 1234));
+ resp->setYiaddr(IOAddress("192.0.2.100"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Set hops value for the response.
+ resp->setHops(req->getHops());
+
+ // This function never throws.
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ // Now the destination address should be relay's address.
+ EXPECT_EQ("192.0.2.1", resp->getRemoteAddr().toText());
+ // The query has been relayed, so the response must be sent to the port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getRemotePort());
+ // Local address should be copied from the query message.
+ EXPECT_EQ("192.0.3.1", resp->getLocalAddr().toText());
+ // The local port is always DHCPv4 server port 67.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ // We will send response over the same interface which was used to receive
+ // query.
+ EXPECT_EQ("eth0", resp->getIface());
+ EXPECT_EQ(1, resp->getIndex());
+
+ // Let's do another test and set other fields: ciaddr and
+ // flags. By doing it, we want to make sure that the relay
+ // address will take precedence.
+ req->setGiaddr(IOAddress("192.0.2.50"));
+ req->setCiaddr(IOAddress("192.0.2.11"));
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+
+ resp->setYiaddr(IOAddress("192.0.2.100"));
+ // Clear remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ // Response should be sent back to the relay address.
+ EXPECT_EQ("192.0.2.50", resp->getRemoteAddr().toText());
+}
- // Check that the requested options are returned.
- optionsCheck(rsp);
+// This test verifies that the destination address of the response message
+// is set to ciaddr when giaddr is set to zero and the ciaddr is set to
+// non-zero address in the received message. This is the case when the
+// client is in Renew or Rebind state.
+TEST_F(Dhcpv4SrvFakeIfaceTest, adjustIfaceDataRenew) {
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Set ciaddr to non-zero address. The response should be sent to this
+ // address as the client is in renewing or rebinding state (it is fully
+ // configured).
+ req->setCiaddr(IOAddress("192.0.2.15"));
+ // Let's configure broadcast flag. It should be ignored because
+ // we are responding directly to the client having an address
+ // and trying to extend his lease. Broadcast flag is only used
+ // when new lease is acquired and server must make a decision
+ // whether to unicast the response to the acquired address or
+ // broadcast it.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+ // This is a direct message, so the hops should be cleared.
+ req->setHops(0);
+ // Set local unicast address as if we are renewing a lease.
+ req->setLocalAddr(IOAddress("192.0.3.1"));
+ // Request is received on the DHCPv4 server port.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent over the same interface.
+ req->setIface("eth0");
+ req->setIndex(1);
+
+ // Create a response.
+ boost::shared_ptr<Pkt4> resp(new Pkt4(DHCPOFFER, 1234));
+ // Let's extend the lease for the client in such a way that
+ // it will actually get different address. The response
+ // should not be sent to this address but rather to ciaddr
+ // as client still have ciaddr configured.
+ resp->setYiaddr(IOAddress("192.0.2.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops value from the query.
+ resp->setHops(req->getHops());
+
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ // Check that server responds to ciaddr
+ EXPECT_EQ("192.0.2.15", resp->getRemoteAddr().toText());
+ // The query was non-relayed, so the response should be sent to a DHCPv4
+ // client port 68.
+ EXPECT_EQ(DHCP4_CLIENT_PORT, resp->getRemotePort());
+ // The response should be sent from the unicast address on which the
+ // query has been received.
+ EXPECT_EQ("192.0.3.1", resp->getLocalAddr().toText());
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+ // The interface data should match the data in the query.
+ EXPECT_EQ("eth0", resp->getIface());
+ EXPECT_EQ(1, resp->getIndex());
- }
+}
- ~Dhcpv4SrvTest() {
- CfgMgr::instance().deleteSubnets4();
+// This test verifies that the destination address of the response message
+// is set correctly when giaddr and ciaddr is zeroed in the received message
+// and the new lease is acquired. The lease address is carried in the
+// response message in the yiaddr field. In this case destination address
+// of the response should be set to yiaddr if server supports direct responses
+// to the client which doesn't have an address yet or broadcast if the server
+// doesn't support direct responses.
+TEST_F(Dhcpv4SrvFakeIfaceTest, adjustIfaceDataSelect) {
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+
+ // Let's clear the broadcast flag.
+ req->setFlags(0);
+
+ // This is a non-relayed message, so let's clear hops count.
+ req->setHops(0);
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth0");
+ req->setIndex(1);
+
+ // Create a response.
+ boost::shared_ptr<Pkt4> resp(new Pkt4(DHCPOFFER, 1234));
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.2.13"));
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+ // Copy hops count.
+ resp->setHops(req->getHops());
+
+ // We want to test the case, when the server (packet filter) doesn't support
+ // ddirect responses to the client which doesn't have an address yet. In
+ // case, the server should send its response to the broadcast address.
+ // We can control whether the current packet filter returns that its support
+ // direct responses or not.
+ current_pkt_filter_->direct_resp_supported_ = false;
+
+ // When running unit tests, the IfaceMgr is using the default Packet
+ // Filtering class, PktFilterInet. This class does not support direct
+ // responses to clients without address assigned. When giaddr and ciaddr
+ // are zero and client has just got new lease, the assigned address is
+ // carried in yiaddr. In order to send this address to the client,
+ // server must broadcast its response.
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ // Check that the response is sent to broadcast address as the
+ // server doesn't have capability to respond directly.
+ EXPECT_EQ("255.255.255.255", resp->getRemoteAddr().toText());
+
+ // Although the query has been sent to the broadcast address, the
+ // server should select a unicast address on the particular interface
+ // as a source address for the response.
+ EXPECT_EQ("192.0.3.1", resp->getLocalAddr().toText());
+
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+ // The response should be sent via the same interface through which
+ // query has been received.
+ EXPECT_EQ("eth0", resp->getIface());
+ EXPECT_EQ(1, resp->getIndex());
+
+ // We also want to test the case when the server has capability to
+ // respond directly to the client which is not configured. Server
+ // makes decision whether it responds directly or broadcast its
+ // response based on the capability reported by IfaceMgr. We can
+ // control whether the current packet filter returns that it supports
+ // direct responses or not.
+ current_pkt_filter_->direct_resp_supported_ = true;
+
+ // Now we expect that the server will send its response to the
+ // address assigned for the client.
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ EXPECT_EQ("192.0.2.13", resp->getRemoteAddr().toText());
+}
- // Let's clean up if there is such a file.
- unlink(SRVID_FILE);
- };
+// This test verifies that the destination address of the response message
+// is set to broadcast address when client set broadcast flag in its
+// query. Client sets this flag to indicate that it can't receive direct
+// responses from the server when it doesn't have its interface configured.
+// Server must respect broadcast flag.
+TEST_F(Dhcpv4SrvFakeIfaceTest, adjustIfaceDataBroadcast) {
+ // Create instance of the incoming packet.
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // Clear giaddr to simulate direct packet.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth0");
+ req->setIndex(1);
+
+ // Let's set the broadcast flag.
+ req->setFlags(Pkt4::FLAG_BROADCAST_MASK);
+
+ // Create a response.
+ boost::shared_ptr<Pkt4> resp(new Pkt4(DHCPOFFER, 1234));
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.2.13"));
+
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ ASSERT_NO_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp));
+
+ // Server must repond to broadcast address when client desired that
+ // by setting the broadcast flag in its request.
+ EXPECT_EQ("255.255.255.255", resp->getRemoteAddr().toText());
+
+ // Although the query has been sent to the broadcast address, the
+ // server should select a unicast address on the particular interface
+ // as a source address for the response.
+ EXPECT_EQ("192.0.3.1", resp->getLocalAddr().toText());
+
+ // The response should be sent from the DHCPv4 server port.
+ EXPECT_EQ(DHCP4_SERVER_PORT, resp->getLocalPort());
+
+ // The response should be sent via the same interface through which
+ // query has been received.
+ EXPECT_EQ("eth0", resp->getIface());
+ EXPECT_EQ(1, resp->getIndex());
- /// @brief A subnet used in most tests
- Subnet4Ptr subnet_;
+}
- /// @brief A pool used in most tests
- Pool4Ptr pool_;
+// This test verifies that exception is thrown of the invalid combination
+// of giaddr and hops is specified in a client's message.
+TEST_F(Dhcpv4SrvFakeIfaceTest, adjustIfaceDataInvalid) {
+ boost::shared_ptr<Pkt4> req(new Pkt4(DHCPDISCOVER, 1234));
+
+ // The hops and giaddr values are used to determine if the client's
+ // message has been relayed or sent directly. The allowed combinations
+ // are (giaddr = 0 and hops = 0) or (giaddr != 0 and hops != 0). Any
+ // other combination is invalid and the adjustIfaceData should throw
+ // an exception. We will test that exception is indeed thrown.
+ req->setGiaddr(IOAddress("0.0.0.0"));
+ req->setHops(1);
+
+ // Clear client address as it hasn't got any address configured yet.
+ req->setCiaddr(IOAddress("0.0.0.0"));
+ // The query is sent to the broadcast address in the Select state.
+ req->setLocalAddr(IOAddress("255.255.255.255"));
+ // The query has been received on the DHCPv4 server port 67.
+ req->setLocalPort(DHCP4_SERVER_PORT);
+ // Set the interface. The response should be sent via the same interface.
+ req->setIface("eth0");
+ req->setIndex(1);
+
+ // Create a response.
+ boost::shared_ptr<Pkt4> resp(new Pkt4(DHCPOFFER, 1234));
+ // Assign some new address for this client.
+ resp->setYiaddr(IOAddress("192.0.2.13"));
+
+ // Clear the remote address.
+ resp->setRemoteAddr(IOAddress("0.0.0.0"));
+
+ EXPECT_THROW(NakedDhcpv4Srv::adjustIfaceData(req, resp), isc::BadValue);
+}
- /// @brief A client-id used in most tests
- ClientIdPtr client_id_;
-};
+// This test verifies that the server identifier option is appended to
+// a specified DHCPv4 message and the server identifier is correct.
+TEST_F(Dhcpv4SrvTest, appendServerID) {
+ Pkt4Ptr response(new Pkt4(DHCPDISCOVER, 1234));
+ // Set a local address. It is required by the function under test
+ // to create the Server Identifier option.
+ response->setLocalAddr(IOAddress("192.0.3.1"));
+
+ // Append the Server Identifier.
+ ASSERT_NO_THROW(NakedDhcpv4Srv::appendServerID(response));
+
+ // Make sure that the option has been added.
+ OptionPtr opt = response->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(opt);
+ Option4AddrLstPtr opt_server_id =
+ boost::dynamic_pointer_cast<Option4AddrLst>(opt);
+ ASSERT_TRUE(opt_server_id);
+
+ // The option is represented as a list of IPv4 addresses but with
+ // only one address added.
+ Option4AddrLst::AddressContainer addrs = opt_server_id->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ // This address should match the local address of the packet.
+ EXPECT_EQ("192.0.3.1", addrs[0].toText());
+}
// Sanity check. Verifies that both Dhcpv4Srv and its derived
// class NakedDhcpv4Srv can be instantiated and destroyed.
@@ -494,20 +388,46 @@ TEST_F(Dhcpv4SrvTest, basic) {
// Check that the base class can be instantiated
boost::scoped_ptr<Dhcpv4Srv> srv;
- ASSERT_NO_THROW(srv.reset(new Dhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
+ ASSERT_NO_THROW(srv.reset(new Dhcpv4Srv(DHCP4_SERVER_PORT + 10000, "type=memfile",
+ false, false)));
srv.reset();
+ // We have to close open sockets because further in this test we will
+ // call the Dhcpv4Srv constructor again. This constructor will try to
+ // set the appropriate packet filter class for IfaceMgr. This requires
+ // that all sockets are closed.
+ IfaceMgr::instance().closeSockets();
// Check that the derived class can be instantiated
boost::scoped_ptr<NakedDhcpv4Srv> naked_srv;
ASSERT_NO_THROW(
- naked_srv.reset(new NakedDhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
- EXPECT_TRUE(naked_srv->getServerID());
+ naked_srv.reset(new NakedDhcpv4Srv(DHCP4_SERVER_PORT + 10000)));
+ // Close sockets again for the next test.
+ IfaceMgr::instance().closeSockets();
ASSERT_NO_THROW(naked_srv.reset(new NakedDhcpv4Srv(0)));
- EXPECT_TRUE(naked_srv->getServerID());
}
-// Verifies that DISCOVER received via relay can be processed correctly,
+// This test verifies that exception is not thrown when an error occurs during
+// opening sockets. This test forces an error by adding a fictious interface
+// to the IfaceMgr. An attempt to open socket on this interface must always
+// fail. The DHCPv4 installs the error handler function to prevent exceptions
+// being thrown from the openSockets4 function.
+// @todo The server tests for socket should be extended but currently the
+// ability to unit test the sockets code is somewhat limited.
+TEST_F(Dhcpv4SrvTest, openActiveSockets) {
+ ASSERT_NO_THROW(CfgMgr::instance().activateAllIfaces());
+
+ Iface iface("bogusiface", 255);
+ iface.flag_loopback_ = false;
+ iface.flag_up_ = true;
+ iface.flag_running_ = true;
+ iface.inactive4_ = false;
+ iface.addAddress(IOAddress("192.0.0.0"));
+ IfaceMgr::instance().addInterface(iface);
+ ASSERT_NO_THROW(Dhcpv4Srv::openActiveSockets(DHCP4_SERVER_PORT, false));
+}
+
+// Verifies that DISCOVER message can be processed correctly,
// that the OFFER message generated in response is valid and
// contains necessary options.
//
@@ -515,29 +435,11 @@ TEST_F(Dhcpv4SrvTest, basic) {
// are other tests that verify correctness of the allocation
// engine. See DiscoverBasic, DiscoverHint, DiscoverNoClientId
// and DiscoverInvalidHint.
-TEST_F(Dhcpv4SrvTest, processDiscoverRelay) {
- testDiscoverRequest(DHCPDISCOVER,
- IOAddress("192.0.2.56"),
- IOAddress("192.0.2.67"));
-}
-
-// Verifies that the non-relayed DISCOVER is processed correctly when
-// client source address is specified.
-TEST_F(Dhcpv4SrvTest, processDiscoverNoRelay) {
- testDiscoverRequest(DHCPDISCOVER,
- IOAddress("0.0.0.0"),
- IOAddress("192.0.2.67"));
-}
-
-// Verified that the non-relayed DISCOVER is processed correctly when
-// client source address is not specified.
-TEST_F(Dhcpv4SrvTest, processDiscoverNoClientAddr) {
- testDiscoverRequest(DHCPDISCOVER,
- IOAddress("0.0.0.0"),
- IOAddress("0.0.0.0"));
+TEST_F(Dhcpv4SrvFakeIfaceTest, processDiscover) {
+ testDiscoverRequest(DHCPDISCOVER);
}
-// Verifies that REQUEST received via relay can be processed correctly,
+// Verifies that REQUEST message can be processed correctly,
// that the OFFER message generated in response is valid and
// contains necessary options.
//
@@ -545,29 +447,11 @@ TEST_F(Dhcpv4SrvTest, processDiscoverNoClientAddr) {
// are other tests that verify correctness of the allocation
// engine. See DiscoverBasic, DiscoverHint, DiscoverNoClientId
// and DiscoverInvalidHint.
-TEST_F(Dhcpv4SrvTest, processRequestRelay) {
- testDiscoverRequest(DHCPREQUEST,
- IOAddress("192.0.2.56"),
- IOAddress("192.0.2.67"));
-}
-
-// Verifies that the non-relayed REQUEST is processed correctly when
-// client source address is specified.
-TEST_F(Dhcpv4SrvTest, processRequestNoRelay) {
- testDiscoverRequest(DHCPREQUEST,
- IOAddress("0.0.0.0"),
- IOAddress("192.0.2.67"));
-}
-
-// Verified that the non-relayed REQUEST is processed correctly when
-// client source address is not specified.
-TEST_F(Dhcpv4SrvTest, processRequestNoClientAddr) {
- testDiscoverRequest(DHCPREQUEST,
- IOAddress("0.0.0.0"),
- IOAddress("0.0.0.0"));
+TEST_F(Dhcpv4SrvFakeIfaceTest, processRequest) {
+ testDiscoverRequest(DHCPREQUEST);
}
-TEST_F(Dhcpv4SrvTest, processRelease) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, processRelease) {
NakedDhcpv4Srv srv;
Pkt4Ptr pkt(new Pkt4(DHCPRELEASE, 1234));
@@ -575,7 +459,7 @@ TEST_F(Dhcpv4SrvTest, processRelease) {
EXPECT_NO_THROW(srv.processRelease(pkt));
}
-TEST_F(Dhcpv4SrvTest, processDecline) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, processDecline) {
NakedDhcpv4Srv srv;
Pkt4Ptr pkt(new Pkt4(DHCPDECLINE, 1234));
@@ -583,7 +467,7 @@ TEST_F(Dhcpv4SrvTest, processDecline) {
EXPECT_NO_THROW(srv.processDecline(pkt));
}
-TEST_F(Dhcpv4SrvTest, processInform) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, processInform) {
NakedDhcpv4Srv srv;
Pkt4Ptr pkt(new Pkt4(DHCPINFORM, 1234));
@@ -640,7 +524,7 @@ TEST_F(Dhcpv4SrvTest, serverReceivedPacketName) {
// - copy of client-id
// - server-id
// - offered address
-TEST_F(Dhcpv4SrvTest, DiscoverBasic) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, DiscoverBasic) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -648,6 +532,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverBasic) {
dis->setRemoteAddr(IOAddress("192.0.2.1"));
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
+ dis->setIface("eth0");
// Pass it to the server and get an offer
Pkt4Ptr offer = srv->processDiscover(dis);
@@ -677,7 +562,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverBasic) {
// - copy of client-id
// - server-id
// - offered address
-TEST_F(Dhcpv4SrvTest, DiscoverHint) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, DiscoverHint) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
IOAddress hint("192.0.2.107");
@@ -687,6 +572,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverHint) {
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
dis->setYiaddr(hint);
+ dis->setIface("eth0");
// Pass it to the server and get an offer
Pkt4Ptr offer = srv->processDiscover(dis);
@@ -698,7 +584,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverHint) {
// lifetime is correct, that T1 and T2 are returned properly
checkAddressParams(offer, subnet_);
- EXPECT_EQ(offer->getYiaddr().toText(), hint.toText());
+ EXPECT_EQ(offer->getYiaddr(), hint);
// Check identifiers
checkServerId(offer, srv->getServerID());
@@ -717,7 +603,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverHint) {
// - copy of client-id
// - server-id
// - offered address
-TEST_F(Dhcpv4SrvTest, DiscoverNoClientId) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, DiscoverNoClientId) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
IOAddress hint("192.0.2.107");
@@ -725,6 +611,8 @@ TEST_F(Dhcpv4SrvTest, DiscoverNoClientId) {
Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
dis->setRemoteAddr(IOAddress("192.0.2.1"));
dis->setYiaddr(hint);
+ dis->setHWAddr(generateHWAddr(6));
+ dis->setIface("eth0");
// Pass it to the server and get an offer
Pkt4Ptr offer = srv->processDiscover(dis);
@@ -736,7 +624,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverNoClientId) {
// lifetime is correct, that T1 and T2 are returned properly
checkAddressParams(offer, subnet_);
- EXPECT_EQ(offer->getYiaddr().toText(), hint.toText());
+ EXPECT_EQ(offer->getYiaddr(), hint);
// Check identifiers
checkServerId(offer, srv->getServerID());
@@ -754,7 +642,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverNoClientId) {
// - copy of client-id
// - server-id
// - offered address (!= hint)
-TEST_F(Dhcpv4SrvTest, DiscoverInvalidHint) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, DiscoverInvalidHint) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
IOAddress hint("10.1.2.3");
@@ -764,6 +652,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverInvalidHint) {
OptionPtr clientid = generateClientId();
dis->addOption(clientid);
dis->setYiaddr(hint);
+ dis->setIface("eth0");
// Pass it to the server and get an offer
Pkt4Ptr offer = srv->processDiscover(dis);
@@ -775,7 +664,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverInvalidHint) {
// lifetime is correct, that T1 and T2 are returned properly
checkAddressParams(offer, subnet_);
- EXPECT_NE(offer->getYiaddr().toText(), hint.toText());
+ EXPECT_NE(offer->getYiaddr(), hint);
// Check identifiers
checkServerId(offer, srv->getServerID());
@@ -792,7 +681,7 @@ TEST_F(Dhcpv4SrvTest, DiscoverInvalidHint) {
// and this is a correct behavior. It is REQUEST that will fail for the third
// client. OFFER is basically saying "if you send me a request, you will
// probably get an address like this" (there are no guarantees).
-TEST_F(Dhcpv4SrvTest, ManyDiscovers) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, ManyDiscovers) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -804,6 +693,11 @@ TEST_F(Dhcpv4SrvTest, ManyDiscovers) {
dis2->setRemoteAddr(IOAddress("192.0.2.2"));
dis3->setRemoteAddr(IOAddress("192.0.2.3"));
+ // Assign interfaces
+ dis1->setIface("eth0");
+ dis2->setIface("eth0");
+ dis3->setIface("eth0");
+
// Different client-id sizes
OptionPtr clientid1 = generateClientId(4); // length 4
OptionPtr clientid2 = generateClientId(5); // length 5
@@ -841,12 +735,39 @@ TEST_F(Dhcpv4SrvTest, ManyDiscovers) {
checkClientId(offer3, clientid3);
// Finally check that the addresses offered are different
- EXPECT_NE(addr1.toText(), addr2.toText());
- EXPECT_NE(addr2.toText(), addr3.toText());
- EXPECT_NE(addr3.toText(), addr1.toText());
- cout << "Offered address to client1=" << addr1.toText() << endl;
- cout << "Offered address to client2=" << addr2.toText() << endl;
- cout << "Offered address to client3=" << addr3.toText() << endl;
+ EXPECT_NE(addr1, addr2);
+ EXPECT_NE(addr2, addr3);
+ EXPECT_NE(addr3, addr1);
+ cout << "Offered address to client1=" << addr1 << endl;
+ cout << "Offered address to client2=" << addr2 << endl;
+ cout << "Offered address to client3=" << addr3 << endl;
+}
+
+// Checks whether echoing back client-id is controllable, i.e.
+// whether the server obeys echo-client-id and sends (or not)
+// client-id
+TEST_F(Dhcpv4SrvFakeIfaceTest, discoverEchoClientId) {
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth0");
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+ checkClientId(offer, clientid);
+
+ CfgMgr::instance().echoClientId(false);
+ offer = srv.processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+ checkClientId(offer, clientid);
}
// This test verifies that incoming REQUEST can be handled properly, that an
@@ -864,7 +785,7 @@ TEST_F(Dhcpv4SrvTest, ManyDiscovers) {
// - assigned address
//
// Test verifies that the lease is actually in the database.
-TEST_F(Dhcpv4SrvTest, RequestBasic) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, RequestBasic) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -874,13 +795,14 @@ TEST_F(Dhcpv4SrvTest, RequestBasic) {
OptionPtr clientid = generateClientId();
req->addOption(clientid);
req->setYiaddr(hint);
+ req->setIface("eth0");
// Pass it to the server and get an advertise
Pkt4Ptr ack = srv->processRequest(req);
// Check if we get response at all
checkResponse(ack, DHCPACK, 1234);
- EXPECT_EQ(hint.toText(), ack->getYiaddr().toText());
+ EXPECT_EQ(hint, ack->getYiaddr());
// Check that address was returned from proper range, that its lease
// lifetime is correct, that T1 and T2 are returned properly
@@ -909,7 +831,7 @@ TEST_F(Dhcpv4SrvTest, RequestBasic) {
// - copy of client-id
// - server-id
// - assigned address (different for each client)
-TEST_F(Dhcpv4SrvTest, ManyRequests) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, ManyRequests) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -927,6 +849,11 @@ TEST_F(Dhcpv4SrvTest, ManyRequests) {
req2->setRemoteAddr(relay);
req3->setRemoteAddr(relay);
+ // Assign interfaces
+ req1->setIface("eth0");
+ req2->setIface("eth0");
+ req3->setIface("eth0");
+
req1->setYiaddr(req_addr1);
req2->setYiaddr(req_addr2);
req3->setYiaddr(req_addr3);
@@ -959,9 +886,9 @@ TEST_F(Dhcpv4SrvTest, ManyRequests) {
IOAddress addr3 = ack3->getYiaddr();
// Check that every client received the address it requested
- EXPECT_EQ(req_addr1.toText(), addr1.toText());
- EXPECT_EQ(req_addr2.toText(), addr2.toText());
- EXPECT_EQ(req_addr3.toText(), addr3.toText());
+ EXPECT_EQ(req_addr1, addr1);
+ EXPECT_EQ(req_addr2, addr2);
+ EXPECT_EQ(req_addr3, addr3);
// Check that the assigned address is indeed from the configured pool
checkAddressParams(ack1, subnet_);
@@ -983,12 +910,37 @@ TEST_F(Dhcpv4SrvTest, ManyRequests) {
l = checkLease(ack3, clientid3, req3->getHWAddr(), addr3);
// Finally check that the addresses offered are different
- EXPECT_NE(addr1.toText(), addr2.toText());
- EXPECT_NE(addr2.toText(), addr3.toText());
- EXPECT_NE(addr3.toText(), addr1.toText());
- cout << "Offered address to client1=" << addr1.toText() << endl;
- cout << "Offered address to client2=" << addr2.toText() << endl;
- cout << "Offered address to client3=" << addr3.toText() << endl;
+ EXPECT_NE(addr1, addr2);
+ EXPECT_NE(addr2, addr3);
+ EXPECT_NE(addr3, addr1);
+ cout << "Offered address to client1=" << addr1 << endl;
+ cout << "Offered address to client2=" << addr2 << endl;
+ cout << "Offered address to client3=" << addr3 << endl;
+}
+
+// Checks whether echoing back client-id is controllable
+TEST_F(Dhcpv4SrvFakeIfaceTest, requestEchoClientId) {
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setIface("eth0");
+
+ // Pass it to the server and get ACK
+ Pkt4Ptr ack = srv.processRequest(dis);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+ checkClientId(ack, clientid);
+
+ CfgMgr::instance().echoClientId(false);
+ ack = srv.processDiscover(dis);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPOFFER, 1234);
+ checkClientId(ack, clientid);
}
@@ -1001,7 +953,7 @@ TEST_F(Dhcpv4SrvTest, ManyRequests) {
// - returned REPLY message has server-id
// - returned REPLY message has IA that includes IAADDR
// - lease is actually renewed in LeaseMgr
-TEST_F(Dhcpv4SrvTest, RenewBasic) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, RenewBasic) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -1015,7 +967,7 @@ TEST_F(Dhcpv4SrvTest, RenewBasic) {
OptionPtr clientid = generateClientId();
// Check that the address we are about to use is indeed in pool
- ASSERT_TRUE(subnet_->inPool(addr));
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
// let's create a lease and put it in the LeaseMgr
uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
@@ -1041,6 +993,7 @@ TEST_F(Dhcpv4SrvTest, RenewBasic) {
req->setRemoteAddr(IOAddress(addr));
req->setYiaddr(addr);
req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
req->addOption(clientid);
req->addOption(srv->getServerID());
@@ -1050,7 +1003,7 @@ TEST_F(Dhcpv4SrvTest, RenewBasic) {
// Check if we get response at all
checkResponse(ack, DHCPACK, 1234);
- EXPECT_EQ(addr.toText(), ack->getYiaddr().toText());
+ EXPECT_EQ(addr, ack->getYiaddr());
// Check that address was returned from proper range, that its lease
// lifetime is correct, that T1 and T2 are returned properly
@@ -1078,6 +1031,56 @@ TEST_F(Dhcpv4SrvTest, RenewBasic) {
EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
}
+// This test verifies that the logic which matches server identifier in the
+// received message with server identifiers used by a server works correctly:
+// - a message with no server identifier is accepted,
+// - a message with a server identifier which matches one of the server
+// identifiers used by a server is accepted,
+// - a message with a server identifier which doesn't match any server
+// identifier used by a server, is not accepted.
+TEST_F(Dhcpv4SrvFakeIfaceTest, acceptServerId) {
+ NakedDhcpv4Srv srv(0);
+
+ Pkt4Ptr pkt(new Pkt4(DHCPREQUEST, 1234));
+ // If no server identifier option is present, the message is always
+ // accepted.
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Create definition of the server identifier option.
+ OptionDefinition def("server-identifier", DHO_DHCP_SERVER_IDENTIFIER,
+ "ipv4-address", false);
+
+ // Add a server identifier option which doesn't match server ids being
+ // used by the server. The accepted server ids are the IPv4 addresses
+ // configured on the interfaces. The 10.1.2.3 is not configured on
+ // any interfaces.
+ OptionCustomPtr other_serverid(new OptionCustom(def, Option::V6));
+ other_serverid->writeAddress(IOAddress("10.1.2.3"));
+ pkt->addOption(other_serverid);
+ EXPECT_FALSE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on eth0 interface.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr eth0_serverid(new OptionCustom(def, Option::V6));
+ eth0_serverid->writeAddress(IOAddress("192.0.3.1"));
+ ASSERT_NO_THROW(pkt->addOption(eth0_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+ // Remove the server identifier.
+ ASSERT_NO_THROW(pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Add a server id being an IPv4 address configured on eth1 interface.
+ // A DHCPv4 message holding this server identifier should be accepted.
+ OptionCustomPtr eth1_serverid(new OptionCustom(def, Option::V6));
+ eth1_serverid->writeAddress(IOAddress("10.0.0.1"));
+ ASSERT_NO_THROW(pkt->addOption(eth1_serverid));
+ EXPECT_TRUE(srv.acceptServerId(pkt));
+
+}
+
// @todo: Implement tests for rejecting renewals
// This test verifies if the sanityCheck() really checks options presence.
@@ -1086,27 +1089,35 @@ TEST_F(Dhcpv4SrvTest, sanityCheck) {
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ pkt->setHWAddr(generateHWAddr(6));
- // Client-id is optional for information-request, so
- EXPECT_NO_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::OPTIONAL));
+ // Server-id is optional for information-request, so
+ EXPECT_NO_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::OPTIONAL));
// Empty packet, no server-id
- EXPECT_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::MANDATORY), RFCViolation);
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY),
+ RFCViolation);
pkt->addOption(srv->getServerID());
// Server-id is mandatory and present = no exception
- EXPECT_NO_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::MANDATORY));
+ EXPECT_NO_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY));
// Server-id is forbidden, but present => exception
- EXPECT_THROW(srv->sanityCheck(pkt, Dhcpv4Srv::FORBIDDEN),
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::FORBIDDEN),
+ RFCViolation);
+
+ // There's no client-id and no HWADDR. Server needs something to
+ // identify the client
+ pkt->setHWAddr(generateHWAddr(0));
+ EXPECT_THROW(NakedDhcpv4Srv::sanityCheck(pkt, Dhcpv4Srv::MANDATORY),
RFCViolation);
}
// This test verifies that incoming (positive) RELEASE can be handled properly.
// As there is no REPLY in DHCPv4, the only thing to verify here is that
// the lease is indeed removed from the database.
-TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, ReleaseBasic) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -1120,7 +1131,7 @@ TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
OptionPtr clientid = generateClientId();
// Check that the address we are about to use is indeed in pool
- ASSERT_TRUE(subnet_->inPool(addr));
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
// Let's create a lease and put it in the LeaseMgr
uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
@@ -1139,10 +1150,11 @@ TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
// Generate client-id also duid_
Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
rel->setRemoteAddr(addr);
- rel->setYiaddr(addr);
+ rel->setCiaddr(addr);
rel->addOption(clientid);
rel->addOption(srv->getServerID());
rel->setHWAddr(hw);
+ rel->setIface("eth0");
// Pass it to the server and hope for a REPLY
// Note: this is no response to RELEASE in DHCPv4
@@ -1153,18 +1165,16 @@ TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
EXPECT_FALSE(l);
// Try to get the lease by hardware address
- // @todo: Uncomment this once trac2592 is implemented
- // Lease4Collection leases = LeaseMgrFactory::instance().getLease4(hw->hwaddr_);
- // EXPECT_EQ(leases.size(), 0);
+ Lease4Collection leases = LeaseMgrFactory::instance().getLease4(hw->hwaddr_);
+ EXPECT_EQ(leases.size(), 0);
// Try to get it by hw/subnet_id combination
l = LeaseMgrFactory::instance().getLease4(hw->hwaddr_, subnet_->getID());
EXPECT_FALSE(l);
// Try by client-id
- // @todo: Uncomment this once trac2592 is implemented
- //Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*client_id_);
- //EXPECT_EQ(leases.size(), 0);
+ leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ EXPECT_EQ(leases.size(), 0);
// Try by client-id/subnet-id
l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
@@ -1179,7 +1189,7 @@ TEST_F(Dhcpv4SrvTest, ReleaseBasic) {
// 1. there is no such lease at all
// 2. there is such a lease, but it is assigned to a different IAID
// 3. there is such a lease, but it belongs to a different client
-TEST_F(Dhcpv4SrvTest, ReleaseReject) {
+TEST_F(Dhcpv4SrvFakeIfaceTest, ReleaseReject) {
boost::scoped_ptr<NakedDhcpv4Srv> srv;
ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
@@ -1198,16 +1208,17 @@ TEST_F(Dhcpv4SrvTest, ReleaseReject) {
OptionPtr clientid = generateClientId();
// Check that the address we are about to use is indeed in pool
- ASSERT_TRUE(subnet_->inPool(addr));
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
// Let's create a RELEASE
// Generate client-id also duid_
Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
rel->setRemoteAddr(addr);
- rel->setYiaddr(addr);
+ rel->setCiaddr(addr);
rel->addOption(clientid);
rel->addOption(srv->getServerID());
rel->setHWAddr(bogus_hw);
+ rel->setIface("eth0");
// Case 1: No lease known to server
SCOPED_TRACE("CASE 1: Lease is not known to the server");
@@ -1266,34 +1277,1928 @@ TEST_F(Dhcpv4SrvTest, ReleaseReject) {
EXPECT_FALSE(l);
}
-// This test verifies if the server-id disk operations (read, write) are
-// working properly.
-TEST_F(Dhcpv4SrvTest, ServerID) {
+// Checks if received relay agent info option is echoed back to the client
+TEST_F(Dhcpv4SrvFakeIfaceTest, relayAgentInfoEcho) {
+
+ NakedDhcpv4Srv srv(0);
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a reposonse
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ OptionPtr rai_query = dis->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_query);
+
+ // Get Relay Agent Info from response...
+ OptionPtr rai_response = offer->getOption(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_response);
+
+ EXPECT_TRUE(rai_response->equal(rai_query));
+}
+
+/// @todo move vendor options tests to a separate file.
+/// @todo Add more extensive vendor options tests, including multiple
+/// vendor options
+
+// Checks if vendor options are parsed correctly and requested vendor options
+// are echoed back.
+TEST_F(Dhcpv4SrvFakeIfaceTest, vendorOptionsDocsis) {
+
+ NakedDhcpv4Srv srv(0);
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 2,"
+ " \"data\": \"10.253.175.16\","
+ " \"csv-format\": True"
+ " }],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"10.254.226.0/25\" ],"
+ " \"subnet\": \"10.254.226.0/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000,"
+ " \"interface\": \"eth0\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // added option 82 (relay agent info) with 3 suboptions. The server
+ // is supposed to echo it back in its response.
+ Pkt4Ptr dis;
+ ASSERT_NO_THROW(dis = captureRelayedDiscover());
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv.run();
+
+ // Check that the server did send a reposonse
+ ASSERT_EQ(1, srv.fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv.fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get Relay Agent Info from query...
+ OptionPtr vendor_opt_response = offer->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(vendor_opt_response);
+
+ // Check if it's of a correct type
+ boost::shared_ptr<OptionVendor> vendor_opt =
+ boost::dynamic_pointer_cast<OptionVendor>(vendor_opt_response);
+ ASSERT_TRUE(vendor_opt);
+
+ // Get Relay Agent Info from response...
+ OptionPtr tftp_servers_generic = vendor_opt->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(tftp_servers_generic);
+
+ Option4AddrLstPtr tftp_servers =
+ boost::dynamic_pointer_cast<Option4AddrLst>(tftp_servers_generic);
+
+ ASSERT_TRUE(tftp_servers);
+
+ Option4AddrLst::AddressContainer addrs = tftp_servers->getAddresses();
+ ASSERT_EQ(1, addrs.size());
+ EXPECT_EQ("10.253.175.16", addrs[0].toText());
+}
+
+
+/// @todo Implement tests for subnetSelect See tests in dhcp6_srv_unittest.cc:
+/// selectSubnetAddr, selectSubnetIface, selectSubnetRelayLinkaddr,
+/// selectSubnetRelayInterfaceId. Note that the concept of interface-id is not
+/// present in the DHCPv4, so not everything is applicable directly.
+/// See ticket #3057
+
+// Checks if hooks are registered properly.
+TEST_F(Dhcpv4SrvTest, Hooks) {
+ NakedDhcpv4Srv srv(0);
+
+ // check if appropriate hooks are registered
+ int hook_index_buffer4_receive = -1;
+ int hook_index_pkt4_receive = -1;
+ int hook_index_select_subnet = -1;
+ int hook_index_lease4_release = -1;
+ int hook_index_pkt4_send = -1;
+ int hook_index_buffer4_send = -1;
+
+ // check if appropriate indexes are set
+ EXPECT_NO_THROW(hook_index_buffer4_receive = ServerHooks::getServerHooks()
+ .getIndex("buffer4_receive"));
+ EXPECT_NO_THROW(hook_index_pkt4_receive = ServerHooks::getServerHooks()
+ .getIndex("pkt4_receive"));
+ EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+ .getIndex("subnet4_select"));
+ EXPECT_NO_THROW(hook_index_lease4_release = ServerHooks::getServerHooks()
+ .getIndex("lease4_release"));
+ EXPECT_NO_THROW(hook_index_pkt4_send = ServerHooks::getServerHooks()
+ .getIndex("pkt4_send"));
+ EXPECT_NO_THROW(hook_index_buffer4_send = ServerHooks::getServerHooks()
+ .getIndex("buffer4_send"));
+
+ EXPECT_TRUE(hook_index_buffer4_receive > 0);
+ EXPECT_TRUE(hook_index_pkt4_receive > 0);
+ EXPECT_TRUE(hook_index_select_subnet > 0);
+ EXPECT_TRUE(hook_index_lease4_release > 0);
+ EXPECT_TRUE(hook_index_pkt4_send > 0);
+ EXPECT_TRUE(hook_index_buffer4_send > 0);
+}
+
+// This test verifies that the following option structure can be parsed:
+// - option (option space 'foobar')
+// - sub option (option space 'foo')
+// - sub option (option space 'bar')
+// @todo Add more thorough unit tests for unpackOptions.
+TEST_F(Dhcpv4SrvTest, unpackOptions) {
+ // Create option definition for each level of encapsulation. Each option
+ // definition is for the option code 1. Options may have the same
+ // option code because they belong to different option spaces.
+
+ // Top level option encapsulates options which belong to 'space-foo'.
+ OptionDefinitionPtr opt_def(new OptionDefinition("option-foobar", 1, "uint32",
+ "space-foo"));\
+ // Middle option encapsulates options which belong to 'space-bar'
+ OptionDefinitionPtr opt_def2(new OptionDefinition("option-foo", 1, "uint16",
+ "space-bar"));
+ // Low level option doesn't encapsulate any option space.
+ OptionDefinitionPtr opt_def3(new OptionDefinition("option-bar", 1,
+ "uint8"));
+
+ // Add option definitions to the Configuration Manager. Each goes under
+ // different option space.
+ CfgMgr& cfgmgr = CfgMgr::instance();
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def, "space-foobar"));
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def2, "space-foo"));
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def3, "space-bar"));
+
+ // Create the buffer holding the structure of options.
+ const char raw_data[] = {
+ // First option starts here.
+ 0x01, // option code = 1
+ 0x0B, // option length = 11
+ 0x00, 0x01, 0x02, 0x03, // This option carries uint32 value
+ // Sub option starts here.
+ 0x01, // option code = 1
+ 0x05, // option length = 5
+ 0x01, 0x02, // this option carries uint16 value
+ // Last option starts here.
+ 0x01, // option code = 1
+ 0x01, // option length = 1
+ 0x00 // This option carries a single uint8
+ // value and has no sub options.
+ };
+ OptionBuffer buf(raw_data, raw_data + sizeof(raw_data));
+
+ // Parse options.
+ NakedDhcpv4Srv srv(0);
+ OptionCollection options;
+ ASSERT_NO_THROW(srv.unpackOptions(buf, "space-foobar", options));
+
+ // There should be one top level option.
+ ASSERT_EQ(1, options.size());
+ boost::shared_ptr<OptionInt<uint32_t> > option_foobar =
+ boost::dynamic_pointer_cast<OptionInt<uint32_t> >(options.begin()->
+ second);
+ ASSERT_TRUE(option_foobar);
+ EXPECT_EQ(1, option_foobar->getType());
+ EXPECT_EQ(0x00010203, option_foobar->getValue());
+ // There should be a middle level option held in option_foobar.
+ boost::shared_ptr<OptionInt<uint16_t> > option_foo =
+ boost::dynamic_pointer_cast<OptionInt<uint16_t> >(option_foobar->
+ getOption(1));
+ ASSERT_TRUE(option_foo);
+ EXPECT_EQ(1, option_foo->getType());
+ EXPECT_EQ(0x0102, option_foo->getValue());
+ // Finally, there should be a low level option under option_foo.
+ boost::shared_ptr<OptionInt<uint8_t> > option_bar =
+ boost::dynamic_pointer_cast<OptionInt<uint8_t> >(option_foo->getOption(1));
+ ASSERT_TRUE(option_bar);
+ EXPECT_EQ(1, option_bar->getType());
+ EXPECT_EQ(0x0, option_bar->getValue());
+}
+
+// Checks whether the server uses default (0.0.0.0) siaddr value, unless
+// explicitly specified
+TEST_F(Dhcpv4SrvFakeIfaceTest, siaddrDefault) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+ IOAddress hint("192.0.2.107");
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ dis->setYiaddr(hint);
+ dis->setIface("eth0");
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Verify that it is 0.0.0.0
+ EXPECT_EQ("0.0.0.0", offer->getSiaddr().toText());
+}
+
+// Checks whether the server uses specified siaddr value
+TEST_F(Dhcpv4SrvFakeIfaceTest, siaddr) {
+ boost::scoped_ptr<NakedDhcpv4Srv> srv;
+ ASSERT_NO_THROW(srv.reset(new NakedDhcpv4Srv(0)));
+ subnet_->setSiaddr(IOAddress("192.0.2.123"));
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv->processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if we get response at all
+ checkResponse(offer, DHCPOFFER, 1234);
+
+ // Verify that its value is proper
+ EXPECT_EQ("192.0.2.123", offer->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as global value is overridden by subnet
+// specific value and returned in server messages. There's also similar test for
+// checking parser only configuration, see Dhcp4ParserTest.nextServerOverride in
+// config_parser_unittest.cc.
+TEST_F(Dhcpv4SrvFakeIfaceTest, nextServerOverride) {
+
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"next-server\": \"1.2.3.4\", "
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+
+ EXPECT_EQ("1.2.3.4", offer->getSiaddr().toText());
+}
+
+// Checks if the next-server defined as global value is used in responses
+// when there is no specific value defined in subnet and returned to the client
+// properly. There's also similar test for checking parser only configuration,
+// see Dhcp4ParserTest.nextServerGlobal in config_parser_unittest.cc.
+TEST_F(Dhcpv4SrvFakeIfaceTest, nextServerGlobal) {
+
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"next-server\": \"192.0.0.1\", "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ],"
+ " \"subnet\": \"192.0.2.0/24\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp4Server(srv, json));
+
+ // check if returned status is OK
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ dis->setRemoteAddr(IOAddress("192.0.2.1"));
+ dis->setIface("eth0");
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+
+ // Pass it to the server and get an offer
+ Pkt4Ptr offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+ EXPECT_EQ(DHCPOFFER, offer->getType());
+
+ EXPECT_EQ("192.0.0.1", offer->getSiaddr().toText());
+}
+
+
+// a dummy MAC address
+const uint8_t dummyMacAddr[] = {0, 1, 2, 3, 4, 5};
+
+// A dummy MAC address, padded with 0s
+const uint8_t dummyChaddr[16] = {0, 1, 2, 3, 4, 5, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0 };
+
+// Let's use some creative test content here (128 chars + \0)
+const uint8_t dummyFile[] = "Lorem ipsum dolor sit amet, consectetur "
+ "adipiscing elit. Proin mollis placerat metus, at "
+ "lacinia orci ornare vitae. Mauris amet.";
+
+// Yet another type of test content (64 chars + \0)
+const uint8_t dummySname[] = "Lorem ipsum dolor sit amet, consectetur "
+ "adipiscing elit posuere.";
+
+/// @brief a class dedicated to Hooks testing in DHCPv4 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv4SrvTest : public Dhcpv4SrvFakeIfaceTest {
+
+public:
+
+ /// @brief creates Dhcpv4Srv and prepares buffers for callouts
+ HooksDhcpv4SrvTest() {
+
+ // Allocate new DHCPv6 Server
+ srv_ = new NakedDhcpv4Srv(0);
+
+ // clear static buffers
+ resetCalloutBuffers();
+ }
+
+ /// @brief destructor (deletes Dhcpv4Srv)
+ virtual ~HooksDhcpv4SrvTest() {
+
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("buffer4_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("buffer4_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("pkt4_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("subnet4_select");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("lease4_renew");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts("lease4_release");
+
+ delete srv_;
+ }
+
+ /// @brief creates an option with specified option code
+ ///
+ /// This method is static, because it is used from callouts
+ /// that do not have a pointer to HooksDhcpv4SSrvTest object
+ ///
+ /// @param option_code code of option to be created
+ ///
+ /// @return pointer to create option object
+ static OptionPtr createOption(uint16_t option_code) {
+
+ char payload[] = {
+ 0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+ };
+
+ OptionBuffer tmp(payload, payload + sizeof(payload));
+ return OptionPtr(new Option(Option::V4, option_code, tmp));
+ }
+
+ /// @brief Generates test packet.
+ ///
+ /// Allocates and generates on-wire buffer that represents test packet, with all
+ /// fixed fields set to non-zero values. Content is not always reasonable.
+ ///
+ /// See generateTestPacket1() function that returns exactly the same packet as
+ /// Pkt4 object.
+ ///
+ /// @return pointer to allocated Pkt4 object
+ // Returns a vector containing a DHCPv4 packet header.
+ Pkt4Ptr
+ generateSimpleDiscover() {
+
+ // That is only part of the header. It contains all "short" fields,
+ // larger fields are constructed separately.
+ uint8_t hdr[] = {
+ 1, 6, 6, 13, // op, htype, hlen, hops,
+ 0x12, 0x34, 0x56, 0x78, // transaction-id
+ 0, 42, 0x80, 0x00, // 42 secs, BROADCAST flags
+ 192, 0, 2, 1, // ciaddr
+ 1, 2, 3, 4, // yiaddr
+ 192, 0, 2, 255, // siaddr
+ 255, 255, 255, 255, // giaddr
+ };
+
+ // Initialize the vector with the header fields defined above.
+ vector<uint8_t> buf(hdr, hdr + sizeof(hdr));
+
+ // Append the large header fields.
+ copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
+ copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
+ copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));
+
+ // Should now have all the header, so check. The "static_cast" is used
+ // to get round an odd bug whereby the linker appears not to find the
+ // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
+ EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());
+
+ // Add magic cookie
+ buf.push_back(0x63);
+ buf.push_back(0x82);
+ buf.push_back(0x53);
+ buf.push_back(0x63);
+
+ // Add message type DISCOVER
+ buf.push_back(static_cast<uint8_t>(DHO_DHCP_MESSAGE_TYPE));
+ buf.push_back(1); // length (just one byte)
+ buf.push_back(static_cast<uint8_t>(DHCPDISCOVER));
+
+ return (Pkt4Ptr(new Pkt4(&buf[0], buf.size())));
+ }
+
+ /// Test callback that stores received callout name and pkt4 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("buffer4_receive");
+
+ callout_handle.getArgument("query4", callback_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that changes hwaddr value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_change_hwaddr(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // If there is at least one option with data
+ if (pkt->data_.size() >= Pkt4::DHCPV4_PKT_HDR_LEN) {
+ // Offset of the first byte of the CHWADDR field. Let's the first
+ // byte to some new value that we could later check
+ pkt->data_[28] = 0xff;
+ }
+
+ // Carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// Test callback that deletes MAC address
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_delete_hwaddr(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ pkt->data_[2] = 0; // offset 2 is hlen, let's set it to zero
+ memset(&pkt->data_[28], 0, Pkt4::MAX_CHADDR_LEN); // Clear CHADDR content
+
+ // carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// Test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_receive_skip(CalloutHandle& callout_handle) {
+
+ callout_handle.setSkip(true);
+
+ // Carry on as usual
+ return buffer4_receive_callout(callout_handle);
+ }
+
+ /// test callback that stores received callout name and pkt4 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt4_receive");
+
+ callout_handle.getArgument("query4", callback_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// test callback that changes client-id value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_change_clientid(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // get rid of the old client-id
+ pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+ // add a new option
+ pkt->addOption(createOption(DHO_DHCP_CLIENT_IDENTIFIER));
+
+ // carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// test callback that deletes client-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ // get rid of the old client-id (and no HWADDR)
+ vector<uint8_t> mac;
+ pkt->delOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ pkt->setHWAddr(1, 0, mac); // HWtype 1, hwardware len = 0
+
+ // carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_receive_skip(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("query4", pkt);
+
+ callout_handle.setSkip(true);
+
+ // carry on as usual
+ return pkt4_receive_callout(callout_handle);
+ }
+
+ /// Test callback that stores received callout name and pkt4 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt4_send");
+
+ callout_handle.getArgument("response4", callback_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ // Test callback that changes server-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_change_serverid(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // get rid of the old server-id
+ pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // add a new option
+ pkt->addOption(createOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// test callback that deletes server-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_delete_serverid(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // get rid of the old client-id
+ pkt->delOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// Test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt4_send_skip(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ callout_handle.setSkip(true);
+
+ // carry on as usual
+ return pkt4_send_callout(callout_handle);
+ }
+
+ /// Test callback that stores received callout name and pkt4 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_send_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("buffer4_send");
+
+ callout_handle.getArgument("response4", callback_pkt4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback changes the output buffer to a hardcoded value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer4_send_change_callout(CalloutHandle& callout_handle) {
+
+ Pkt4Ptr pkt;
+ callout_handle.getArgument("response4", pkt);
+
+ // modify buffer to set a diffferent payload
+ pkt->getBuffer().clear();
+ pkt->getBuffer().writeData(dummyFile, sizeof(dummyFile));
+
+ return (0);
+ }
+
+ /// Test callback that stores received callout name and pkt4 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ skip_callout(CalloutHandle& callout_handle) {
+
+ callout_handle.setSkip(true);
+
+ return (0);
+ }
+
+ /// Test callback that stores received callout name and subnet4 values
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("subnet4_select");
+
+ callout_handle.getArgument("query4", callback_pkt4_);
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("subnet4collection", callback_subnet4collection_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that picks the other subnet if possible.
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet4_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+ // Call the basic calllout to record all passed values
+ subnet4_select_callout(callout_handle);
+
+ const Subnet4Collection* subnets;
+ Subnet4Ptr subnet;
+ callout_handle.getArgument("subnet4", subnet);
+ callout_handle.getArgument("subnet4collection", subnets);
+
+ // Let's change to a different subnet
+ if (subnets->size() > 1) {
+ subnet = (*subnets)[1]; // Let's pick the other subnet
+ callout_handle.setArgument("subnet4", subnet);
+ }
+
+ return (0);
+ }
+
+ /// Test callback that stores received callout name passed parameters
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_release_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_release");
+
+ callout_handle.getArgument("query4", callback_pkt4_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that stores received callout name and subnet4 values
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease4_renew_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease4_renew");
+
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+ callout_handle.getArgument("hwaddr", callback_hwaddr_);
+ callout_handle.getArgument("clientid", callback_clientid_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+
+ /// resets buffers used to store data received by callouts
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_pkt4_.reset();
+ callback_lease4_.reset();
+ callback_hwaddr_.reset();
+ callback_clientid_.reset();
+ callback_subnet4_.reset();
+ callback_subnet4collection_ = NULL;
+ callback_argument_names_.clear();
+ }
+
+ /// pointer to Dhcpv4Srv that is used in tests
+ NakedDhcpv4Srv* srv_;
+
+ // The following fields are used in testing pkt4_receive_callout
+
+ /// String name of the received callout
+ static string callback_name_;
+
+ /// Pkt4 structure returned in the callout
+ static Pkt4Ptr callback_pkt4_;
+
+ /// Lease4 structure returned in the callout
+ static Lease4Ptr callback_lease4_;
+
+ /// Hardware address returned in the callout
+ static HWAddrPtr callback_hwaddr_;
+
+ /// Client-id returned in the callout
+ static ClientIdPtr callback_clientid_;
+
+ /// Pointer to a subnet received by callout
+ static Subnet4Ptr callback_subnet4_;
+
+ /// A list of all available subnets (received by callout)
+ static const Subnet4Collection* callback_subnet4collection_;
+
+ /// A list of all received arguments
+ static vector<string> callback_argument_names_;
+};
+
+// The following fields are used in testing pkt4_receive_callout.
+// See fields description in the class for details
+string HooksDhcpv4SrvTest::callback_name_;
+Pkt4Ptr HooksDhcpv4SrvTest::callback_pkt4_;
+Subnet4Ptr HooksDhcpv4SrvTest::callback_subnet4_;
+HWAddrPtr HooksDhcpv4SrvTest::callback_hwaddr_;
+ClientIdPtr HooksDhcpv4SrvTest::callback_clientid_;
+Lease4Ptr HooksDhcpv4SrvTest::callback_lease4_;
+const Subnet4Collection* HooksDhcpv4SrvTest::callback_subnet4collection_;
+vector<string> HooksDhcpv4SrvTest::callback_argument_names_;
+
+// Checks if callouts installed on pkt4_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "buffer4_receive".
+TEST_F(HooksDhcpv4SrvTest, Buffer4ReceiveSimple) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr dis = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(dis);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_receive", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == dis.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query4"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on buffer4_receive is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveValueChange) {
+
+ // Install callback that modifies MAC addr of incoming packet
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_change_hwaddr));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer4_receive callback.
+ srv_->run();
+
+ // Check that the server did send a reposonse
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr offer = srv_->fake_sent_.front();
+ ASSERT_TRUE(offer);
+
+ // Get client-id...
+ HWAddrPtr hwaddr = offer->getHWAddr();
+
+ ASSERT_TRUE(hwaddr); // basic sanity check. HWAddr is always present
+
+ // ... and check if it is the modified value
+ ASSERT_FALSE(hwaddr->hwaddr_.empty()); // there must be a MAC address
+ EXPECT_EQ(0xff, hwaddr->hwaddr_[0]); // check that its first byte was modified
+}
+
+// Checks if callouts installed on buffer4_receive is able to set skip flag that
+// will cause the server to not parse the packet. Even though the packet is valid,
+// the server should eventually drop it, because there won't be mandatory options
+// (or rather option objects) in it.
+TEST_F(HooksDhcpv4SrvTest, buffer4ReceiveSkip) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_receive", buffer4_receive_skip));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt4_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt4_receive".
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveSimple) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt4_receive", callback_name_);
+
+ // check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == sol.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query4"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt4_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv4SrvTest, valueChange_pkt4_receive) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_change_clientid));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the server did send a reposonce
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt4_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveDeleteClientId) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_delete_clientid));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not send a response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt4_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, pkt4ReceiveSkip) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_receive", pkt4_receive_skip));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// Checks if callouts installed on pkt4_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv4SrvTest, pkt4SendSimple) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt4_send", callback_name_);
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == adv.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("response4"));
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt4_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv4SrvTest, pkt4SendValueChange) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_change_serverid));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // check that the server did send a reposonce
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(DHO_DHCP_SERVER_IDENTIFIER);
+ EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt4_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv4SrvTest, pkt4SendDeleteServerId) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_delete_serverid));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the server indeed sent a malformed ADVERTISE
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Get that ADVERTISE
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Make sure that it does not have server-id
+ EXPECT_FALSE(adv->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+}
+
+// Checks if callouts installed on pkt4_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv4SrvTest, skip_pkt4_send) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt4_send", pkt4_send_skip));
+
+ // Let's create a simple REQUEST
+ Pkt4Ptr sol = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_send callback.
+ srv_->run();
+
+ // Check that the server sent the message
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Get the first packet and check that it has zero length (i.e. the server
+ // did not do packing on its own)
+ Pkt4Ptr sent = srv_->fake_sent_.front();
+ EXPECT_EQ(0, sent->getBuffer().getLength());
+}
+
+// Checks if callouts installed on buffer4_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv4SrvTest, buffer4SendSimple) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer4_send", callback_name_);
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == adv.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("response4"));
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on buffer4_send are indeed called and that
+// the output buffer can be changed.
+TEST_F(HooksDhcpv4SrvTest, buffer4Send) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", buffer4_send_change_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt4Ptr adv = srv_->fake_sent_.front();
+
+ // The callout is supposed to fill the output buffer with dummyFile content
+ ASSERT_EQ(sizeof(dummyFile), adv->getBuffer().getLength());
+ EXPECT_EQ(0, memcmp(adv->getBuffer().getData(), dummyFile, sizeof(dummyFile)));
+}
+
+// Checks if callouts installed on buffer4_send can set skip flag and that flag
+// causes the packet to not be sent
+TEST_F(HooksDhcpv4SrvTest, buffer4SendSkip) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer4_send", skip_callout));
+
+ // Let's create a simple DISCOVER
+ Pkt4Ptr discover = generateSimpleDiscover();
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(discover);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive4(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt4_receive callback.
+ srv_->run();
+
+ // Check that there is no packet sent.
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// This test checks if subnet4_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv4SrvTest, subnet4SelectSimple) {
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet4_select", subnet4_select_callout));
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.0/25\" ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\" "
+ " }, {"
+ " \"pool\": [ \"192.0.3.0/25\" ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Prepare discover packet. Server should select first subnet for it
+ Pkt4Ptr sol = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ sol->setRemoteAddr(IOAddress("192.0.2.1"));
+ sol->setIface("eth0");
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr adv = srv_->processDiscover(sol);
+
+ // check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("subnet4_select", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == sol.get());
+
+ const Subnet4Collection* exp_subnets = CfgMgr::instance().getSubnets4();
+
+ // The server is supposed to pick the first subnet, because of matching
+ // interface. Check that the value is reported properly.
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(exp_subnets->front().get(), callback_subnet4_.get());
+
+ // Server is supposed to report two subnets
+ ASSERT_EQ(exp_subnets->size(), callback_subnet4collection_->size());
+
+ // Compare that the available subnets are reported as expected
+ EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet4collection_)[0].get());
+ EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet4collection_)[1].get());
+}
+
+// This test checks if callout installed on subnet4_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv4SrvTest, subnet4SelectChange) {
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet4_select", subnet4_select_different_subnet_callout));
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.0/25\" ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"interface\": \"eth0\" "
+ " }, {"
+ " \"pool\": [ \"192.0.3.0/25\" ],"
+ " \"subnet\": \"192.0.3.0/24\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = config::parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Prepare discover packet. Server should select first subnet for it
+ Pkt4Ptr sol = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234));
+ sol->setRemoteAddr(IOAddress("192.0.2.1"));
+ sol->setIface("eth0");
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr adv = srv_->processDiscover(sol);
+
+ // check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // The response should have an address from second pool, so let's check it
+ IOAddress addr = adv->getYiaddr();
+ EXPECT_NE("0.0.0.0", addr.toText());
+
+ // Get all subnets and use second subnet for verification
+ const Subnet4Collection* subnets = CfgMgr::instance().getSubnets4();
+ ASSERT_EQ(2, subnets->size());
+
+ // Advertised address must belong to the second pool (in subnet's range,
+ // in dynamic pool)
+ EXPECT_TRUE((*subnets)[1]->inRange(addr));
+ EXPECT_TRUE((*subnets)[1]->inPool(Lease::TYPE_V4, addr));
+}
+
+// This test verifies that incoming (positive) REQUEST/Renewing can be handled
+// properly and that callout installed on lease4_renew is triggered with
+// expected parameters.
+TEST_F(HooksDhcpv4SrvTest, lease4RenewSimple) {
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_t1 = 50;
+ const uint32_t temp_t2 = 75;
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_renew", lease4_renew_callout));
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2, sizeof(hwaddr2),
+ &client_id_->getDuid()[0], client_id_->getDuid().size(),
+ temp_valid, temp_t1, temp_t2, temp_timestamp,
+ subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+
+ req->addOption(clientid);
+ req->addOption(srv_->getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt4Ptr ack = srv_->processRequest(req);
+
+ // Check if we get response at all
+ checkResponse(ack, DHCPACK, 1234);
+
+ // Check that the lease is really in the database
+ l = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt were really updated
+ EXPECT_EQ(l->t1_, subnet_->getT1());
+ EXPECT_EQ(l->t2_, subnet_->getT2());
+ EXPECT_EQ(l->valid_lft_, subnet_->getValid());
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_renew", callback_name_);
+
+ // Check that hwaddr parameter is passed properly
+ ASSERT_TRUE(callback_hwaddr_);
+ EXPECT_TRUE(*callback_hwaddr_ == *req->getHWAddr());
+
+ // Check that the subnet is passed properly
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(callback_subnet4_->toText(), subnet_->toText());
+
+ ASSERT_TRUE(callback_clientid_);
+ ASSERT_TRUE(client_id_);
+ EXPECT_TRUE(*client_id_ == *callback_clientid_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("subnet4");
+ expected_argument_names.push_back("clientid");
+ expected_argument_names.push_back("hwaddr");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+}
+
+// This test verifies that a callout installed on lease4_renew can trigger
+// the server to not renew a lease.
+TEST_F(HooksDhcpv4SrvTest, lease4RenewSkip) {
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_t1 = 50;
+ const uint32_t temp_t2 = 75;
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_renew", skip_callout));
+
+ // Generate client-id also sets client_id_ member
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // let's create a lease and put it in the LeaseMgr
+ uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ Lease4Ptr used(new Lease4(IOAddress("192.0.2.106"), hwaddr2, sizeof(hwaddr2),
+ &client_id_->getDuid()[0], client_id_->getDuid().size(),
+ temp_valid, temp_t1, temp_t2, temp_timestamp,
+ subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt really set.
+ // Constructed lease looks as if it was assigned 10 seconds ago
+ // EXPECT_EQ(l->t1_, temp_t1);
+ // EXPECT_EQ(l->t2_, temp_t2);
+ EXPECT_EQ(l->valid_lft_, temp_valid);
+ EXPECT_EQ(l->cltt_, temp_timestamp);
+
+ // Let's create a RENEW
+ Pkt4Ptr req = Pkt4Ptr(new Pkt4(DHCPREQUEST, 1234));
+ req->setRemoteAddr(IOAddress(addr));
+ req->setYiaddr(addr);
+ req->setCiaddr(addr); // client's address
+ req->setIface("eth0");
+
+ req->addOption(clientid);
+ req->addOption(srv_->getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt4Ptr ack = srv_->processRequest(req);
+ ASSERT_TRUE(ack);
+
+ // Check that the lease is really in the database
+ l = checkLease(ack, clientid, req->getHWAddr(), addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, valid and cltt were NOT updated
+ EXPECT_EQ(temp_t1, l->t1_);
+ EXPECT_EQ(temp_t2, l->t2_);
+ EXPECT_EQ(temp_valid, l->valid_lft_);
+ EXPECT_EQ(temp_timestamp, l->cltt_);
+
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+}
+
+// This test verifies that valid RELEASE triggers lease4_release callouts
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSimple) {
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_t1 = 50;
+ const uint32_t temp_t2 = 75;
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", lease4_release_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, mac_addr, sizeof(mac_addr),
+ &client_id_->getDuid()[0], client_id_->getDuid().size(),
+ temp_valid, temp_t1, temp_t2, temp_timestamp,
+ subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setCiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Pass it to the server and hope for a REPLY
+ // Note: this is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be gone from LeaseMgr
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_FALSE(l);
+
+ // Try to get the lease by hardware address
+ // @todo: Uncomment this once trac2592 is implemented
+ // Lease4Collection leases = LeaseMgrFactory::instance().getLease4(hw->hwaddr_);
+ // EXPECT_EQ(leases.size(), 0);
+
+ // Try to get it by hw/subnet_id combination
+ l = LeaseMgrFactory::instance().getLease4(hw->hwaddr_, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Try by client-id
+ // @todo: Uncomment this once trac2592 is implemented
+ //Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ //EXPECT_EQ(leases.size(), 0);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_FALSE(l);
+
+ // Ok, the lease is *really* not there.
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease4_release", callback_name_);
+
+ // Check that pkt4 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt4_.get() == rel.get());
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query4");
+ expected_argument_names.push_back("lease4");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test verifies that skip flag returned by a callout installed on the
+// lease4_release hook point will keep the lease
+TEST_F(HooksDhcpv4SrvTest, lease4ReleaseSkip) {
+
+ const IOAddress addr("192.0.2.106");
+ const uint32_t temp_t1 = 50;
+ const uint32_t temp_t2 = 75;
+ const uint32_t temp_valid = 100;
+ const time_t temp_timestamp = time(NULL) - 10;
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_release", skip_callout));
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_V4, addr));
+
+ // Let's create a lease and put it in the LeaseMgr
+ uint8_t mac_addr[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
+ HWAddrPtr hw(new HWAddr(mac_addr, sizeof(mac_addr), HTYPE_ETHER));
+ Lease4Ptr used(new Lease4(addr, mac_addr, sizeof(mac_addr),
+ &client_id_->getDuid()[0], client_id_->getDuid().size(),
+ temp_valid, temp_t1, temp_t2, temp_timestamp,
+ subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Check that the lease is really in the database
+ Lease4Ptr l = LeaseMgrFactory::instance().getLease4(addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ // Generate client-id also duid_
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setRemoteAddr(addr);
+ rel->setYiaddr(addr);
+ rel->addOption(clientid);
+ rel->addOption(srv_->getServerID());
+ rel->setHWAddr(hw);
+
+ // Pass it to the server and hope for a REPLY
+ // Note: this is no response to RELEASE in DHCPv4
+ EXPECT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease should be still there
+ l = LeaseMgrFactory::instance().getLease4(addr);
+ EXPECT_TRUE(l);
+
+ // Try by client-id/subnet-id
+ l = LeaseMgrFactory::instance().getLease4(*client_id_, subnet_->getID());
+ EXPECT_TRUE(l);
+
+ // Try to get the lease by hardware address
+ // @todo: Uncomment this once trac2592 is implemented
+ // Lease4Collection leases = LeaseMgrFactory::instance().getLease4(hw->hwaddr_);
+ // EXPECT_EQ(leases.size(), 1);
+
+ // Try by client-id
+ // @todo: Uncomment this once trac2592 is implemented
+ //Lease4Collection leases = LeaseMgrFactory::instance().getLease4(*client_id_);
+ //EXPECT_EQ(leases.size(), 1);
+}
+
+// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems
+TEST_F(Dhcpv4SrvFakeIfaceTest, docsisVendorOptionsParse) {
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt4Ptr dis = captureRelayedDiscover();
+ ASSERT_NO_THROW(dis->unpack());
+
+ // Check if the packet contain
+ OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(opt);
+
+ boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+
+ // This particular capture that we have included options 1 and 5
+ EXPECT_TRUE(vendor->getOption(1));
+ EXPECT_TRUE(vendor->getOption(5));
+
+ // It did not include options any other options
+ EXPECT_FALSE(vendor->getOption(2));
+ EXPECT_FALSE(vendor->getOption(3));
+ EXPECT_FALSE(vendor->getOption(17));
+}
+
+// Checks if server is able to parse incoming docsis option and extract suboption 1 (docsis ORO)
+TEST_F(Dhcpv4SrvFakeIfaceTest, docsisVendorORO) {
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt4Ptr dis = captureRelayedDiscover();
+ EXPECT_NO_THROW(dis->unpack());
+
+ // Check if the packet contains vendor specific information option
+ OptionPtr opt = dis->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(opt);
+
+ boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+
+ opt = vendor->getOption(DOCSIS3_V4_ORO);
+ ASSERT_TRUE(opt);
+
+ OptionUint8ArrayPtr oro = boost::dynamic_pointer_cast<OptionUint8Array>(opt);
+ EXPECT_TRUE(oro);
+}
+
+// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
+// vendor options is parsed correctly and the requested options are actually assigned.
+TEST_F(Dhcpv4SrvFakeIfaceTest, vendorOptionsORO) {
+
+ NakedDhcpv4Srv srv(0);
+
+ ConstElementPtr x;
+ string config = "{ \"interfaces\": [ \"all\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 2,"
+ " \"data\": \"192.0.2.1, 192.0.2.2\","
+ " \"csv-format\": True"
+ " }],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.0/25\" ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"rebind-timer\": 2000, "
+ " \"renew-timer\": 1000, "
+ " \"valid-lifetime\": 4000,"
+ " \"interface\": \"eth0\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(x = configureDhcp4Server(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = isc::config::parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ boost::shared_ptr<Pkt4> dis(new Pkt4(DHCPDISCOVER, 1234));
+ // Set the giaddr and hops to non-zero address as if it was relayed.
+ dis->setGiaddr(IOAddress("192.0.2.1"));
+ dis->setHops(1);
+
+ OptionPtr clientid = generateClientId();
+ dis->addOption(clientid);
+ // Set interface. It is required by the server to generate server id.
+ dis->setIface("eth0");
+
+ // Pass it to the server and get an advertise
+ Pkt4Ptr offer = srv.processDiscover(dis);
+
+ // check if we get response at all
+ ASSERT_TRUE(offer);
+
+ // We did not include any vendor opts in DISCOVER, so there should be none
+ // in OFFER.
+ ASSERT_FALSE(offer->getOption(DHO_VIVSO_SUBOPTIONS));
+
+ // Let's add a vendor-option (vendor-id=4491) with a single sub-option.
+ // That suboption has code 1 and is a docsis ORO option.
+ boost::shared_ptr<OptionUint8Array> vendor_oro(new OptionUint8Array(Option::V4,
+ DOCSIS3_V4_ORO));
+ vendor_oro->addValue(DOCSIS3_V4_TFTP_SERVERS); // Request option 33
+ OptionPtr vendor(new OptionVendor(Option::V4, 4491));
+ vendor->addOption(vendor_oro);
+ dis->addOption(vendor);
+
+ // Need to process SOLICIT again after requesting new option.
+ offer = srv.processDiscover(dis);
+ ASSERT_TRUE(offer);
+
+ // Check if thre is vendor option response
+ OptionPtr tmp = offer->getOption(DHO_VIVSO_SUBOPTIONS);
+ ASSERT_TRUE(tmp);
+
+ // The response should be OptionVendor object
+ boost::shared_ptr<OptionVendor> vendor_resp =
+ boost::dynamic_pointer_cast<OptionVendor>(tmp);
+ ASSERT_TRUE(vendor_resp);
+
+ OptionPtr docsis2 = vendor_resp->getOption(DOCSIS3_V4_TFTP_SERVERS);
+ ASSERT_TRUE(docsis2);
+
+ Option4AddrLstPtr tftp_srvs = boost::dynamic_pointer_cast<Option4AddrLst>(docsis2);
+ ASSERT_TRUE(tftp_srvs);
+
+ Option4AddrLst::AddressContainer addrs = tftp_srvs->getAddresses();
+ ASSERT_EQ(2, addrs.size());
+ EXPECT_EQ("192.0.2.1", addrs[0].toText());
+ EXPECT_EQ("192.0.2.2", addrs[1].toText());
+}
+
+// Test checks whether it is possible to use option definitions defined in
+// src/lib/dhcp/docsis3_option_defs.h.
+TEST_F(Dhcpv4SrvFakeIfaceTest, vendorOptionsDocsisDefinitions) {
+ ConstElementPtr x;
+ string config_prefix = "{ \"interfaces\": [ \"all\" ],"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ " \"option-data\": [ {"
+ " \"name\": \"tftp-servers\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": ";
+ string config_postfix = ","
+ " \"data\": \"192.0.2.1\","
+ " \"csv-format\": True"
+ " }],"
+ "\"subnet4\": [ { "
+ " \"pool\": [ \"192.0.2.1 - 192.0.2.50\" ],"
+ " \"subnet\": \"192.0.2.0/24\", "
+ " \"renew-timer\": 1000, "
+ " \"rebind-timer\": 1000, "
+ " \"valid-lifetime\": 4000,"
+ " \"interface\": \"\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // There is docsis3 (vendor-id=4491) vendor option 2, which is a
+ // tftp-server. Its format is list of IPv4 addresses.
+ string config_valid = config_prefix + "2" + config_postfix;
+
+ // There is no option 99 defined in vendor-id=4491. As there is no
+ // definition, the config should fail.
+ string config_bogus = config_prefix + "99" + config_postfix;
+
+ ElementPtr json_bogus = Element::fromJSON(config_bogus);
+ ElementPtr json_valid = Element::fromJSON(config_valid);
+
+ NakedDhcpv4Srv srv(0);
+
+ // This should fail (missing option definition)
+ EXPECT_NO_THROW(x = configureDhcp4Server(srv, json_bogus));
+ ASSERT_TRUE(x);
+ comment_ = isc::config::parseAnswer(rcode_, x);
+ ASSERT_EQ(1, rcode_);
+
+ // This should work (option definition present)
+ EXPECT_NO_THROW(x = configureDhcp4Server(srv, json_valid));
+ ASSERT_TRUE(x);
+ comment_ = isc::config::parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+}
+
+// Checks if client packets are classified properly
+TEST_F(Dhcpv4SrvTest, clientClassification) {
+
NakedDhcpv4Srv srv(0);
- string srvid_text = "192.0.2.100";
- IOAddress srvid(srvid_text);
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // vendor-class set to docsis3.0
+ Pkt4Ptr dis1;
+ ASSERT_NO_THROW(dis1 = captureRelayedDiscover());
+ ASSERT_NO_THROW(dis1->unpack());
- fstream file1(SRVID_FILE, ios::out | ios::trunc);
- file1 << srvid_text;
- file1.close();
+ srv.classifyPacket(dis1);
- // Test reading from a file
- EXPECT_TRUE(srv.loadServerID(SRVID_FILE));
- ASSERT_TRUE(srv.getServerID());
- EXPECT_EQ(srvid_text, srv.srvidToString(srv.getServerID()));
+ EXPECT_TRUE(dis1->inClass("docsis3.0"));
+ EXPECT_FALSE(dis1->inClass("eRouter1.0"));
- // Now test writing to a file
- EXPECT_EQ(0, unlink(SRVID_FILE));
- EXPECT_NO_THROW(srv.writeServerID(SRVID_FILE));
+ // Let's create a relayed DISCOVER. This particular relayed DISCOVER has
+ // vendor-class set to eRouter1.0
+ Pkt4Ptr dis2;
+ ASSERT_NO_THROW(dis2 = captureRelayedDiscover2());
+ ASSERT_NO_THROW(dis2->unpack());
- fstream file2(SRVID_FILE, ios::in);
- ASSERT_TRUE(file2.good());
- string text;
- file2 >> text;
- file2.close();
+ srv.classifyPacket(dis2);
- EXPECT_EQ(srvid_text, text);
+ EXPECT_TRUE(dis2->inClass("eRouter1.0"));
+ EXPECT_FALSE(dis2->inClass("docsis3.0"));
}
-} // end of anonymous namespace
+}; // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.cc b/src/bin/dhcp4/tests/dhcp4_test_utils.cc
new file mode 100644
index 0000000..e983e7b
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_test_utils.cc
@@ -0,0 +1,613 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <config/ccsession.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+
+using namespace std;
+using namespace isc::asiolink;
+
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// dummy server-id file location
+static const char* SRVID_FILE = "server-id-test.txt";
+
+Dhcpv4SrvTest::Dhcpv4SrvTest()
+:rcode_(-1) {
+ subnet_ = Subnet4Ptr(new Subnet4(IOAddress("192.0.2.0"), 24, 1000,
+ 2000, 3000));
+ pool_ = Pool4Ptr(new Pool4(IOAddress("192.0.2.100"), IOAddress("192.0.2.110")));
+ subnet_->addPool(pool_);
+
+ CfgMgr::instance().deleteActiveIfaces();
+ CfgMgr::instance().deleteSubnets4();
+ CfgMgr::instance().addSubnet4(subnet_);
+
+ // Add Router option.
+ Option4AddrLstPtr opt_routers(new Option4AddrLst(DHO_ROUTERS));
+ opt_routers->setAddress(IOAddress("192.0.2.2"));
+ subnet_->addOption(opt_routers, false, "dhcp4");
+
+ // it's ok if that fails. There should not be such a file anyway
+ unlink(SRVID_FILE);
+}
+
+Dhcpv4SrvTest::~Dhcpv4SrvTest() {
+
+ // Make sure that we revert to default value
+ CfgMgr::instance().echoClientId(true);
+}
+
+void Dhcpv4SrvTest::addPrlOption(Pkt4Ptr& pkt) {
+
+ OptionUint8ArrayPtr option_prl =
+ OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Let's request options that have been configured for the subnet.
+ option_prl->addValue(DHO_DOMAIN_NAME_SERVERS);
+ option_prl->addValue(DHO_DOMAIN_NAME);
+ option_prl->addValue(DHO_LOG_SERVERS);
+ option_prl->addValue(DHO_COOKIE_SERVERS);
+ // Let's also request the option that hasn't been configured. In such
+ // case server should ignore request for this particular option.
+ option_prl->addValue(DHO_LPR_SERVERS);
+ // And add 'Parameter Request List' option into the DISCOVER packet.
+ pkt->addOption(option_prl);
+}
+
+void Dhcpv4SrvTest::configureRequestedOptions() {
+ // dns-servers
+ Option4AddrLstPtr
+ option_dns_servers(new Option4AddrLst(DHO_DOMAIN_NAME_SERVERS));
+ option_dns_servers->addAddress(IOAddress("192.0.2.1"));
+ option_dns_servers->addAddress(IOAddress("192.0.2.100"));
+ ASSERT_NO_THROW(subnet_->addOption(option_dns_servers, false, "dhcp4"));
+
+ // domain-name
+ OptionDefinition def("domain-name", DHO_DOMAIN_NAME, OPT_FQDN_TYPE);
+ OptionCustomPtr option_domain_name(new OptionCustom(def, Option::V4));
+ option_domain_name->writeFqdn("example.com");
+ subnet_->addOption(option_domain_name, false, "dhcp4");
+
+ // log-servers
+ Option4AddrLstPtr option_log_servers(new Option4AddrLst(DHO_LOG_SERVERS));
+ option_log_servers->addAddress(IOAddress("192.0.2.2"));
+ option_log_servers->addAddress(IOAddress("192.0.2.10"));
+ ASSERT_NO_THROW(subnet_->addOption(option_log_servers, false, "dhcp4"));
+
+ // cookie-servers
+ Option4AddrLstPtr option_cookie_servers(new Option4AddrLst(DHO_COOKIE_SERVERS));
+ option_cookie_servers->addAddress(IOAddress("192.0.2.1"));
+ ASSERT_NO_THROW(subnet_->addOption(option_cookie_servers, false, "dhcp4"));
+}
+
+void Dhcpv4SrvTest::messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a) {
+ ASSERT_TRUE(q);
+ ASSERT_TRUE(a);
+
+ EXPECT_EQ(q->getHops(), a->getHops());
+ EXPECT_EQ(q->getIface(), a->getIface());
+ EXPECT_EQ(q->getIndex(), a->getIndex());
+ EXPECT_EQ(q->getGiaddr(), a->getGiaddr());
+ // When processing an incoming packet the remote address
+ // is copied as a src address, and the source address is
+ // copied as a remote address to the response.
+ EXPECT_TRUE(q->getLocalHWAddr() == a->getLocalHWAddr());
+ EXPECT_TRUE(q->getRemoteHWAddr() == a->getRemoteHWAddr());
+
+ // Check that the server identifier is present in the response.
+ // Presence (or absence) of other options is checked elsewhere.
+ EXPECT_TRUE(a->getOption(DHO_DHCP_SERVER_IDENTIFIER));
+
+ // Check that something is offered
+ EXPECT_NE("0.0.0.0", a->getYiaddr().toText());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::basicOptionsPresent(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option missing in the response";
+ if (!pkt->getOption(DHO_DOMAIN_NAME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "domain-name " << errmsg));
+
+ } else if (!pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dns-servers " << errmsg));
+
+ } else if (!pkt->getOption(DHO_SUBNET_MASK)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "subnet-mask " << errmsg));
+
+ } else if (!pkt->getOption(DHO_ROUTERS)) {
+ return (::testing::AssertionFailure(::testing::Message() << "routers "
+ << errmsg));
+
+ } else if (!pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+ return (::testing::AssertionFailure(::testing::Message() <<
+ "dhcp-lease-time " << errmsg));
+
+ }
+ return (::testing::AssertionSuccess());
+
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noBasicOptions(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option present in the response";
+ if (pkt->getOption(DHO_DOMAIN_NAME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "domain-name " << errmsg));
+
+ } else if (pkt->getOption(DHO_DOMAIN_NAME_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dns-servers " << errmsg));
+
+ } else if (pkt->getOption(DHO_SUBNET_MASK)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "subnet-mask " << errmsg));
+
+ } else if (pkt->getOption(DHO_ROUTERS)) {
+ return (::testing::AssertionFailure(::testing::Message() << "routers "
+ << errmsg));
+
+ } else if (pkt->getOption(DHO_DHCP_LEASE_TIME)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "dhcp-lease-time " << errmsg));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::requestedOptionsPresent(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option missing in the response";
+ if (!pkt->getOption(DHO_LOG_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "log-servers " << errmsg));
+
+ } else if (!pkt->getOption(DHO_COOKIE_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "cookie-servers " << errmsg));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::noRequestedOptions(const Pkt4Ptr& pkt) {
+ std::ostringstream errmsg;
+ errmsg << "option present in the response";
+ if (pkt->getOption(DHO_LOG_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "log-servers " << errmsg));
+
+ } else if (pkt->getOption(DHO_COOKIE_SERVERS)) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "cookie-servers " << errmsg));
+
+ }
+ return (::testing::AssertionSuccess());
+}
+
+OptionPtr Dhcpv4SrvTest::generateClientId(size_t size /*= 4*/) {
+
+ OptionBuffer clnt_id(size);
+ for (int i = 0; i < size; i++) {
+ clnt_id[i] = 100 + i;
+ }
+
+ client_id_ = ClientIdPtr(new ClientId(clnt_id));
+
+ return (OptionPtr(new Option(Option::V4, DHO_DHCP_CLIENT_IDENTIFIER,
+ clnt_id.begin(),
+ clnt_id.begin() + size)));
+}
+
+HWAddrPtr Dhcpv4SrvTest::generateHWAddr(size_t size /*= 6*/) {
+ const uint8_t hw_type = 123; // Just a fake number (typically 6=HTYPE_ETHER, see dhcp4.h)
+ OptionBuffer mac(size);
+ for (int i = 0; i < size; ++i) {
+ mac[i] = 50 + i;
+ }
+ return (HWAddrPtr(new HWAddr(mac, hw_type)));
+}
+
+void Dhcpv4SrvTest::checkAddressParams(const Pkt4Ptr& rsp, const SubnetPtr subnet,
+ bool t1_mandatory /*= false*/,
+ bool t2_mandatory /*= false*/) {
+
+ // Technically inPool implies inRange, but let's be on the safe
+ // side and check both.
+ EXPECT_TRUE(subnet->inRange(rsp->getYiaddr()));
+ EXPECT_TRUE(subnet->inPool(Lease::TYPE_V4, rsp->getYiaddr()));
+
+ // Check lease time
+ OptionPtr opt = rsp->getOption(DHO_DHCP_LEASE_TIME);
+ if (!opt) {
+ ADD_FAILURE() << "Lease time option missing in response";
+ } else {
+ EXPECT_EQ(opt->getUint32(), subnet->getValid());
+ }
+
+ // Check T1 timer
+ opt = rsp->getOption(DHO_DHCP_RENEWAL_TIME);
+ if (opt) {
+ EXPECT_EQ(opt->getUint32(), subnet->getT1());
+ } else {
+ if (t1_mandatory) {
+ ADD_FAILURE() << "Required T1 option missing";
+ }
+ }
+
+ // Check T2 timer
+ opt = rsp->getOption(DHO_DHCP_REBINDING_TIME);
+ if (opt) {
+ EXPECT_EQ(opt->getUint32(), subnet->getT2());
+ } else {
+ if (t2_mandatory) {
+ ADD_FAILURE() << "Required T2 option missing";
+ }
+ }
+}
+
+void Dhcpv4SrvTest::checkResponse(const Pkt4Ptr& rsp, uint8_t expected_message_type,
+ uint32_t expected_transid) {
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(expected_message_type, rsp->getType());
+ EXPECT_EQ(expected_transid, rsp->getTransid());
+}
+
+Lease4Ptr Dhcpv4SrvTest::checkLease(const Pkt4Ptr& rsp,
+ const OptionPtr& client_id,
+ const HWAddrPtr&,
+ const IOAddress& expected_addr) {
+
+ ClientIdPtr id;
+ if (client_id) {
+ OptionBuffer data = client_id->getData();
+ id.reset(new ClientId(data));
+ }
+
+ Lease4Ptr lease = LeaseMgrFactory::instance().getLease4(expected_addr);
+ if (!lease) {
+ cout << "Lease for " << expected_addr
+ << " not found in the database backend.";
+ return (Lease4Ptr());
+ }
+
+ EXPECT_EQ(rsp->getYiaddr(), expected_addr);
+
+ EXPECT_EQ(expected_addr, lease->addr_);
+ if (client_id) {
+ EXPECT_TRUE(*lease->client_id_ == *id);
+ }
+ EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
+
+ return (lease);
+}
+
+void Dhcpv4SrvTest::checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid) {
+ // Check that server included its server-id
+ OptionPtr opt = rsp->getOption(DHO_DHCP_SERVER_IDENTIFIER);
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(opt->getType(), expected_srvid->getType() );
+ EXPECT_EQ(opt->len(), expected_srvid->len() );
+ EXPECT_TRUE(opt->getData() == expected_srvid->getData());
+}
+
+void Dhcpv4SrvTest::checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid) {
+
+ bool include_clientid = CfgMgr::instance().echoClientId();
+
+ // check that server included our own client-id
+ OptionPtr opt = rsp->getOption(DHO_DHCP_CLIENT_IDENTIFIER);
+ if (include_clientid) {
+ // Normal mode: echo back (see RFC6842)
+ ASSERT_TRUE(opt);
+ EXPECT_EQ(expected_clientid->getType(), opt->getType());
+ EXPECT_EQ(expected_clientid->len(), opt->len());
+ EXPECT_TRUE(expected_clientid->getData() == opt->getData());
+ } else {
+ // Backward compatibility mode for pre-RFC6842 devices
+ ASSERT_FALSE(opt);
+ }
+}
+
+::testing::AssertionResult
+Dhcpv4SrvTest::createPacketFromBuffer(const Pkt4Ptr& src_pkt,
+ Pkt4Ptr& dst_pkt) {
+ // Create on-wire format of the packet. If pack() has been called
+ // on this instance of the packet already, the next call to pack()
+ // should remove all contents of the output buffer.
+ try {
+ src_pkt->pack();
+ } catch (const Exception& ex) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Failed to parse source packet: "
+ << ex.what()));
+ }
+ // Get the output buffer from the source packet.
+ const util::OutputBuffer& buf = src_pkt->getBuffer();
+ // Create a copy of the packet using the output buffer from the source
+ // packet.
+ try {
+ dst_pkt.reset(new Pkt4(static_cast<const uint8_t*>(buf.getData()),
+ buf.getLength()));
+ } catch (const Exception& ex) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Failed to create a"
+ " destination packet from"
+ " the buffer: "
+ << ex.what()));
+ }
+
+ try {
+ // Parse the new packet and return to the caller.
+ dst_pkt->unpack();
+ } catch (const Exception& ex) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Failed to parse a"
+ << " destination packet: "
+ << ex.what()));
+ }
+
+ return (::testing::AssertionSuccess());
+}
+
+void Dhcpv4SrvTest::TearDown() {
+
+ CfgMgr::instance().deleteSubnets4();
+
+ // Let's clean up if there is such a file.
+ unlink(SRVID_FILE);
+
+ // Close all open sockets.
+ IfaceMgr::instance().closeSockets();
+
+ // Some unit tests override the default packet filtering class, used
+ // by the IfaceMgr. The dummy class, called PktFilterTest, reports the
+ // capability to directly respond to the clients without IP address
+ // assigned. This capability is not supported by the default packet
+ // filtering class: PktFilterInet. Therefore setting the dummy class
+ // allows to test scenarios, when server responds to the broadcast address
+ // on client's request, despite having support for direct response.
+ // The following call restores the use of original packet filtering class
+ // after the test.
+ try {
+ IfaceMgr::instance().setPacketFilter(PktFilterPtr(new PktFilterInet()));
+
+ } catch (const Exception& ex) {
+ FAIL() << "Failed to restore the default (PktFilterInet) packet filtering"
+ << " class after the test. Exception has been caught: "
+ << ex.what();
+ }
+
+}
+
+Dhcpv4SrvFakeIfaceTest::Dhcpv4SrvFakeIfaceTest()
+: Dhcpv4SrvTest(), current_pkt_filter_() {
+ // Remove current interface configuration. Instead we want to add
+ // a couple of fake interfaces.
+ IfaceMgr& ifacemgr = IfaceMgr::instance();
+ ifacemgr.closeSockets();
+ ifacemgr.clearIfaces();
+
+ // Add fake interfaces.
+ ifacemgr.addInterface(createIface("lo", 0, "127.0.0.1"));
+ ifacemgr.addInterface(createIface("eth0", 1, "192.0.3.1"));
+ ifacemgr.addInterface(createIface("eth1", 2, "10.0.0.1"));
+
+ // In order to use fake interfaces we have to supply the custom
+ // packet filtering class, which can mimic opening sockets on
+ // fake interafaces.
+ current_pkt_filter_.reset(new PktFilterTest());
+ ifacemgr.setPacketFilter(current_pkt_filter_);
+ ifacemgr.openSockets4();
+}
+
+void
+Dhcpv4SrvFakeIfaceTest::TearDown() {
+ // The base class function restores the original packet filtering class.
+ Dhcpv4SrvTest::TearDown();
+ // The base class however, doesn't re-detect real interfaces.
+ try {
+ IfaceMgr::instance().clearIfaces();
+ IfaceMgr::instance().detectIfaces();
+
+ } catch (const Exception& ex) {
+ FAIL() << "Failed to restore interface configuration after using"
+ " fake interfaces";
+ }
+}
+
+Iface
+Dhcpv4SrvFakeIfaceTest::createIface(const std::string& name, const int ifindex,
+ const std::string& addr) {
+ Iface iface(name, ifindex);
+ iface.addAddress(IOAddress(addr));
+ if (name == "lo") {
+ iface.flag_loopback_ = true;
+ }
+ iface.flag_up_ = true;
+ iface.flag_running_ = true;
+ iface.inactive4_ = false;
+ return (iface);
+}
+
+void
+Dhcpv4SrvFakeIfaceTest::testDiscoverRequest(const uint8_t msg_type) {
+ // Create an instance of the tested class.
+ boost::scoped_ptr<NakedDhcpv4Srv> srv(new NakedDhcpv4Srv(0));
+
+ // Initialize the source HW address.
+ vector<uint8_t> mac(6);
+ for (int i = 0; i < 6; ++i) {
+ mac[i] = i * 10;
+ }
+ // Initialized the destination HW address.
+ vector<uint8_t> dst_mac(6);
+ for (int i = 0; i < 6; ++i) {
+ dst_mac[i] = i * 20;
+ }
+ // Create a DHCP message. It will be used to simulate the
+ // incoming message.
+ boost::shared_ptr<Pkt4> req(new Pkt4(msg_type, 1234));
+ // Create a response message. It will hold a reponse packet.
+ // Initially, set it to NULL.
+ boost::shared_ptr<Pkt4> rsp;
+ // Set the name of the interface on which packet is received.
+ req->setIface("eth0");
+ // Set the interface index. It is just a dummy value and will
+ // not be interpreted.
+ req->setIndex(17);
+ // Set the target HW address. This value is normally used to
+ // construct the data link layer header.
+ req->setRemoteHWAddr(1, 6, dst_mac);
+ // Set the HW address. This value is set on DHCP level (in chaddr).
+ req->setHWAddr(1, 6, mac);
+ // Set local HW address. It is used to construct the data link layer
+ // header.
+ req->setLocalHWAddr(1, 6, mac);
+ // Set target IP address.
+ req->setRemoteAddr(IOAddress("192.0.2.55"));
+ // Set relay address and hops.
+ req->setGiaddr(IOAddress("192.0.2.10"));
+ req->setHops(1);
+
+ // We are going to test that certain options are returned
+ // in the response message when requested using 'Parameter
+ // Request List' option. Let's configure those options that
+ // are returned when requested.
+ configureRequestedOptions();
+
+ // Create a copy of the original packet by parsing its wire format.
+ // This simulates the real life scenario when we process the packet
+ // which was parsed from its wire format.
+ Pkt4Ptr received;
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(
+ rsp = srv->processDiscover(received);
+ );
+
+ // Should return OFFER
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPOFFER, rsp->getType());
+
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+
+ // Should return ACK
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+
+ }
+
+ messageCheck(received, rsp);
+
+ // Basic options should be present when we got the lease.
+ EXPECT_TRUE(basicOptionsPresent(rsp));
+ // We did not request any options so these should not be present
+ // in the RSP.
+ EXPECT_TRUE(noRequestedOptions(rsp));
+
+ // Repeat the test but request some options.
+ // Add 'Parameter Request List' option.
+ addPrlOption(req);
+
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPOFFER, rsp->getType());
+
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(DHCPACK, rsp->getType());
+ }
+
+ // Check that the requested options are returned.
+ EXPECT_TRUE(basicOptionsPresent(rsp));
+ EXPECT_TRUE(requestedOptionsPresent(rsp));
+
+ // The following part of the test will test that the NAK is sent when
+ // there is no address pool configured. In the same time, we expect
+ // that the requested options are not included in NAK message, but that
+ // they are only included when yiaddr is set to non-zero value.
+ ASSERT_NO_THROW(subnet_->delPools(Lease::TYPE_V4));
+
+ // There has been a lease allocated for the particular client. So,
+ // even though we deleted the subnet, the client would get the
+ // existing lease (not a NAK). Therefore, we have to change the chaddr
+ // in the packet so as the existing lease is not returned.
+ req->setHWAddr(1, 6, std::vector<uint8_t>(2, 6));
+ ASSERT_TRUE(createPacketFromBuffer(req, received));
+ ASSERT_TRUE(received->getOption(DHO_DHCP_PARAMETER_REQUEST_LIST));
+
+ // Set interface. It is required for the server to generate server id.
+ received->setIface("eth0");
+
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(rsp = srv->processDiscover(received));
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ } else {
+ ASSERT_NO_THROW(rsp = srv->processRequest(received));
+ // Should return non-NULL packet.
+ ASSERT_TRUE(rsp);
+ }
+ // We should get the NAK packet with yiaddr set to 0.
+ EXPECT_EQ(DHCPNAK, rsp->getType());
+ ASSERT_EQ("0.0.0.0", rsp->getYiaddr().toText());
+
+ // Make sure that none of the requested options is returned in NAK.
+ // Also options such as Routers or Subnet Mask should not be there,
+ // because lease hasn't been acquired.
+ EXPECT_TRUE(noRequestedOptions(rsp));
+ EXPECT_TRUE(noBasicOptions(rsp));
+}
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.h b/src/bin/dhcp4/tests/dhcp4_test_utils.h
new file mode 100644
index 0000000..8a22117
--- /dev/null
+++ b/src/bin/dhcp4/tests/dhcp4_test_utils.h
@@ -0,0 +1,461 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file dhcp4_test_utils.h
+///
+/// @brief This file contains utility classes used for DHCPv4 server testing
+
+#ifndef DHCP4_TEST_UTILS_H
+#define DHCP4_TEST_UTILS_H
+
+#include <gtest/gtest.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/option4_addrlst.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt_filter.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/lease.h>
+#include <dhcp4/dhcp4_srv.h>
+#include <asiolink/io_address.h>
+#include <config/ccsession.h>
+#include <list>
+
+#include <boost/shared_ptr.hpp>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Dummy Packet Filtering class.
+///
+/// This class reports capability to respond directly to the client which
+/// doesn't have address configured yet.
+///
+/// All packet and socket handling functions do nothing because they are not
+/// used in unit tests.
+class PktFilterTest : public PktFilter {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Sets the 'direct response' capability to true.
+ PktFilterTest()
+ : direct_resp_supported_(true) {
+ }
+
+ /// @brief Reports 'direct response' capability.
+ ///
+ /// @return always true.
+ virtual bool isDirectResponseSupported() const {
+ return (direct_resp_supported_);
+ }
+
+ /// Does nothing.
+ virtual SocketInfo openSocket(const Iface&,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool, const bool) {
+ return (SocketInfo(addr, port, 0));
+ }
+
+ /// Does nothing.
+ virtual Pkt4Ptr receive(const Iface&, const SocketInfo&) {
+ return Pkt4Ptr();
+ }
+
+ /// Does nothing.
+ virtual int send(const Iface&, uint16_t, const Pkt4Ptr&) {
+ return (0);
+ }
+
+ /// @brief Holds a boolean value which indicates whether direct response
+ /// capability is supported (true) or not (false).
+ bool direct_resp_supported_;
+
+};
+
+typedef boost::shared_ptr<PktFilterTest> PktFilterTestPtr;
+
+class Dhcpv4SrvTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Initializes common objects used in many tests.
+ /// Also sets up initial configuration in CfgMgr.
+ Dhcpv4SrvTest();
+
+ /// @brief destructor
+ virtual ~Dhcpv4SrvTest();
+
+ /// @brief Add 'Parameter Request List' option to the packet.
+ ///
+ /// This function adds PRL option comprising the following option codes:
+ /// - 5 - Name Server
+ /// - 15 - Domain Name
+ /// - 7 - Log Server
+ /// - 8 - Quotes Server
+ /// - 9 - LPR Server
+ ///
+ /// @param pkt packet to add PRL option to.
+ void addPrlOption(Pkt4Ptr& pkt);
+
+ /// @brief Configures options being requested in the PRL option.
+ ///
+ /// The lpr-servers option is NOT configured here although it is
+ /// added to the 'Parameter Request List' option in the
+ /// \ref addPrlOption. When requested option is not configured
+ /// the server should not return it in its response. The goal
+ /// of not configuring the requested option is to verify that
+ /// the server will not return it.
+ void configureRequestedOptions();
+
+ /// @brief checks that the response matches request
+ /// @param q query (client's message)
+ /// @param a answer (server's message)
+ void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a);
+
+ /// @brief Check that certain basic (always added when lease is acquired)
+ /// are present in a message.
+ ///
+ /// @param pkt A message to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult basicOptionsPresent(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain basic (always added when lease is acquired)
+ /// are not present.
+ ///
+ /// @param pkt A packet to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult noBasicOptions(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain requested options are present in the message.
+ ///
+ /// @param pkt A message to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult requestedOptionsPresent(const Pkt4Ptr& pkt);
+
+ /// @brief Check that certain options (requested with PRL option)
+ /// are not present.
+ ///
+ /// @param pkt A packet to be checked.
+ /// @return Assertion result which indicates whether test passed or failed.
+ ::testing::AssertionResult noRequestedOptions(const Pkt4Ptr& pkt);
+
+ /// @brief generates client-id option
+ ///
+ /// Generate client-id option of specified length
+ /// Ids with different lengths are sufficent to generate
+ /// unique ids. If more fine grained control is required,
+ /// tests generate client-ids on their own.
+ /// Sets client_id_ field.
+ /// @param size size of the client-id to be generated
+ OptionPtr generateClientId(size_t size = 4);
+
+ /// @brief generate hardware address
+ ///
+ /// @param size size of the generated MAC address
+ /// @param pointer to Hardware Address object
+ HWAddrPtr generateHWAddr(size_t size = 6);
+
+ /// Check that address was returned from proper range, that its lease
+ /// lifetime is correct, that T1 and T2 are returned properly
+ /// @param rsp response to be checked
+ /// @param subnet subnet that should be used to verify assigned address
+ /// and options
+ /// @param t1_mandatory is T1 mandatory?
+ /// @param t2_mandatory is T2 mandatory?
+ void checkAddressParams(const Pkt4Ptr& rsp, const SubnetPtr subnet,
+ bool t1_mandatory = false,
+ bool t2_mandatory = false);
+
+ /// @brief Basic checks for generated response (message type and trans-id).
+ ///
+ /// @param rsp response packet to be validated
+ /// @param expected_message_type expected message type
+ /// @param expected_transid expected transaction-id
+ void checkResponse(const Pkt4Ptr& rsp, uint8_t expected_message_type,
+ uint32_t expected_transid);
+
+ /// @brief Checks if the lease sent to client is present in the database
+ ///
+ /// @param rsp response packet to be validated
+ /// @param client_id expected client-identifier (or NULL)
+ /// @param HWAddr expected hardware address (not used now)
+ /// @param expected_addr expected address
+ Lease4Ptr checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id,
+ const HWAddrPtr&,
+ const isc::asiolink::IOAddress& expected_addr);
+
+ /// @brief Checks if server response (OFFER, ACK, NAK) includes proper server-id
+ /// @param rsp response packet to be validated
+ /// @param expected_srvid expected value of server-id
+ void checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid);
+
+ /// @brief Checks if server response (OFFER, ACK, NAK) includes proper client-id
+ ///
+ /// This method follows values reported by CfgMgr in echoClientId() method.
+ /// Depending on its configuration, the client-id is either mandatory or
+ /// forbidden to appear in the response.
+ ///
+ /// @param rsp response packet to be validated
+ /// @param expected_clientid expected value of client-id
+ void checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid);
+
+ /// @brief sets default fields in a captured packet
+ ///
+ /// Sets UDP ports, addresses and interface.
+ ///
+ /// @param pkt packet to have default fields set
+ void captureSetDefaultFields(const Pkt4Ptr& pkt);
+
+ /// @brief returns captured DISCOVER that went through a relay
+ ///
+ /// See method code for a detailed explanation. This is a discover from
+ /// docsis3.0 device (Cable Modem)
+ ///
+ /// @return relayed DISCOVER
+ Pkt4Ptr captureRelayedDiscover();
+
+ /// @brief Create packet from output buffer of another packet.
+ ///
+ /// This function creates a packet using an output buffer from another
+ /// packet. This imitates reception of a packet from the wire. The
+ /// unpack function is then called to parse packet contents and to
+ /// create a collection of the options carried by this packet.
+ ///
+ /// This function is useful for unit tests which verify that the received
+ /// packet is parsed correctly. In those cases it is usually inappropriate
+ /// to create an instance of the packet, add options, set packet
+ /// fields and use such packet as an input to processDiscover or
+ /// processRequest function. This is because, such a packet has certain
+ /// options already initialized and there is no way to verify that they
+ /// have been initialized when packet instance was created or wire buffer
+ /// processing. By creating a packet from the buffer we guarantee that the
+ /// new packet is entirely initialized during wire data parsing.
+ ///
+ /// @param src_pkt A source packet, to be copied.
+ /// @param [out] dst_pkt A destination packet.
+ ///
+ /// @return assertion result indicating if a function completed with
+ /// success or failure.
+ static ::testing::AssertionResult
+ createPacketFromBuffer(const Pkt4Ptr& src_pkt,
+ Pkt4Ptr& dst_pkt);
+
+ /// @brief returns captured DISCOVER that went through a relay
+ ///
+ /// See method code for a detailed explanation. This is a discover from
+ /// eRouter1.0 device (CPE device integrated with cable modem)
+ ///
+ /// @return relayed DISCOVER
+ Pkt4Ptr captureRelayedDiscover2();
+
+ /// @brief generates a DHCPv4 packet based on provided hex string
+ ///
+ /// @return created packet
+ Pkt4Ptr packetFromCapture(const std::string& hex_string);
+
+ /// @brief This function cleans up after the test.
+ virtual void TearDown();
+
+ /// @brief A subnet used in most tests
+ Subnet4Ptr subnet_;
+
+ /// @brief A pool used in most tests
+ Pool4Ptr pool_;
+
+ /// @brief A client-id used in most tests
+ ClientIdPtr client_id_;
+
+ int rcode_;
+
+ isc::data::ConstElementPtr comment_;
+
+};
+
+/// @brief Test fixture class to be used for tests which require fake
+/// interfaces.
+///
+/// The DHCPv4 server must always append the server identifier to its response.
+/// The server identifier is typically an IP address assigned to the interface
+/// on which the query has been received. The DHCPv4 server uses IfaceMgr to
+/// check this address. In order to test this functionality, a set of interfaces
+/// must be known to the test. This test fixture class creates a set of well
+/// known (fake) interfaces which can be assigned to the test DHCPv4 messages
+/// so as the response (including server identifier) can be validated.
+/// The real interfaces are removed from the IfaceMgr in the constructor and
+/// they are re-assigned in the destructor.
+class Dhcpv4SrvFakeIfaceTest : public Dhcpv4SrvTest {
+public:
+ /// @brief Constructor.
+ ///
+ /// Creates a set of fake interfaces:
+ /// - lo, index: 0, address: 127.0.0.1
+ /// - eth0, index: 1, address: 192.0.3.1
+ /// - eth1, index: 2, address: 10.0.0.1
+ ///
+ /// These interfaces replace the real interfaces detected by the IfaceMgr.
+ Dhcpv4SrvFakeIfaceTest();
+
+ /// @brief Restores the original interface configuration.
+ virtual void TearDown();
+
+ /// @brief Creates an instance of the interface.
+ ///
+ /// @param name Name of the interface.
+ /// @param ifindex Index of the interface.
+ /// @param addr IP address assigned to the interface, represented as string.
+ ///
+ /// @return Iface Instance of the interface.
+ static Iface createIface(const std::string& name, const int ifindex,
+ const std::string& addr);
+
+ /// @brief Tests if Discover or Request message is processed correctly
+ ///
+ /// This test verifies that the Parameter Request List option is handled
+ /// correctly, i.e. it checks that certain options are present in the
+ /// server's response when they are requested and that they are not present
+ /// when they are not requested or NAK occurs.
+ ///
+ /// @todo We need an additional test for PRL option using real traffic
+ /// capture.
+ ///
+ /// @param msg_type DHCPDISCOVER or DHCPREQUEST
+ void testDiscoverRequest(const uint8_t msg_type);
+
+ /// @brief Holds a pointer to the packet filter object currently used
+ /// by the IfaceMgr.
+ PktFilterTestPtr current_pkt_filter_;
+
+};
+
+/// @brief "Naked" DHCPv4 server, exposes internal fields
+class NakedDhcpv4Srv: public Dhcpv4Srv {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// This constructor disables default modes of operation used by the
+ /// Dhcpv4Srv class:
+ /// - Send/receive broadcast messages through sockets on interfaces
+ /// which support broadcast traffic.
+ /// - Direct DHCPv4 traffic - communication with clients which do not
+ /// have IP address assigned yet.
+ ///
+ /// Enabling these modes requires root privilges so they must be
+ /// disabled for unit testing.
+ ///
+ /// Note, that disabling broadcast options on sockets does not impact
+ /// the operation of these tests because they use local loopback
+ /// interface which doesn't have broadcast capability anyway. It rather
+ /// prevents setting broadcast options on other (broadcast capable)
+ /// sockets which are opened on other interfaces in Dhcpv4Srv constructor.
+ ///
+ /// The Direct DHCPv4 Traffic capability can be disabled here because
+ /// it is tested with PktFilterLPFTest unittest. The tests which belong
+ /// to PktFilterLPFTest can be enabled on demand when root privileges can
+ /// be guaranteed.
+ ///
+ /// @param port port number to listen on; the default value 0 indicates
+ /// that sockets should not be opened.
+ NakedDhcpv4Srv(uint16_t port = 0)
+ : Dhcpv4Srv(port, "type=memfile", false, false) {
+ // Create fixed server id.
+ server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER,
+ asiolink::IOAddress("192.0.3.1")));
+ }
+
+ /// @brief Returns fixed server identifier assigned to the naked server
+ /// instance.
+ OptionPtr getServerID() const {
+ return (server_id_);
+ }
+
+ /// @brief fakes packet reception
+ /// @param timeout ignored
+ ///
+ /// The method receives all packets queued in receive queue, one after
+ /// another. Once the queue is empty, it initiates the shutdown procedure.
+ ///
+ /// See fake_received_ field for description
+ virtual Pkt4Ptr receivePacket(int /*timeout*/) {
+
+ // If there is anything prepared as fake incoming traffic, use it
+ if (!fake_received_.empty()) {
+ Pkt4Ptr pkt = fake_received_.front();
+ fake_received_.pop_front();
+ return (pkt);
+ }
+
+ // If not, just trigger shutdown and return immediately
+ shutdown();
+ return (Pkt4Ptr());
+ }
+
+ /// @brief fake packet sending
+ ///
+ /// Pretend to send a packet, but instead just store it in fake_send_ list
+ /// where test can later inspect server's response.
+ virtual void sendPacket(const Pkt4Ptr& pkt) {
+ fake_sent_.push_back(pkt);
+ }
+
+ /// @brief adds a packet to fake receive queue
+ ///
+ /// See fake_received_ field for description
+ void fakeReceive(const Pkt4Ptr& pkt) {
+ pkt->setIface("eth0");
+ fake_received_.push_back(pkt);
+ }
+
+ virtual ~NakedDhcpv4Srv() {
+ }
+
+ /// @brief Dummy server identifier option used by various tests.
+ OptionPtr server_id_;
+
+ /// @brief packets we pretend to receive
+ ///
+ /// Instead of setting up sockets on interfaces that change between OSes, it
+ /// is much easier to fake packet reception. This is a list of packets that
+ /// we pretend to have received. You can schedule new packets to be received
+ /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods.
+ std::list<Pkt4Ptr> fake_received_;
+
+ std::list<Pkt4Ptr> fake_sent_;
+
+ using Dhcpv4Srv::adjustIfaceData;
+ using Dhcpv4Srv::appendServerID;
+ using Dhcpv4Srv::processDiscover;
+ using Dhcpv4Srv::processRequest;
+ using Dhcpv4Srv::processRelease;
+ using Dhcpv4Srv::processDecline;
+ using Dhcpv4Srv::processInform;
+ using Dhcpv4Srv::processClientName;
+ using Dhcpv4Srv::computeDhcid;
+ using Dhcpv4Srv::createNameChangeRequests;
+ using Dhcpv4Srv::acceptServerId;
+ using Dhcpv4Srv::sanityCheck;
+ using Dhcpv4Srv::srvidToString;
+ using Dhcpv4Srv::unpackOptions;
+ using Dhcpv4Srv::name_change_reqs_;
+ using Dhcpv4Srv::classifyPacket;
+};
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // DHCP4_TEST_UTILS_H
diff --git a/src/bin/dhcp4/tests/fqdn_unittest.cc b/src/bin/dhcp4/tests/fqdn_unittest.cc
new file mode 100644
index 0000000..d3bf9ae
--- /dev/null
+++ b/src/bin/dhcp4/tests/fqdn_unittest.cc
@@ -0,0 +1,781 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <dhcp_ddns/ncr_msg.h>
+
+#include <gtest/gtest.h>
+#include <boost/scoped_ptr.hpp>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::dhcp_ddns;
+
+namespace {
+
+class NameDhcpv4SrvTest : public Dhcpv4SrvFakeIfaceTest {
+public:
+ NameDhcpv4SrvTest() : Dhcpv4SrvFakeIfaceTest() {
+ srv_ = new NakedDhcpv4Srv(0);
+ }
+ virtual ~NameDhcpv4SrvTest() {
+ delete srv_;
+ }
+
+ // Create a lease to be used by various tests.
+ Lease4Ptr createLease(const isc::asiolink::IOAddress& addr,
+ const std::string& hostname,
+ const bool fqdn_fwd,
+ const bool fqdn_rev) {
+ const uint8_t hwaddr[] = { 0, 1, 2, 3, 4, 5, 6 };
+ Lease4Ptr lease(new Lease4(addr, hwaddr, sizeof(hwaddr),
+ &generateClientId()->getData()[0],
+ generateClientId()->getData().size(),
+ 100, 50, 75, time(NULL), subnet_->getID()));
+ // @todo Set this through the Lease4 constructor.
+ lease->hostname_ = hostname;
+ lease->fqdn_fwd_ = fqdn_fwd;
+ lease->fqdn_rev_ = fqdn_rev;
+
+ return (lease);
+ }
+
+ // Create an instance of the DHCPv4 Client FQDN Option.
+ Option4ClientFqdnPtr
+ createClientFqdn(const uint8_t flags,
+ const std::string& fqdn_name,
+ Option4ClientFqdn::DomainNameType fqdn_type) {
+ return (Option4ClientFqdnPtr(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::
+ RCODE_CLIENT(),
+ fqdn_name,
+ fqdn_type)));
+ }
+
+ // Create an instance of the Hostname option.
+ OptionCustomPtr
+ createHostname(const std::string& hostname) {
+ OptionDefinition def("hostname", DHO_HOST_NAME, "string");
+ OptionCustomPtr opt_hostname(new OptionCustom(def, Option::V4));
+ opt_hostname->writeString(hostname);
+ return (opt_hostname);
+ }
+
+ // Generates partial hostname from the address. The format of the
+ // generated address is: host-A-B-C-D, where A.B.C.D is an IP
+ // address.
+ std::string generatedNameFromAddress(const IOAddress& addr) {
+ std::string gen_name = addr.toText();
+ std::replace(gen_name.begin(), gen_name.end(), '.', '-');
+ std::ostringstream hostname;
+ hostname << "host-" << gen_name;
+ return (hostname.str());
+ }
+
+ // Get the Client FQDN Option from the given message.
+ Option4ClientFqdnPtr getClientFqdnOption(const Pkt4Ptr& pkt) {
+ return (boost::dynamic_pointer_cast<
+ Option4ClientFqdn>(pkt->getOption(DHO_FQDN)));
+ }
+
+ // get the Hostname option from the given message.
+ OptionCustomPtr getHostnameOption(const Pkt4Ptr& pkt) {
+ return (boost::dynamic_pointer_cast<
+ OptionCustom>(pkt->getOption(DHO_HOST_NAME)));
+ }
+
+ // Create a message holding DHCPv4 Client FQDN Option.
+ Pkt4Ptr generatePktWithFqdn(const uint8_t msg_type,
+ const uint8_t fqdn_flags,
+ const std::string& fqdn_domain_name,
+ Option4ClientFqdn::DomainNameType fqdn_type,
+ const bool include_prl,
+ const bool include_clientid = true) {
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ pkt->setIface("eth0");
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ if (include_clientid) {
+ pkt->addOption(generateClientId());
+ }
+
+ // Create Client FQDN Option with the specified flags and
+ // domain-name.
+ pkt->addOption(createClientFqdn(fqdn_flags, fqdn_domain_name,
+ fqdn_type));
+
+ // Control whether or not to request that server returns the FQDN
+ // option. Server may be configured to always return it or return
+ // only in case client requested it.
+ if (include_prl) {
+ OptionUint8ArrayPtr option_prl =
+ OptionUint8ArrayPtr(new OptionUint8Array(Option::V4,
+ DHO_DHCP_PARAMETER_REQUEST_LIST));
+ option_prl->addValue(DHO_FQDN);
+ }
+ return (pkt);
+ }
+
+ // Create a message holding a Hostname option.
+ Pkt4Ptr generatePktWithHostname(const uint8_t msg_type,
+ const std::string& hostname) {
+
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("192.0.2.3"));
+ // For DISCOVER we don't include server id, because client broadcasts
+ // the message to all servers.
+ if (msg_type != DHCPDISCOVER) {
+ pkt->addOption(srv_->getServerID());
+ }
+
+ pkt->addOption(generateClientId());
+
+
+ // Create Client FQDN Option with the specified flags and
+ // domain-name.
+ pkt->addOption(createHostname(hostname));
+
+ return (pkt);
+
+ }
+
+ // Test that server generates the appropriate FQDN option in response to
+ // client's FQDN option.
+ void testProcessFqdn(const Pkt4Ptr& query, const uint8_t exp_flags,
+ const std::string& exp_domain_name,
+ Option4ClientFqdn::DomainNameType
+ exp_domain_type = Option4ClientFqdn::FULL) {
+ ASSERT_TRUE(getClientFqdnOption(query));
+
+ Pkt4Ptr answer;
+ if (query->getType() == DHCPDISCOVER) {
+ answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+ } else {
+ answer.reset(new Pkt4(DHCPACK, 1234));
+
+ }
+ ASSERT_NO_THROW(srv_->processClientName(query, answer));
+
+ Option4ClientFqdnPtr fqdn = getClientFqdnOption(answer);
+ ASSERT_TRUE(fqdn);
+
+ const bool flag_n = (exp_flags & Option4ClientFqdn::FLAG_N) != 0;
+ const bool flag_s = (exp_flags & Option4ClientFqdn::FLAG_S) != 0;
+ const bool flag_o = (exp_flags & Option4ClientFqdn::FLAG_O) != 0;
+ const bool flag_e = (exp_flags & Option4ClientFqdn::FLAG_E) != 0;
+
+ EXPECT_EQ(flag_n, fqdn->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ(flag_s, fqdn->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_EQ(flag_o, fqdn->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_EQ(flag_e, fqdn->getFlag(Option4ClientFqdn::FLAG_E));
+
+ EXPECT_EQ(exp_domain_name, fqdn->getDomainName());
+ EXPECT_EQ(exp_domain_type, fqdn->getDomainNameType());
+
+ }
+
+ // Processes the Hostname option in the client's message and returns
+ // the hostname option which would be sent to the client. It will
+ // throw NULL pointer if the hostname option is not to be included
+ // in the response.
+ OptionCustomPtr processHostname(const Pkt4Ptr& query) {
+ if (!getHostnameOption(query)) {
+ ADD_FAILURE() << "Hostname option not carried in the query";
+ }
+
+ Pkt4Ptr answer;
+ if (query->getType() == DHCPDISCOVER) {
+ answer.reset(new Pkt4(DHCPOFFER, 1234));
+
+ } else {
+ answer.reset(new Pkt4(DHCPACK, 1234));
+
+ }
+ srv_->processClientName(query, answer);
+
+ OptionCustomPtr hostname = getHostnameOption(answer);
+ return (hostname);
+
+ }
+
+ // Test that the client message holding an FQDN is processed and the
+ // NameChangeRequests are generated.
+ void testProcessMessageWithFqdn(const uint8_t msg_type,
+ const std::string& hostname) {
+ Pkt4Ptr req = generatePktWithFqdn(msg_type, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E, hostname,
+ Option4ClientFqdn::FULL, true);
+ Pkt4Ptr reply;
+ if (msg_type == DHCPDISCOVER) {
+ ASSERT_NO_THROW(reply = srv_->processDiscover(req));
+
+ } else if (msg_type == DHCPREQUEST) {
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ } else if (msg_type == DHCPRELEASE) {
+ ASSERT_NO_THROW(srv_->processRelease(req));
+ return;
+
+ } else {
+ return;
+ }
+
+ if (msg_type == DHCPDISCOVER) {
+ checkResponse(reply, DHCPOFFER, 1234);
+
+ } else {
+ checkResponse(reply, DHCPACK, 1234);
+ }
+
+ }
+
+ // Verify that NameChangeRequest holds valid values.
+ void verifyNameChangeRequest(const isc::dhcp_ddns::NameChangeType type,
+ const bool reverse, const bool forward,
+ const std::string& addr,
+ const std::string& fqdn,
+ const std::string& dhcid,
+ const time_t cltt,
+ const uint16_t len,
+ const bool not_strict_expire_check = false) {
+ NameChangeRequest ncr = srv_->name_change_reqs_.front();
+ EXPECT_EQ(type, ncr.getChangeType());
+ EXPECT_EQ(forward, ncr.isForwardChange());
+ EXPECT_EQ(reverse, ncr.isReverseChange());
+ EXPECT_EQ(addr, ncr.getIpAddress());
+ EXPECT_EQ(fqdn, ncr.getFqdn());
+ // Compare dhcid if it is not empty. In some cases, the DHCID is
+ // not known in advance and can't be compared.
+ if (!dhcid.empty()) {
+ EXPECT_EQ(dhcid, ncr.getDhcid().toStr());
+ }
+ // In some cases, the test doesn't have access to the last transmission
+ // time for the particular client. In such cases, the test can use the
+ // current time as cltt but the it may not check the lease expiration time
+ // for equality but rather check that the lease expiration time is not
+ // greater than the current time + lease lifetime.
+ if (not_strict_expire_check) {
+ EXPECT_GE(cltt + len, ncr.getLeaseExpiresOn());
+ } else {
+ EXPECT_EQ(cltt + len, ncr.getLeaseExpiresOn());
+ }
+ EXPECT_EQ(len, ncr.getLeaseLength());
+ EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr.getStatus());
+ srv_->name_change_reqs_.pop();
+ }
+
+ NakedDhcpv4Srv* srv_;
+
+};
+
+// Test that the exception is thrown if lease pointer specified as the argument
+// of computeDhcid function is NULL.
+TEST_F(NameDhcpv4SrvTest, dhcidNullLease) {
+ Lease4Ptr lease;
+ EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+
+}
+
+// Test that the appropriate exception is thrown if the lease object used
+// to compute DHCID comprises wrong hostname.
+TEST_F(NameDhcpv4SrvTest, dhcidWrongHostname) {
+ // First, make sure that the lease with the correct hostname is accepted.
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+ "myhost.example.com.", true, true);
+ ASSERT_NO_THROW(srv_->computeDhcid(lease));
+
+ // Now, use the wrong hostname. It should result in the exception.
+ lease->hostname_ = "myhost...example.com.";
+ EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+
+ // Also, empty hostname is wrong.
+ lease->hostname_ = "";
+ EXPECT_THROW(srv_->computeDhcid(lease), isc::dhcp::DhcidComputeError);
+}
+
+// Test that the DHCID is computed correctly, when the lease holds
+// correct hostname and non-NULL client id.
+TEST_F(NameDhcpv4SrvTest, dhcidComputeFromClientId) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+ "myhost.example.com.",
+ true, true);
+ isc::dhcp_ddns::D2Dhcid dhcid;
+ ASSERT_NO_THROW(dhcid = srv_->computeDhcid(lease));
+
+ // Make sure that the computed DHCID is valid.
+ std::string dhcid_ref = "00010132E91AA355CFBB753C0F0497A5A9404"
+ "36965B68B6D438D98E680BF10B09F3BCF";
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+// Test that the DHCID is computed correctly, when the lease holds correct
+// hostname and NULL client id.
+TEST_F(NameDhcpv4SrvTest, dhcidComputeFromHWAddr) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"),
+ "myhost.example.com.",
+ true, true);
+ lease->client_id_.reset();
+
+ isc::dhcp_ddns::D2Dhcid dhcid;
+ ASSERT_NO_THROW(dhcid = srv_->computeDhcid(lease));
+
+ // Make sure that the computed DHCID is valid.
+ std::string dhcid_ref = "0000012247F6DC4423C3E8627434A9D6868609"
+ "D88948F78018B215EDCAA30C0C135035";
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+
+// Test that server confirms to perform the forward and reverse DNS update,
+// when client asks for it.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL,
+ true);
+
+ testProcessFqdn(query,
+ Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+
+}
+
+// Test that server processes the Hostname option sent by a client and
+// responds with the Hostname option to confirm that the server has
+// taken responsibility for the update.
+TEST_F(NameDhcpv4SrvTest, serverUpdateHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ "myhost.example.com."));
+ OptionCustomPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("myhost.example.com.", hostname->readString());
+
+}
+
+// Test that the server skips processing of the empty Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateEmptyHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, ""));
+ OptionCustomPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+ EXPECT_FALSE(hostname);
+}
+
+// Test that the server skips processing of a wrong Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateWrongHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST,
+ "abc..example.com"));
+ OptionCustomPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+ EXPECT_FALSE(hostname);
+}
+
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the partial name.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardPartialNameFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S,
+ "myhost",
+ Option4ClientFqdn::PARTIAL,
+ true);
+
+ testProcessFqdn(query,
+ Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+
+}
+
+// Test that server generates the fully qualified domain name for the client
+// if client supplies the unqualified name in the Hostname option.
+TEST_F(NameDhcpv4SrvTest, serverUpdateUnqualifiedHostname) {
+ Pkt4Ptr query;
+ ASSERT_NO_THROW(query = generatePktWithHostname(DHCPREQUEST, "myhost"));
+ OptionCustomPtr hostname;
+ ASSERT_NO_THROW(hostname = processHostname(query));
+
+ ASSERT_TRUE(hostname);
+ EXPECT_EQ("myhost.example.com.", hostname->readString());
+
+}
+
+// Test that server sets empty domain-name in the FQDN option when client
+// supplied no domain-name. The domain-name is supposed to be set after the
+// lease is acquired. The domain-name is then generated from the IP address
+// assigned to a client.
+TEST_F(NameDhcpv4SrvTest, serverUpdateForwardNoNameFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S,
+ "",
+ Option4ClientFqdn::PARTIAL,
+ true);
+
+ testProcessFqdn(query,
+ Option4ClientFqdn::FLAG_E | Option4ClientFqdn::FLAG_S,
+ "", Option4ClientFqdn::PARTIAL);
+
+}
+
+// Test server's response when client requests no DNS update.
+TEST_F(NameDhcpv4SrvTest, noUpdateFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL,
+ true);
+ testProcessFqdn(query, Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_N,
+ "myhost.example.com.");
+}
+
+// Test that server does not accept delegation of the forward DNS update
+// to a client.
+TEST_F(NameDhcpv4SrvTest, clientUpdateNotAllowedFqdn) {
+ Pkt4Ptr query = generatePktWithFqdn(DHCPREQUEST,
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL,
+ true);
+
+ testProcessFqdn(query, Option4ClientFqdn::FLAG_E |
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_O,
+ "myhost.example.com.");
+
+}
+
+// Test that exactly one NameChangeRequest is generated when the new lease
+// has been acquired (old lease is NULL).
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNewLease) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+ true, true);
+ Lease4Ptr old_lease;
+
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ "192.0.2.3", "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436965"
+ "B68B6D438D98E680BF10B09F3BCF",
+ lease->cltt_, 100);
+}
+
+// Test that no NameChangeRequest is generated when a lease is renewed and
+// the FQDN data hasn't changed.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenewNoChange) {
+ Lease4Ptr lease = createLease(IOAddress("192.0.2.3"), "myhost.example.com.",
+ true, true);
+ Lease4Ptr old_lease = createLease(IOAddress("192.0.2.3"),
+ "myhost.example.com.", true, true);
+ old_lease->valid_lft_ += 100;
+
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease, old_lease));
+ EXPECT_TRUE(srv_->name_change_reqs_.empty());
+}
+
+// Test that no NameChangeRequest is generated when forward and reverse
+// DNS update flags are not set in the lease.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsNoUpdate) {
+ Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+ "lease1.example.com.",
+ true, true);
+ Lease4Ptr lease2 = createLease(IOAddress("192.0.2.3"),
+ "lease2.example.com.",
+ false, false);
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+ EXPECT_EQ(1, srv_->name_change_reqs_.size());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "192.0.2.3", "lease1.example.com.",
+ "0001013A5B311F5B9FB10DDF8E53689B874F25D"
+ "62CC147C2FF237A64C90E5A597C9B7A",
+ lease1->cltt_, 100);
+
+ lease2->hostname_ = "";
+ lease2->fqdn_rev_ = true;
+ lease2->fqdn_fwd_ = true;
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+ EXPECT_EQ(1, srv_->name_change_reqs_.size());
+
+}
+
+// Test that two NameChangeRequests are generated when the lease is being
+// renewed and the new lease has updated FQDN data.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsRenew) {
+ Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+ "lease1.example.com.",
+ true, true);
+ Lease4Ptr lease2 = createLease(IOAddress("192.0.2.3"),
+ "lease2.example.com.",
+ true, true);
+ ASSERT_NO_THROW(srv_->createNameChangeRequests(lease2, lease1));
+ ASSERT_EQ(2, srv_->name_change_reqs_.size());
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "192.0.2.3", "lease1.example.com.",
+ "0001013A5B311F5B9FB10DDF8E53689B874F25D"
+ "62CC147C2FF237A64C90E5A597C9B7A",
+ lease1->cltt_, 100);
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ "192.0.2.3", "lease2.example.com.",
+ "000101F906D2BB752E1B2EECC5FF2BF434C0B2D"
+ "D6D7F7BD873F4F280165DB8C9DBA7CB",
+ lease2->cltt_, 100);
+
+}
+
+// This test verifies that exception is thrown when leases passed to the
+// createNameChangeRequests function do not match, i.e. they comprise
+// different IP addresses, client ids etc.
+TEST_F(NameDhcpv4SrvTest, createNameChangeRequestsLeaseMismatch) {
+ Lease4Ptr lease1 = createLease(IOAddress("192.0.2.3"),
+ "lease1.example.com.",
+ true, true);
+ Lease4Ptr lease2 = createLease(IOAddress("192.0.2.4"),
+ "lease2.example.com.",
+ true, true);
+ EXPECT_THROW(srv_->createNameChangeRequests(lease2, lease1),
+ isc::Unexpected);
+}
+
+// Test that the OFFER message generated as a result of the DISCOVER message
+// processing will not result in generation of the NameChangeRequests.
+TEST_F(NameDhcpv4SrvTest, processDiscover) {
+ Pkt4Ptr req = generatePktWithFqdn(DHCPDISCOVER, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processDiscover(req));
+ checkResponse(reply, DHCPOFFER, 1234);
+
+ EXPECT_TRUE(srv_->name_change_reqs_.empty());
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when DHCPv4 Client FQDN option specifies an empty domain-name.
+TEST_F(NameDhcpv4SrvTest, processRequestFqdnEmptyDomainName) {
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "", Option4ClientFqdn::PARTIAL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+ // The hostname is generated from the IP address acquired (yiaddr).
+ std::ostringstream hostname;
+ hostname << generatedNameFromAddress(reply->getYiaddr())
+ << ".example.com.";
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), hostname.str(),
+ "", // empty DHCID forces that it is not checked
+ time(NULL) + subnet_->getValid(),
+ subnet_->getValid(), true);
+}
+
+// Test that server generates client's hostname from the IP address assigned
+// to it when Hostname option carries the top level domain-name.
+TEST_F(NameDhcpv4SrvTest, processRequestEmptyHostname) {
+ Pkt4Ptr req = generatePktWithHostname(DHCPREQUEST, ".");
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req->setIface("eth0");
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+ // The hostname is generated from the IP address acquired (yiaddr).
+ std::ostringstream hostname;
+ hostname << generatedNameFromAddress(reply->getYiaddr()) << ".example.com.";
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), hostname.str(),
+ "", // empty DHCID forces that it is not checked
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying FQDN option with
+// a different domain-name. Server should use existing lease for the second
+// request but modify the DNS entries for the lease according to the contents
+// of the FQDN sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsFqdn) {
+ Pkt4Ptr req1 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message but with a different FQDN. Server
+ // should generate two NameChangeRequests: one to remove existing entry,
+ // another one to add new entry with updated domain-name.
+ Pkt4Ptr req2 = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "otherhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be two NameChangeRequests. Verify that they are valid.
+ ASSERT_EQ(2, srv_->name_change_reqs_.size());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ reply->getYiaddr().toText(),
+ "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(),
+ "otherhost.example.com.",
+ "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+ "AFDCE8C3D0E53F35CC584DD63C89CA",
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that client may send two requests, each carrying Hostname option with
+// a different name. Server should use existing lease for the second request
+// but modify the DNS entries for the lease according to the contents of the
+// Hostname sent in the second request.
+TEST_F(NameDhcpv4SrvTest, processTwoRequestsHostname) {
+ Pkt4Ptr req1 = generatePktWithHostname(DHCPREQUEST, "myhost.example.com.");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req1->setIface("eth0");
+
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req1));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create another Request message but with a different Hostname. Server
+ // should generate two NameChangeRequests: one to remove existing entry,
+ // another one to add new entry with updated domain-name.
+ Pkt4Ptr req2 = generatePktWithHostname(DHCPREQUEST, "otherhost");
+
+ // Set interface for the incoming packet. The server requires it to
+ // generate client id.
+ req2->setIface("eth0");
+
+ ASSERT_NO_THROW(reply = srv_->processRequest(req2));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // There should be two NameChangeRequests. Verify that they are valid.
+ ASSERT_EQ(2, srv_->name_change_reqs_.size());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_REMOVE, true, true,
+ reply->getYiaddr().toText(),
+ "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(),
+ "otherhost.example.com.",
+ "000101A5AEEA7498BD5AD9D3BF600E49FF39A7E3"
+ "AFDCE8C3D0E53F35CC584DD63C89CA",
+ time(NULL), subnet_->getValid(), true);
+}
+
+// Test that when the Release message is sent for the previously acquired
+// lease, then server genenerates a NameChangeRequest to remove the entries
+// corresponding to the lease being released.
+TEST_F(NameDhcpv4SrvTest, processRequestRelease) {
+ Pkt4Ptr req = generatePktWithFqdn(DHCPREQUEST, Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ "myhost.example.com.",
+ Option4ClientFqdn::FULL, true);
+
+ Pkt4Ptr reply;
+ ASSERT_NO_THROW(reply = srv_->processRequest(req));
+
+ checkResponse(reply, DHCPACK, 1234);
+
+ // Verify that there is one NameChangeRequest generated.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+ verifyNameChangeRequest(isc::dhcp_ddns::CHG_ADD, true, true,
+ reply->getYiaddr().toText(), "myhost.example.com.",
+ "00010132E91AA355CFBB753C0F0497A5A940436"
+ "965B68B6D438D98E680BF10B09F3BCF",
+ time(NULL), subnet_->getValid(), true);
+
+ // Create a Release message.
+ Pkt4Ptr rel = Pkt4Ptr(new Pkt4(DHCPRELEASE, 1234));
+ rel->setCiaddr(reply->getYiaddr());
+ rel->setRemoteAddr(IOAddress("192.0.2.3"));
+ rel->addOption(generateClientId());
+ rel->addOption(srv_->getServerID());
+
+ ASSERT_NO_THROW(srv_->processRelease(rel));
+
+ // The lease has been removed, so there should be a NameChangeRequest to
+ // remove corresponding DNS entries.
+ ASSERT_EQ(1, srv_->name_change_reqs_.size());
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp4/tests/marker_file.cc b/src/bin/dhcp4/tests/marker_file.cc
new file mode 100644
index 0000000..d1c4aba
--- /dev/null
+++ b/src/bin/dhcp4/tests/marker_file.cc
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include "marker_file.h"
+
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+using namespace std;
+
+// Check the marker file.
+
+bool
+checkMarkerFile(const char* name, const char* expected) {
+ // Open the file for input
+ fstream file(name, fstream::in);
+
+ // Is it open?
+ if (!file.is_open()) {
+ ADD_FAILURE() << "Unable to open " << name << ". It was expected "
+ << "to be present and to contain the string '"
+ << expected << "'";
+ return (false);
+ }
+
+ // OK, is open, so read the data and see what we have. Compare it
+ // against what is expected.
+ string content;
+ getline(file, content);
+
+ string expected_str(expected);
+ EXPECT_EQ(expected_str, content) << "Marker file " << name
+ << "did not contain the expected data";
+ file.close();
+
+ return (expected_str == content);
+}
+
+// Check if the marker file exists - this is a wrapper for "access(2)" and
+// really tests if the file exists and is accessible
+
+bool
+checkMarkerFileExists(const char* name) {
+ return (access(name, F_OK) == 0);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/bin/dhcp4/tests/marker_file.h.in b/src/bin/dhcp4/tests/marker_file.h.in
new file mode 100644
index 0000000..52fc006
--- /dev/null
+++ b/src/bin/dhcp4/tests/marker_file.h.in
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef MARKER_FILE_H
+#define MARKER_FILE_H
+
+/// @file
+/// Define a marker file that is used in tests to prove that an "unload"
+/// function has been called
+
+namespace {
+const char* const LOAD_MARKER_FILE = "@abs_builddir@/load_marker.txt";
+const char* const UNLOAD_MARKER_FILE = "@abs_builddir@/unload_marker.txt";
+}
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Check marker file
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Marker files are used by the load/unload functions in the hooks
+/// libraries in these tests to signal whether they have been loaded or
+/// unloaded. The file (if present) contains a single line holding
+/// a set of characters.
+///
+/// This convenience function checks the file to see if the characters
+/// are those expected.
+///
+/// @param name Name of the marker file.
+/// @param expected Characters expected. If a marker file is present,
+/// it is expected to contain characters.
+///
+/// @return true if all tests pass, false if not (in which case a failure
+/// will have been logged).
+bool
+checkMarkerFile(const char* name, const char* expected);
+
+/// @brief Check marker file exists
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Checkes that the specified file does NOT exist.
+///
+/// @param name Name of the marker file.
+///
+/// @return true if file exists, false if not.
+bool
+checkMarkerFileExists(const char* name);
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+#endif // MARKER_FILE_H
+
diff --git a/src/bin/dhcp4/tests/test_data_files_config.h.in b/src/bin/dhcp4/tests/test_data_files_config.h.in
new file mode 100644
index 0000000..0b778cf
--- /dev/null
+++ b/src/bin/dhcp4/tests/test_data_files_config.h.in
@@ -0,0 +1,17 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @brief Path to dhcp4 source dir so tests against the dhcp4.spec file
+/// can find it reliably.
+#define DHCP4_SRC_DIR "@abs_top_srcdir@/src/bin/dhcp4"
diff --git a/src/bin/dhcp4/tests/test_libraries.h.in b/src/bin/dhcp4/tests/test_libraries.h.in
new file mode 100644
index 0000000..1cddf00
--- /dev/null
+++ b/src/bin/dhcp4/tests/test_libraries.h.in
@@ -0,0 +1,39 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+#define DLL_SUFFIX ".so"
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// shared library.
+
+// Library with load/unload functions creating marker files to check their
+// operation.
+const char* const CALLOUT_LIBRARY_1 = "@abs_builddir@/.libs/libco1.so";
+const char* const CALLOUT_LIBRARY_2 = "@abs_builddir@/.libs/libco2.so";
+
+// Name of a library which is not present.
+const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/bin/dhcp4/tests/wireshark.cc b/src/bin/dhcp4/tests/wireshark.cc
new file mode 100644
index 0000000..c07bc1b
--- /dev/null
+++ b/src/bin/dhcp4/tests/wireshark.cc
@@ -0,0 +1,190 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp4/tests/dhcp4_test_utils.h>
+#include <string>
+#include <asiolink/io_address.h>
+#include <util/encode/hex.h>
+
+/// @file wireshark.cc
+///
+/// @brief contains packet captures imported from Wireshark
+///
+/// These are actual packets captured over wire. They are used in various
+/// tests.
+///
+/// The procedure to export Wireshark -> unit-tests is manual, but rather
+/// easy to follow:
+/// 1. Open a file in wireshark
+/// 2. Find the packet you want to export
+/// 3. There's a protocol stack (Frame, Ethernet, IPv6, UDP, DHCPv6, ...)
+/// 4. Right click on DHCPv6 -> Copy -> Bytes -> Hex Stream
+/// 5. Paste it as: string hex_string="[paste here]";
+/// 6. Coding guidelines line restrictions apply, so wrap your code as necessary
+/// 7. Make sure you decribe the capture appropriately
+/// 8. Follow whatever rest of the methods are doing (set ports, ifaces etc.)
+/// 9. To easily copy packet description, click File... -> Extract packet
+/// dissections -> as plain text file...
+/// (Make sure that the packet is expanded in the view. The text file will
+/// contain whatever expansion level you have in the graphical tree.)
+
+using namespace std;
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+Pkt4Ptr Dhcpv4SrvTest::packetFromCapture(const std::string& hex_string) {
+ std::vector<uint8_t> bin;
+
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(hex_string, bin);
+
+ Pkt4Ptr pkt(new Pkt4(&bin[0], bin.size()));
+ captureSetDefaultFields(pkt);
+
+ return (pkt);
+}
+
+void Dhcpv4SrvTest::captureSetDefaultFields(const Pkt4Ptr& pkt) {
+ pkt->setRemotePort(546);
+ pkt->setRemoteAddr(IOAddress("fe80::1"));
+ pkt->setLocalPort(0);
+ pkt->setLocalAddr(IOAddress("ff02::1:2"));
+ pkt->setIndex(2);
+ pkt->setIface("eth0");
+}
+
+Pkt4Ptr Dhcpv4SrvTest::captureRelayedDiscover() {
+
+/* This is packet 1 from capture
+ dhcp-val/pcap/docsis-*-CG3000DCR-Registration-Filtered.cap
+
+string exported from Wireshark:
+
+User Datagram Protocol, Src Port: bootps (67), Dst Port: bootps (67)
+ Source port: bootps (67)
+ Destination port: bootps (67)
+ Length: 541
+ Checksum: 0x2181 [validation disabled]
+
+Bootstrap Protocol
+ Message type: Boot Request (1)
+ Hardware type: Ethernet
+ Hardware address length: 6
+ Hops: 1
+ Transaction ID: 0x5d05478d
+ Seconds elapsed: 0
+ Bootp flags: 0x0000 (Unicast)
+ Client IP address: 0.0.0.0 (0.0.0.0)
+ Your (client) IP address: 0.0.0.0 (0.0.0.0)
+ Next server IP address: 0.0.0.0 (0.0.0.0)
+ Relay agent IP address: 10.254.226.1 (10.254.226.1)
+ Client MAC address: Netgear_b8:15:14 (20:e5:2a:b8:15:14)
+ Client hardware address padding: 00000000000000000000
+ Server host name not given
+ Boot file name not given
+ Magic cookie: DHCP
+ Option: (53) DHCP Message Type
+ Option: (55) Parameter Request List
+ Option: (60) Vendor class identifier (docsis3.0)
+ Option: (125) V-I Vendor-specific Information
+ - suboption 1 (Option Request): requesting option 2
+ - suboption 5 (Modem Caps): 117 bytes
+ Option: (43) Vendor-Specific Information (CableLabs)
+ Option: (61) Client identifier
+ Option: (57) Maximum DHCP Message Size
+ Option: (82) Agent Information Option
+ Option: (255) End */
+
+ string hex_string =
+ "010106015d05478d000000000000000000000000000000000afee20120e52ab8151400"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000638253633501013707"
+ "0102030407067d3c0a646f63736973332e303a7d7f0000118b7a010102057501010102"
+ "010303010104010105010106010107010f0801100901030a01010b01180c01010d0200"
+ "400e0200100f010110040000000211010014010015013f160101170101180104190104"
+ "1a01041b01201c01021d01081e01201f01102001102101022201012301002401002501"
+ "01260200ff2701012b59020345434d030b45434d3a45524f55544552040d3242523232"
+ "39553430303434430504312e3034060856312e33332e30330707322e332e3052320806"
+ "30303039354209094347333030304443520a074e657467656172fe01083d0fff2ab815"
+ "140003000120e52ab81514390205dc5219010420000002020620e52ab8151409090000"
+ "118b0401020300ff";
+
+ return (packetFromCapture(hex_string));
+}
+
+Pkt4Ptr Dhcpv4SrvTest::captureRelayedDiscover2() {
+
+/* This is packet 5 from capture
+ dhcp-val/pcap/docsis-*-CG3000DCR-Registration-Filtered.cap
+
+string exported from Wireshark:
+
+User Datagram Protocol, Src Port: bootps (67), Dst Port: bootps (67)
+Bootstrap Protocol
+ Message type: Boot Request (1)
+ Hardware type: Ethernet (0x01)
+ Hardware address length: 6
+ Hops: 1
+ Transaction ID: 0x5d05478f
+ Seconds elapsed: 5
+ Bootp flags: 0x0000 (Unicast)
+ Client IP address: 0.0.0.0 (0.0.0.0)
+ Your (client) IP address: 0.0.0.0 (0.0.0.0)
+ Next server IP address: 0.0.0.0 (0.0.0.0)
+ Relay agent IP address: 10.254.226.1 (10.254.226.1)
+ Client MAC address: Netgear_b8:15:15 (20:e5:2a:b8:15:15)
+ Client hardware address padding: 00000000000000000000
+ Server host name not given
+ Boot file name not given
+ Magic cookie: DHCP
+ Option: (53) DHCP Message Type
+ Option: (55) Parameter Request List
+ Option: (43) Vendor-Specific Information
+ Option: (60) Vendor class identifier (eRouter1.0)
+ Option: (15) Domain Name
+ Option: (61) Client identifier
+ Option: (57) Maximum DHCP Message Size
+ Option: (82) Agent Information Option
+ Option: (255) End */
+
+ string hex_string =
+ "010106015d05478f000500000000000000000000000000000afee20120e52ab8151500"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000000000000"
+ "000000000000000000000000000000000000000000000000000063825363350101370e"
+ "480102030406070c0f171a36337a2b63020745524f55544552030b45434d3a45524f55"
+ "544552040d324252323239553430303434430504312e3034060856312e33332e303307"
+ "07322e332e305232080630303039354209094347333030304443520a074e6574676561"
+ "720f0745524f555445523c0a65526f75746572312e300f14687364312e70612e636f6d"
+ "636173742e6e65742e3d0fff2ab815150003000120e52ab81515390205dc5219010420"
+ "000002020620e52ab8151409090000118b0401020300ff";
+
+ return (packetFromCapture(hex_string));
+}
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
diff --git a/src/bin/dhcp6/.gitignore b/src/bin/dhcp6/.gitignore
index 5878189..dae9039 100644
--- a/src/bin/dhcp6/.gitignore
+++ b/src/bin/dhcp6/.gitignore
@@ -4,3 +4,4 @@
/dhcp6_messages.h
/spec_config.h
/spec_config.h.pre
+/s-messages
diff --git a/src/bin/dhcp6/Makefile.am b/src/bin/dhcp6/Makefile.am
index 3b07510..9c7a5df 100644
--- a/src/bin/dhcp6/Makefile.am
+++ b/src/bin/dhcp6/Makefile.am
@@ -17,7 +17,7 @@ endif
pkglibexecdir = $(libexecdir)/@PACKAGE@
-CLEANFILES = spec_config.h dhcp6_messages.h dhcp6_messages.cc
+CLEANFILES = spec_config.h dhcp6_messages.h dhcp6_messages.cc s-messages
man_MANS = b10-dhcp6.8
DISTCLEANFILES = $(man_MANS)
@@ -41,8 +41,11 @@ endif
spec_config.h: spec_config.h.pre
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" spec_config.h.pre >$@
-dhcp6_messages.h dhcp6_messages.cc: dhcp6_messages.mes
+dhcp6_messages.h dhcp6_messages.cc: s-messages
+
+s-messages: dhcp6_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/dhcp6/dhcp6_messages.mes
+ touch $@
BUILT_SOURCES = spec_config.h dhcp6_messages.h dhcp6_messages.cc
@@ -61,10 +64,12 @@ b10_dhcp6_LDADD = $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
b10_dhcp6_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+b10_dhcp6_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
b10_dhcp6dir = $(pkgdatadir)
b10_dhcp6_DATA = dhcp6.spec
diff --git a/src/bin/dhcp6/config_parser.cc b/src/bin/dhcp6/config_parser.cc
index 0c09361..32105ad 100644
--- a/src/bin/dhcp6/config_parser.cc
+++ b/src/bin/dhcp6/config_parser.cc
@@ -22,6 +22,7 @@
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/dbaccess_parser.h>
#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/dhcp_parsers.h>
#include <dhcpsrv/pool.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/triplet.h>
@@ -49,1006 +50,186 @@ using namespace isc::asiolink;
namespace {
-// Forward declarations of some of the parser classes.
-// They are used to define pointer types for these classes.
-class BooleanParser;
-class StringParser;
-class Uint32Parser;
-
// Pointers to various parser objects.
typedef boost::shared_ptr<BooleanParser> BooleanParserPtr;
typedef boost::shared_ptr<StringParser> StringParserPtr;
typedef boost::shared_ptr<Uint32Parser> Uint32ParserPtr;
-/// @brief Factory method that will create a parser for a given element name
-typedef isc::dhcp::DhcpConfigParser* ParserFactory(const std::string& config_id);
-
-/// @brief Collection of factories that create parsers for specified element names
-typedef std::map<std::string, ParserFactory*> FactoryMap;
-
-/// @brief Storage for option definitions.
-typedef OptionSpaceContainer<OptionDefContainer,
- OptionDefinitionPtr> OptionDefStorage;
-
-/// @brief Collection of address pools.
-///
-/// This type is used as intermediate storage, when pools are parsed, but there is
-/// no subnet object created yet to store them.
-typedef std::vector<isc::dhcp::Pool6Ptr> PoolStorage;
-
-/// Collection of containers holding option spaces. Each container within
-/// a particular option space holds so-called option descriptors.
-typedef OptionSpaceContainer<Subnet::OptionContainer,
- Subnet::OptionDescriptor> OptionStorage;
-
-/// @brief Global uint32 parameters that will be used as defaults.
-Uint32Storage uint32_defaults;
-
-/// @brief global string parameters that will be used as defaults.
-StringStorage string_defaults;
-
-/// @brief Global storage for options that will be used as defaults.
-OptionStorage option_defaults;
-
-/// @brief Global storage for option definitions.
-OptionDefStorage option_def_intermediate;
-
-/// @brief a dummy configuration parser
-///
-/// This is a debugging parser. It does not configure anything,
-/// will accept any configuration and will just print it out
-/// on commit. Useful for debugging existing configurations and
-/// adding new ones.
-class DebugParser : public DhcpConfigParser {
-public:
-
- /// @brief Constructor
- ///
- /// See @ref DhcpConfigParser class for details.
- ///
- /// @param param_name name of the parsed parameter
- DebugParser(const std::string& param_name)
- :param_name_(param_name) {
- }
-
- /// @brief builds parameter value
- ///
- /// See @ref DhcpConfigParser class for details.
- ///
- /// @param new_config pointer to the new configuration
- virtual void build(ConstElementPtr new_config) {
- std::cout << "Build for token: [" << param_name_ << "] = ["
- << value_->str() << "]" << std::endl;
- value_ = new_config;
- }
-
- /// @brief Pretends to apply the configuration.
- ///
- /// This is a method required by the base class. It pretends to apply the
- /// configuration, but in fact it only prints the parameter out.
- ///
- /// See @ref DhcpConfigParser class for details.
- virtual void commit() {
- // Debug message. The whole DebugParser class is used only for parser
- // debugging, and is not used in production code. It is very convenient
- // to keep it around. Please do not turn this cout into logger calls.
- std::cout << "Commit for token: [" << param_name_ << "] = ["
- << value_->str() << "]" << std::endl;
- }
-
- /// @brief factory that constructs DebugParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new DebugParser(param_name));
- }
-
-private:
- /// name of the parsed parameter
- std::string param_name_;
-
- /// pointer to the actual value of the parameter
- ConstElementPtr value_;
-};
-
-
-/// @brief A boolean value parser.
-///
-/// This parser handles configuration values of the boolean type.
-/// Parsed values are stored in a provided storage. If no storage
-/// is provided then the build function throws an exception.
-class BooleanParser : public DhcpConfigParser {
-public:
- /// @brief Constructor.
- ///
- /// @param param_name name of the parameter.
- BooleanParser(const std::string& param_name)
- : storage_(NULL),
- param_name_(param_name),
- value_(false) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief Parse a boolean value.
- ///
- /// @param value a value to be parsed.
- ///
- /// @throw isc::InvalidOperation if a storage has not been set
- /// prior to calling this function
- /// @throw isc::dhcp::DhcpConfigError if a provided parameter's
- /// name is empty.
- virtual void build(ConstElementPtr value) {
- if (storage_ == NULL) {
- isc_throw(isc::InvalidOperation, "parser logic error:"
- << " storage for the " << param_name_
- << " value has not been set");
- } else if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- // The Config Manager checks if user specified a
- // valid value for a boolean parameter: True or False.
- // It is then ok to assume that if str() does not return
- // 'true' the value is 'false'.
- value_ = (value->str() == "true") ? true : false;
- }
-
- /// @brief Put a parsed value to the storage.
- virtual void commit() {
- if (storage_ != NULL && !param_name_.empty()) {
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief Create an instance of the boolean parser.
- ///
- /// @param param_name name of the parameter for which the
- /// parser is created.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new BooleanParser(param_name));
- }
-
- /// @brief Set the storage for parsed value.
- ///
- /// This function must be called prior to calling build.
- ///
- /// @param storage a pointer to the storage where parsed data
- /// is to be stored.
- void setStorage(BooleanStorage* storage) {
- storage_ = storage;
- }
-
-private:
- /// Pointer to the storage where parsed value is stored.
- BooleanStorage* storage_;
- /// Name of the parameter which value is parsed with this parser.
- std::string param_name_;
- /// Parsed value.
- bool value_;
-};
-
-/// @brief Configuration parser for uint32 parameters
-///
-/// This class is a generic parser that is able to handle any uint32 integer
-/// type. By default it stores the value in external global container
-/// (uint32_defaults). If used in smaller scopes (e.g. to parse parameters
-/// in subnet config), it can be pointed to a different storage, using
-/// setStorage() method. This class follows the parser interface, laid out
-/// in its base class, @ref DhcpConfigParser.
-///
-/// For overview of usability of this generic purpose parser, see
-/// @ref dhcpv6ConfigInherit page.
-///
-/// @todo this class should be turned into the template class which
-/// will handle all uintX_types of data (see ticket #2415).
-class Uint32Parser : public DhcpConfigParser {
-public:
-
- /// @brief constructor for Uint32Parser
- ///
- /// @param param_name name of the configuration parameter being parsed
- Uint32Parser(const std::string& param_name)
- : storage_(&uint32_defaults),
- param_name_(param_name) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief Parses configuration configuration parameter as uint32_t.
- ///
- /// @param value pointer to the content of parsed values
- /// @throw isc::dhcp::DhcpConfigError if failed to parse
- /// the configuration parameter as uint32_t value.
- virtual void build(ConstElementPtr value) {
- if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
-
- bool parse_error = false;
- // Cast the provided value to int64 value to check.
- int64_t int64value = 0;
- try {
- // Parsing the value as a int64 value allows to
- // check if the provided value is within the range
- // of uint32_t (is not negative or greater than
- // maximal uint32_t value).
- int64value = boost::lexical_cast<int64_t>(value->str());
- } catch (const boost::bad_lexical_cast&) {
- parse_error = true;
- }
- if (!parse_error) {
- // Check that the value is not out of bounds.
- if ((int64value < 0) ||
- (int64value > std::numeric_limits<uint32_t>::max())) {
- parse_error = true;
- } else {
- // A value is not out of bounds so let's cast it to
- // the uint32_t type.
- value_ = static_cast<uint32_t>(int64value);
- }
-
- }
- // Invalid value provided.
- if (parse_error) {
- isc_throw(isc::dhcp::DhcpConfigError, "Failed to parse value " << value->str()
- << " as unsigned 32-bit integer.");
- }
- }
-
- /// @brief Stores the parsed uint32_t value in a storage.
- virtual void commit() {
- if (storage_ != NULL) {
- // If a given parameter already exists in the storage we override
- // its value. If it doesn't we insert a new element.
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief Factory that constructs Uint32Parser objects.
- ///
- /// @param param_name name of the parameter to be parsed.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new Uint32Parser(param_name));
- }
-
- /// @brief Sets storage for value of this parameter.
- ///
- /// See @ref dhcpv6ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container.
- void setStorage(Uint32Storage* storage) {
- storage_ = storage;
- }
-
-private:
- /// pointer to the storage, where parsed value will be stored
- Uint32Storage* storage_;
- /// name of the parameter to be parsed
- std::string param_name_;
- /// the actual parsed value
- uint32_t value_;
-};
-
-/// @brief Configuration parser for string parameters
-///
-/// This class is a generic parser that is able to handle any string
-/// parameter. By default it stores the value in an external global container
-/// (string_defaults). If used in smaller scopes (e.g. to parse parameters
-/// in subnet config), it can be pointed to a different storage, using the
-/// setStorage() method. This class follows the parser interface, laid out
-/// in its base class, @ref DhcpConfigParser.
-///
-/// For overview of usability of this generic purpose parser, see
-/// @ref dhcpv6ConfigInherit page.
-class StringParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor for StringParser
- ///
- /// @param param_name name of the configuration parameter being parsed
- StringParser(const std::string& param_name)
- : storage_(&string_defaults),
- param_name_(param_name) {
- // Empty parameter name is invalid.
- if (param_name_.empty()) {
- isc_throw(DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- }
-
- /// @brief parses parameter value
- ///
- /// Parses configuration parameter's value as string.
- ///
- /// @param value pointer to the content of parsed values
- /// @throws DhcpConfigError if the parsed parameter's name is empty.
- virtual void build(ConstElementPtr value) {
- if (param_name_.empty()) {
- isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
- << "empty parameter name provided");
- }
- value_ = value->str();
- boost::erase_all(value_, "\"");
- }
-
- /// @brief Stores the parsed value in a storage.
- virtual void commit() {
- if (storage_ != NULL && !param_name_.empty()) {
- // If a given parameter already exists in the storage we override
- // its value. If it doesn't we insert a new element.
- storage_->setParam(param_name_, value_);
- }
- }
-
- /// @brief Factory that constructs StringParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new StringParser(param_name));
- }
-
- /// @brief Sets storage for value of this parameter.
- ///
- /// See @ref dhcpv6ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container
- void setStorage(StringStorage* storage) {
- storage_ = storage;
- }
-
-private:
- /// Pointer to the storage, where parsed value will be stored
- StringStorage* storage_;
- /// Name of the parameter to be parsed
- std::string param_name_;
- /// The actual parsed value
- std::string value_;
-};
-
-/// @brief parser for interface list definition
-///
-/// This parser handles Dhcp6/interface entry.
-/// It contains a list of network interfaces that the server listens on.
-/// In particular, it can contain an entry called "all" or "any" that
-/// designates all interfaces.
-///
-/// It is useful for parsing Dhcp6/interface parameter.
-class InterfaceListConfigParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor
- ///
- /// As this is a dedicated parser, it must be used to parse
- /// "interface" parameter only. All other types will throw exception.
- ///
- /// @param param_name name of the configuration parameter being parsed
- /// @throw BadValue if supplied parameter name is not "interface"
- InterfaceListConfigParser(const std::string& param_name) {
- if (param_name != "interface") {
- isc_throw(isc::BadValue, "Internal error. Interface configuration "
- "parser called for the wrong parameter: " << param_name);
- }
- }
-
- /// @brief parses parameters value
- ///
- /// Parses configuration entry (list of parameters) and stores it in
- /// storage.
- ///
- /// @param value pointer to the content of parsed values
- virtual void build(ConstElementPtr value) {
- BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
- interfaces_.push_back(iface->str());
- }
- }
-
- /// @brief commits interfaces list configuration
- virtual void commit() {
- /// @todo: Implement per interface listening. Currently always listening
- /// on all interfaces.
- }
-
- /// @brief factory that constructs InterfaceListConfigParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new InterfaceListConfigParser(param_name));
- }
-
-private:
- /// contains list of network interfaces
- vector<string> interfaces_;
-};
-
-/// @brief parser for pool definition
-///
-/// This parser handles pool definitions, i.e. a list of entries of one
-/// of two syntaxes: min-max and prefix/len. Pool6 objects are created
-/// and stored in chosen PoolStorage container.
-///
-/// As there are no default values for pool, setStorage() must be called
-/// before build(). Otherwise an exception will be thrown.
-///
-/// It is useful for parsing Dhcp6/subnet6[X]/pool parameters.
-class PoolParser : public DhcpConfigParser {
-public:
-
- /// @brief constructor.
- PoolParser(const std::string& /*param_name*/)
- : pools_(NULL) {
- // ignore parameter name, it is always Dhcp6/subnet6[X]/pool
- }
-
- /// @brief parses the actual list
- ///
- /// This method parses the actual list of interfaces.
- /// No validation is done at this stage, everything is interpreted as
- /// interface name.
- /// @param pools_list list of pools defined for a subnet
- /// @throw isc::InvalidOperation if storage was not specified
- /// (setStorage() not called)
- void build(ConstElementPtr pools_list) {
-
- // setStorage() should have been called before build
- if (!pools_) {
- isc_throw(isc::InvalidOperation, "parser logic error: no pool storage set,"
- " but pool parser asked to parse pools");
- }
-
- BOOST_FOREACH(ConstElementPtr text_pool, pools_list->listValue()) {
-
- // That should be a single pool representation. It should contain
- // text in the form prefix/len or first - last. Note that spaces
- // are allowed
- string txt = text_pool->stringValue();
-
- // first let's remove any whitespaces
- boost::erase_all(txt, " "); // space
- boost::erase_all(txt, "\t"); // tabulation
-
- // Is this prefix/len notation?
- size_t pos = txt.find("/");
- if (pos != string::npos) {
- IOAddress addr("::");
- uint8_t len = 0;
- try {
- addr = IOAddress(txt.substr(0, pos));
-
- // start with the first character after /
- string prefix_len = txt.substr(pos + 1);
-
- // It is lexically cast to int and then downcast to uint8_t.
- // Direct cast to uint8_t (which is really an unsigned char)
- // will result in interpreting the first digit as output
- // value and throwing exception if length is written on two
- // digits (because there are extra characters left over).
-
- // No checks for values over 128. Range correctness will
- // be checked in Pool6 constructor.
- len = boost::lexical_cast<int>(prefix_len);
- } catch (...) {
- isc_throw(DhcpConfigError, "failed to parse pool "
- "definition: " << text_pool->stringValue());
- }
-
- Pool6Ptr pool(new Pool6(Pool6::TYPE_IA, addr, len));
- local_pools_.push_back(pool);
- continue;
- }
-
- // Is this min-max notation?
- pos = txt.find("-");
- if (pos != string::npos) {
- // using min-max notation
- IOAddress min(txt.substr(0, pos));
- IOAddress max(txt.substr(pos + 1));
-
- Pool6Ptr pool(new Pool6(Pool6::TYPE_IA, min, max));
-
- local_pools_.push_back(pool);
- continue;
- }
-
- isc_throw(DhcpConfigError, "failed to parse pool definition:"
- << text_pool->stringValue() <<
- ". Does not contain - (for min-max) nor / (prefix/len)");
- }
- }
-
- /// @brief sets storage for value of this parameter
- ///
- /// See @ref dhcpv6ConfigInherit for details.
- ///
- /// @param storage pointer to the storage container
- void setStorage(PoolStorage* storage) {
- pools_ = storage;
- }
-
- /// @brief Stores the parsed values in a storage provided
- /// by an upper level parser.
- virtual void commit() {
- if (pools_) {
- // local_pools_ holds the values produced by the build function.
- // At this point parsing should have completed successfuly so
- // we can append new data to the supplied storage.
- pools_->insert(pools_->end(), local_pools_.begin(),
- local_pools_.end());
- }
- }
-
- /// @brief factory that constructs PoolParser objects
- ///
- /// @param param_name name of the parameter to be parsed
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new PoolParser(param_name));
- }
-
-private:
- /// @brief pointer to the actual Pools storage
- ///
- /// This is typically a storage somewhere in Subnet parser
- /// (an upper level parser).
- PoolStorage* pools_;
- /// A temporary storage for pools configuration. It is a
- /// storage where pools are stored by build function.
- PoolStorage local_pools_;
-};
-
-
-/// @brief Parser for option data value.
+/// @brief Parser for DHCP6 option data value.
///
/// This parser parses configuration entries that specify value of
-/// a single option. These entries include option name, option code
-/// and data carried by the option. The option data can be specified
-/// in one of the two available formats: binary value represented as
-/// a string of hexadecimal digits or a list of comma separated values.
-/// The format being used is controlled by csv-format configuration
-/// parameter. When setting this value to True, the latter format is
-/// used. The subsequent values in the CSV format apply to relevant
-/// option data fields in the configured option. For example the
-/// configuration: "data" : "192.168.2.0, 56, hello world" can be
-/// used to set values for the option comprising IPv4 address,
-/// integer and string data field. Note that order matters. If the
-/// order of values does not match the order of data fields within
-/// an option the configuration will not be accepted. If parsing
-/// is successful then an instance of an option is created and
-/// added to the storage provided by the calling class.
-class OptionDataParser : public DhcpConfigParser {
+/// a single option specific to DHCP6. It provides the DHCP6-specific
+/// implementation of the abstract class OptionDataParser.
+class Dhcp6OptionDataParser : public OptionDataParser {
public:
-
/// @brief Constructor.
///
- /// Class constructor.
- OptionDataParser(const std::string&)
- : options_(NULL),
- // initialize option to NULL ptr
- option_descriptor_(false) { }
-
- /// @brief Parses the single option data.
- ///
- /// This method parses the data of a single option from the configuration.
- /// The option data includes option name, option code and data being
- /// carried by this option. Eventually it creates the instance of the
- /// option.
- ///
- /// @warning setStorage must be called with valid storage pointer prior
- /// to calling this method.
- ///
- /// @param option_data_entries collection of entries that define value
- /// for a particular option.
- /// @throw DhcpConfigError if invalid parameter specified in
- /// the configuration.
- /// @throw isc::InvalidOperation if failed to set storage prior to
- /// calling build.
- virtual void build(ConstElementPtr option_data_entries) {
-
- if (options_ == NULL) {
- isc_throw(isc::InvalidOperation, "Parser logic error: storage must be set before "
- "parsing option data.");
- }
- BOOST_FOREACH(ConfigPair param, option_data_entries->mapValue()) {
- ParserPtr parser;
- if (param.first == "name" || param.first == "data" ||
- param.first == "space") {
- boost::shared_ptr<StringParser>
- name_parser(dynamic_cast<StringParser*>(StringParser::factory(param.first)));
- if (name_parser) {
- name_parser->setStorage(&string_values_);
- parser = name_parser;
- }
- } else if (param.first == "code") {
- boost::shared_ptr<Uint32Parser>
- code_parser(dynamic_cast<Uint32Parser*>(Uint32Parser::factory(param.first)));
- if (code_parser) {
- code_parser->setStorage(&uint32_values_);
- parser = code_parser;
- }
- } else if (param.first == "csv-format") {
- boost::shared_ptr<BooleanParser>
- value_parser(dynamic_cast<BooleanParser*>(BooleanParser::factory(param.first)));
- if (value_parser) {
- value_parser->setStorage(&boolean_values_);
- parser = value_parser;
- }
- } else {
- isc_throw(DhcpConfigError,
- "parser error: option-data parameter not supported: "
- << param.first);
- }
- parser->build(param.second);
- // Before we can create an option we need to get the data from
- // the child parsers. The only way to do it is to invoke commit
- // on them so as they store the values in appropriate storages
- // that this class provided to them. Note that this will not
- // modify values stored in the global storages so the configuration
- // will remain consistent even parsing fails somewhere further on.
- parser->commit();
- }
- // Try to create the option instance.
- createOption();
- }
-
- /// @brief Commits option value.
- ///
- /// This function adds a new option to the storage or replaces an existing option
- /// with the same code.
- ///
- /// @throw isc::InvalidOperation if failed to set pointer to storage or failed
- /// to call build() prior to commit. If that happens data in the storage
- /// remain un-modified.
- virtual void commit() {
- if (options_ == NULL) {
- isc_throw(isc::InvalidOperation, "parser logic error: storage must be set before "
- "committing option data.");
- } else if (!option_descriptor_.option) {
- // Before we can commit the new option should be configured. If it is not
- // than somebody must have called commit() before build().
- isc_throw(isc::InvalidOperation, "parser logic error: no option has been configured and"
- " thus there is nothing to commit. Has build() been called?");
- }
- uint16_t opt_type = option_descriptor_.option->getType();
- Subnet::OptionContainerPtr options = options_->getItems(option_space_);
- // The getItems() should never return NULL pointer. If there are no
- // options configured for the particular option space a pointer
- // to an empty container should be returned.
- assert(options);
- Subnet::OptionContainerTypeIndex& idx = options->get<1>();
- // Try to find options with the particular option code in the main
- // storage. If found, remove these options because they will be
- // replaced with new one.
- Subnet::OptionContainerTypeRange range =
- idx.equal_range(opt_type);
- if (std::distance(range.first, range.second) > 0) {
- idx.erase(range.first, range.second);
- }
- // Append new option to the main storage.
- options_->addItem(option_descriptor_, option_space_);
- }
-
- /// @brief Set storage for the parser.
- ///
- /// Sets storage for the parser. This storage points to the
- /// vector of options and is used by multiple instances of
- /// OptionDataParser. Each instance creates exactly one object
- /// of dhcp::Option or derived type and appends it to this
- /// storage.
- ///
- /// @param storage pointer to the options storage
- void setStorage(OptionStorage* storage) {
- options_ = storage;
+ /// @param dummy first param, option names are always "Dhcp6/option-data[n]"
+ /// @param options is the option storage in which to store the parsed option
+ /// upon "commit".
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ Dhcp6OptionDataParser(const std::string&, OptionStoragePtr options,
+ ParserContextPtr global_context)
+ :OptionDataParser("", options, global_context) {
}
-private:
-
- /// @brief Create option instance.
- ///
- /// Creates an instance of an option and adds it to the provided
- /// options storage. If the option data parsed by \ref build function
- /// are invalid or insufficient this function emits an exception.
- ///
- /// @warning this function does not check if options_ storage pointer
- /// is initialized but this check is not needed here because it is done
- /// in the \ref build function.
+ /// @brief static factory method for instantiating Dhcp4OptionDataParsers
///
- /// @throw DhcpConfigError if parameters provided in the configuration
- /// are invalid.
- void createOption() {
-
- // Option code is held in the uint32_t storage but is supposed to
- // be uint16_t value. We need to check that value in the configuration
- // does not exceed range of uint16_t and is not zero.
- uint32_t option_code = uint32_values_.getParam("code");
- if (option_code == 0) {
- isc_throw(DhcpConfigError, "option code must not be zero."
- << " Option code '0' is reserved in DHCPv6.");
- } else if (option_code > std::numeric_limits<uint16_t>::max()) {
- isc_throw(DhcpConfigError, "invalid option code '" << option_code
- << "', it must not exceed '"
- << std::numeric_limits<uint16_t>::max() << "'");
- }
- // Check that the option name has been specified, is non-empty and does not
- // contain spaces.
- std::string option_name = string_values_.getParam("name");
- if (option_name.empty()) {
- isc_throw(DhcpConfigError, "name of the option with code '"
- << option_code << "' is empty");
- } else if (option_name.find(" ") != std::string::npos) {
- isc_throw(DhcpConfigError, "invalid option name '" << option_name
- << "', space character is not allowed");
- }
-
- std::string option_space = string_values_.getParam("space");
- if (!OptionSpace::validateName(option_space)) {
- isc_throw(DhcpConfigError, "invalid option space name '"
- << option_space << "' specified for option '"
- << option_name << "' (code '" << option_code
- << "')");
- }
-
+ /// @param param_name name of the parameter to be parsed.
+ /// @param options storage where the parameter value is to be stored.
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ /// @return returns a pointer to a new OptionDataParser. Caller is
+ /// is responsible for deleting it when it is no longer needed.
+ static OptionDataParser* factory(const std::string& param_name,
+ OptionStoragePtr options, ParserContextPtr global_context) {
+ return (new Dhcp6OptionDataParser(param_name, options, global_context));
+ }
+
+
+protected:
+ /// @brief Finds an option definition within the server's option space
+ ///
+ /// Given an option space and an option code, find the correpsonding
+ /// option defintion within the server's option defintion storage.
+ ///
+ /// @param option_space name of the parameter option space
+ /// @param option_code numeric value of the parameter to find
+ /// @return OptionDefintionPtr of the option defintion or an
+ /// empty OptionDefinitionPtr if not found.
+ /// @throw DhcpConfigError if the option space requested is not valid
+ /// for this server.
+ virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
+ std::string& option_space, uint32_t option_code) {
OptionDefinitionPtr def;
if (option_space == "dhcp6" &&
LibDHCP::isStandardOption(Option::V6, option_code)) {
def = LibDHCP::getOptionDef(Option::V6, option_code);
-
} else if (option_space == "dhcp4") {
isc_throw(DhcpConfigError, "'dhcp4' option space name is reserved"
- << " for DHCPv4 server");
+ << " for DHCPv4 server");
} else {
- // If we are not dealing with a standard option then we
- // need to search for its definition among user-configured
- // options. They are expected to be in the global storage
- // already.
- OptionDefContainerPtr defs = option_def_intermediate.getItems(option_space);
- // The getItems() should never return the NULL pointer. If there are
- // no option definitions for the particular option space a pointer
- // to an empty container should be returned.
- assert(defs);
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- OptionDefContainerTypeRange range = idx.equal_range(option_code);
- if (std::distance(range.first, range.second) > 0) {
- def = *range.first;
+ // Check if this is a vendor-option. If it is, get vendor-specific
+ // definition.
+ uint32_t vendor_id = SubnetConfigParser::optionSpaceToVendorId(option_space);
+ if (vendor_id) {
+ def = LibDHCP::getVendorOptionDef(Option::V6, vendor_id, option_code);
}
- if (!def) {
- isc_throw(DhcpConfigError, "definition for the option '"
- << option_space << "." << option_name
- << "' having code '" << option_code
- << "' does not exist");
- }
-
}
- // Get option data from the configuration database ('data' field).
- const std::string option_data = string_values_.getParam("data");
- const bool csv_format = boolean_values_.getParam("csv-format");
-
- // Transform string of hexadecimal digits into binary format.
- std::vector<uint8_t> binary;
- std::vector<std::string> data_tokens;
-
- if (csv_format) {
- // If the option data is specified as a string of comma
- // separated values then we need to split this string into
- // individual values - each value will be used to initialize
- // one data field of an option.
- data_tokens = isc::util::str::tokens(option_data, ",");
- } else {
- // Otherwise, the option data is specified as a string of
- // hexadecimal digits that we have to turn into binary format.
- try {
- util::encode::decodeHex(option_data, binary);
- } catch (...) {
- isc_throw(DhcpConfigError, "Parser error: option data is not a valid"
- << " string of hexadecimal digits: " << option_data);
- }
- }
-
- OptionPtr option;
- if (!def) {
- if (csv_format) {
- isc_throw(DhcpConfigError, "the CSV option data format can be"
- " used to specify values for an option that has a"
- " definition. The option with code " << option_code
- << " does not have a definition.");
- }
-
- // @todo We have a limited set of option definitions initialized at the moment.
- // In the future we want to initialize option definitions for all options.
- // Consequently an error will be issued if an option definition does not exist
- // for a particular option code. For now it is ok to create generic option
- // if definition does not exist.
- OptionPtr option(new Option(Option::V6, static_cast<uint16_t>(option_code),
- binary));
- // The created option is stored in option_descriptor_ class member until the
- // commit stage when it is inserted into the main storage. If an option with the
- // same code exists in main storage already the old option is replaced.
- option_descriptor_.option = option;
- option_descriptor_.persistent = false;
- } else {
-
- // Option name should match the definition. The option name
- // may seem to be redundant but in the future we may want
- // to reference options and definitions using their names
- // and/or option codes so keeping the option name in the
- // definition of option value makes sense.
- if (def->getName() != option_name) {
- isc_throw(DhcpConfigError, "specified option name '"
- << option_name << "' does not match the "
- << "option definition: '" << option_space
- << "." << def->getName() << "'");
- }
-
- // Option definition has been found so let's use it to create
- // an instance of our option.
- try {
- OptionPtr option = csv_format ?
- def->optionFactory(Option::V6, option_code, data_tokens) :
- def->optionFactory(Option::V6, option_code, binary);
- Subnet::OptionDescriptor desc(option, false);
- option_descriptor_.option = option;
- option_descriptor_.persistent = false;
- } catch (const isc::Exception& ex) {
- isc_throw(DhcpConfigError, "option data does not match"
- << " option definition (space: " << option_space
- << ", code: " << option_code << "): "
- << ex.what());
- }
- }
- // All went good, so we can set the option space name.
- option_space_ = option_space;
+ return (def);
}
-
- /// Storage for uint32 values (e.g. option code).
- Uint32Storage uint32_values_;
- /// Storage for string values (e.g. option name or data).
- StringStorage string_values_;
- /// Storage for boolean values.
- BooleanStorage boolean_values_;
- /// Pointer to options storage. This storage is provided by
- /// the calling class and is shared by all OptionDataParser objects.
- OptionStorage* options_;
- /// Option descriptor holds newly configured option.
- isc::dhcp::Subnet::OptionDescriptor option_descriptor_;
- /// Option space name where the option belongs to.
- std::string option_space_;
};
-/// @brief Parser for option data values within a subnet.
+/// @brief Parser for IPv4 pool definitions.
+///
+/// This is the IPv6 derivation of the PoolParser class and handles pool
+/// definitions, i.e. a list of entries of one of two syntaxes: min-max and
+/// prefix/len for IPv6 pools. Pool6 objects are created and stored in chosen
+/// PoolStorage container.
///
-/// This parser iterates over all entries that define options
-/// data for a particular subnet and creates a collection of options.
-/// If parsing is successful, all these options are added to the Subnet
-/// object.
-class OptionDataListParser : public DhcpConfigParser {
+/// It is useful for parsing Dhcp6/subnet6[X]/pool parameters.
+class Pool6Parser : public PoolParser {
public:
/// @brief Constructor.
///
- /// Unless otherwise specified, parsed options will be stored in
- /// a global option container (option_default). That storage location
- /// is overriden on a subnet basis.
- OptionDataListParser(const std::string&)
- : options_(&option_defaults), local_options_() { }
-
- /// @brief Parses entries that define options' data for a subnet.
- ///
- /// This method iterates over all entries that define option data
- /// for options within a single subnet and creates options' instances.
- ///
- /// @param option_data_list pointer to a list of options' data sets.
- /// @throw DhcpConfigError if option parsing failed.
- void build(ConstElementPtr option_data_list) {
- BOOST_FOREACH(ConstElementPtr option_value, option_data_list->listValue()) {
- boost::shared_ptr<OptionDataParser> parser(new OptionDataParser("option-data"));
- // options_ member will hold instances of all options thus
- // each OptionDataParser takes it as a storage.
- parser->setStorage(&local_options_);
- // Build the instance of a single option.
- parser->build(option_value);
- // Store a parser as it will be used to commit.
- parsers_.push_back(parser);
- }
- }
-
- /// @brief Set storage for option instances.
- ///
- /// @param storage pointer to options storage.
- void setStorage(OptionStorage* storage) {
- options_ = storage;
- }
-
-
- /// @brief Commit all option values.
- ///
- /// This function invokes commit for all option values.
- void commit() {
- BOOST_FOREACH(ParserPtr parser, parsers_) {
- parser->commit();
- }
- // Parsing was successful and we have all configured
- // options in local storage. We can now replace old values
- // with new values.
- std::swap(local_options_, *options_);
- }
-
- /// @brief Create DhcpDataListParser object
- ///
- /// @param param_name param name.
- ///
- /// @return DhcpConfigParser object.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new OptionDataListParser(param_name));
+ /// @param param_name name of the parameter. Note, it is passed through
+ /// but unused, parameter is currently always "Dhcp6/subnet6[X]/pool"
+ /// @param pools storage container in which to store the parsed pool
+ /// upon "commit"
+ Pool6Parser(const std::string& param_name, PoolStoragePtr pools)
+ :PoolParser(param_name, pools) {
+ }
+
+protected:
+ /// @brief Creates a Pool6 object given a IPv6 prefix and the prefix length.
+ ///
+ /// @param addr is the IPv6 prefix of the pool.
+ /// @param len is the prefix length.
+ /// @param ptype is the type of IPv6 pool (Pool::PoolType). Note this is
+ /// passed in as an int32_t and cast to PoolType to accommodate a
+ /// polymorphic interface.
+ /// @return returns a PoolPtr to the new Pool4 object.
+ PoolPtr poolMaker (IOAddress &addr, uint32_t len, int32_t ptype)
+ {
+ return (PoolPtr(new Pool6(static_cast<isc::dhcp::Lease::Type>
+ (ptype), addr, len)));
+ }
+
+ /// @brief Creates a Pool6 object given starting and ending IPv6 addresses.
+ ///
+ /// @param min is the first IPv6 address in the pool.
+ /// @param max is the last IPv6 address in the pool.
+ /// @param ptype is the type of IPv6 pool (Pool::PoolType). Note this is
+ /// passed in as an int32_t and cast to PoolType to accommodate a
+ /// polymorphic interface.
+ /// @return returns a PoolPtr to the new Pool4 object.
+ PoolPtr poolMaker (IOAddress &min, IOAddress &max, int32_t ptype)
+ {
+ return (PoolPtr(new Pool6(static_cast<isc::dhcp::Lease::Type>
+ (ptype), min, max)));
}
-
- /// Pointer to options instances storage.
- OptionStorage* options_;
- /// Intermediate option storage. This storage is used by
- /// lower level parsers to add new options. Values held
- /// in this storage are assigned to main storage (options_)
- /// if overall parsing was successful.
- OptionStorage local_options_;
- /// Collection of parsers;
- ParserCollection parsers_;
};
-/// @brief Parser for a single option definition.
+/// @brief Parser for IPv6 prefix delegation definitions.
+///
+/// This class handles prefix delegation pool definitions for IPv6 subnets
+/// Pool6 objects are created and stored in the given PoolStorage container.
+///
+/// PdPool defintions currently support three elements: prefix, prefix-len,
+/// and delegated-len, as shown in the example JSON text below:
+///
+/// @code
+///
+/// {
+/// "prefix": "2001:db8:1::",
+/// "prefix-len": 64,
+/// "delegated-len": 128
+/// }
+/// @endcode
///
-/// This parser creates an instance of a single option definition.
-class OptionDefParser: DhcpConfigParser {
+class PdPoolParser : public DhcpConfigParser {
public:
/// @brief Constructor.
///
- /// This constructor sets the pointer to the option definitions
- /// storage to NULL. It must be set to point to the actual storage
- /// before \ref build is called.
- OptionDefParser(const std::string&)
- : storage_(NULL) {
+ /// @param param_name name of the parameter. Note, it is passed through
+ /// but unused, parameter is currently always "Dhcp6/subnet6[X]/pool"
+ /// @param pools storage container in which to store the parsed pool
+ /// upon "commit"
+ PdPoolParser(const std::string&, PoolStoragePtr pools)
+ : uint32_values_(new Uint32Storage()),
+ string_values_(new StringStorage()), pools_(pools) {
+ if (!pools_) {
+ isc_throw(isc::dhcp::DhcpConfigError,
+ "PdPoolParser context storage may not be NULL");
+ }
}
- /// @brief Parses an entry that describes single option definition.
+ /// @brief Builds a prefix delegation pool from the given configuration
///
- /// @param option_def a configuration entry to be parsed.
+ /// This function parses configuration entries and creates an instance
+ /// of a dhcp::Pool6 configured for prefix delegation.
///
- /// @throw DhcpConfigError if parsing was unsuccessful.
- void build(ConstElementPtr option_def) {
- if (storage_ == NULL) {
- isc_throw(DhcpConfigError, "parser logic error: storage must be set"
- " before parsing option definition data");
- }
+ /// @param pd_pool_ pointer to an element that holds configuration entries
+ /// that define a prefix delegation pool.
+ ///
+ /// @throw DhcpConfigError if configuration parsing fails.
+ virtual void build(ConstElementPtr pd_pool_) {
// Parse the elements that make up the option definition.
- BOOST_FOREACH(ConfigPair param, option_def->mapValue()) {
+ BOOST_FOREACH(ConfigPair param, pd_pool_->mapValue()) {
std::string entry(param.first);
ParserPtr parser;
- if (entry == "name" || entry == "type" || entry == "record-types" ||
- entry == "space" || entry == "encapsulate") {
- StringParserPtr
- str_parser(dynamic_cast<StringParser*>(StringParser::factory(entry)));
- if (str_parser) {
- str_parser->setStorage(&string_values_);
- parser = str_parser;
- }
- } else if (entry == "code") {
- Uint32ParserPtr
- code_parser(dynamic_cast<Uint32Parser*>(Uint32Parser::factory(entry)));
- if (code_parser) {
- code_parser->setStorage(&uint32_values_);
- parser = code_parser;
- }
- } else if (entry == "array") {
- BooleanParserPtr
- array_parser(dynamic_cast<BooleanParser*>(BooleanParser::factory(entry)));
- if (array_parser) {
- array_parser->setStorage(&boolean_values_);
- parser = array_parser;
- }
+ if (entry == "prefix") {
+ StringParserPtr str_parser(new StringParser(entry,
+ string_values_));
+ parser = str_parser;
+ } else if (entry == "prefix-len" || entry == "delegated-len") {
+ Uint32ParserPtr code_parser(new Uint32Parser(entry,
+ uint32_values_));
+ parser = code_parser;
} else {
isc_throw(DhcpConfigError, "invalid parameter: " << entry);
}
@@ -1057,415 +238,230 @@ public:
parser->commit();
}
- // Create an instance of option definition.
- createOptionDef();
-
- // Get all items we collected so far for the particular option space.
- OptionDefContainerPtr defs = storage_->getItems(option_space_name_);
- // Check if there are any items with option code the same as the
- // one specified for the definition we are now creating.
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- const OptionDefContainerTypeRange& range =
- idx.equal_range(option_definition_->getCode());
- // If there are any items with this option code already we need
- // to issue an error because we don't allow duplicates for
- // option definitions within an option space.
- if (std::distance(range.first, range.second) > 0) {
- isc_throw(DhcpConfigError, "duplicated option definition for"
- << " code '" << option_definition_->getCode() << "'");
- }
- }
+ try {
+ // We should now have all of the pool elements we need to create
+ // the pool. Fetch them and pass them into the Pool6 constructor.
+ // The constructor is expected to enforce any value validation.
+ const std::string addr_str = string_values_->getParam("prefix");
+ IOAddress addr(addr_str);
- /// @brief Stores the parsed option definition in the data store.
- void commit() {
- if (storage_ && option_definition_ &&
- OptionSpace::validateName(option_space_name_)) {
- storage_->addItem(option_definition_, option_space_name_);
- }
- }
+ uint32_t prefix_len = uint32_values_->getParam("prefix-len");
- /// @brief Sets a pointer to the data store.
- ///
- /// The newly created instance of an option definition will be
- /// added to the data store given by the argument.
- ///
- /// @param storage pointer to the data store where the option definition
- /// will be added to.
- void setStorage(OptionDefStorage* storage) {
- storage_ = storage;
- }
-
-private:
+ uint32_t delegated_len = uint32_values_->getParam("delegated-len");
- /// @brief Create option definition from the parsed parameters.
- void createOptionDef() {
- // Get the option space name and validate it.
- std::string space = string_values_.getParam("space");
- if (!OptionSpace::validateName(space)) {
- isc_throw(DhcpConfigError, "invalid option space name '"
- << space << "'");
+ // Attempt to construct the local pool.
+ pool_.reset(new Pool6(Lease::TYPE_PD, addr, prefix_len,
+ delegated_len));
+ } catch (const std::exception& ex) {
+ isc_throw(isc::dhcp::DhcpConfigError,
+ "PdPoolParser failed to build pool: " << ex.what());
}
+ }
- // Get other parameters that are needed to create the
- // option definition.
- std::string name = string_values_.getParam("name");
- uint32_t code = uint32_values_.getParam("code");
- std::string type = string_values_.getParam("type");
- bool array_type = boolean_values_.getParam("array");
- std::string encapsulates = string_values_.getParam("encapsulate");
-
- // Create option definition.
- OptionDefinitionPtr def;
- // We need to check if user has set encapsulated option space
- // name. If so, different constructor will be used.
- if (!encapsulates.empty()) {
- // Arrays can't be used together with sub-options.
- if (array_type) {
- isc_throw(DhcpConfigError, "option '" << space << "."
- << "name" << "', comprising an array of data"
- << " fields may not encapsulate any option space");
-
- } else if (encapsulates == space) {
- isc_throw(DhcpConfigError, "option must not encapsulate"
- << " an option space it belongs to: '"
- << space << "." << name << "' is set to"
- << " encapsulate '" << space << "'");
-
- } else {
- def.reset(new OptionDefinition(name, code, type,
- encapsulates.c_str()));
- }
+ // @brief Commits the constructed local pool to the pool storage.
+ virtual void commit() {
+ // Add the local pool to the external storage ptr.
+ pools_->push_back(pool_);
+ }
- } else {
- def.reset(new OptionDefinition(name, code, type, array_type));
+protected:
+ /// Storage for subnet-specific integer values.
+ Uint32StoragePtr uint32_values_;
- }
+ /// Storage for subnet-specific string values.
+ StringStoragePtr string_values_;
- // The record-types field may carry a list of comma separated names
- // of data types that form a record.
- std::string record_types = string_values_.getParam("record-types");
- // Split the list of record types into tokens.
- std::vector<std::string> record_tokens =
- isc::util::str::tokens(record_types, ",");
- // Iterate over each token and add a record type into
- // option definition.
- BOOST_FOREACH(std::string record_type, record_tokens) {
- try {
- boost::trim(record_type);
- if (!record_type.empty()) {
- def->addRecordField(record_type);
- }
- } catch (const Exception& ex) {
- isc_throw(DhcpConfigError, "invalid record type values"
- << " specified for the option definition: "
- << ex.what());
- }
- }
+ /// Parsers are stored here.
+ ParserCollection parsers_;
- // Check the option definition parameters are valid.
- try {
- def->validate();
- } catch (const isc::Exception& ex) {
- isc_throw(DhcpConfigError, "invalid option definition"
- << " parameters: " << ex.what());
- }
- // Option definition has been created successfully.
- option_space_name_ = space;
- option_definition_ = def;
- }
+ /// Pointer to the created pool object.
+ isc::dhcp::Pool6Ptr pool_;
- /// Instance of option definition being created by this parser.
- OptionDefinitionPtr option_definition_;
- /// Name of the space the option definition belongs to.
- std::string option_space_name_;
-
- /// Pointer to a storage where the option definition will be
- /// added when \ref commit is called.
- OptionDefStorage* storage_;
-
- /// Storage for boolean values.
- BooleanStorage boolean_values_;
- /// Storage for string values.
- StringStorage string_values_;
- /// Storage for uint32 values.
- Uint32Storage uint32_values_;
+ /// Pointer to storage to which the local pool is written upon commit.
+ isc::dhcp::PoolStoragePtr pools_;
};
-/// @brief Parser for a list of option definitions.
+/// @brief Parser for a list of prefix delegation pools.
///
-/// This parser iterates over all configuration entries that define
-/// option definitions and creates instances of these definitions.
-/// If the parsing is successful, the collection of created definitions
-/// is put into the provided storage.
-class OptionDefListParser : DhcpConfigParser {
+/// This parser iterates over a list of prefix delegation pool entries and
+/// creates pool instances for each one. If the parsing is successful, the
+/// collection of pools is committed to the provided storage.
+class PdPoolListParser : public DhcpConfigParser {
public:
-
/// @brief Constructor.
///
- /// This constructor initializes the pointer to option definitions
- /// storage to NULL value. This pointer has to be set to point to
- /// the actual storage before the \ref build function is called.
- OptionDefListParser(const std::string&) {
+ /// @param dummy first argument is ignored, all Parser constructors
+ /// accept string as first argument.
+ /// @param storage is the pool storage in which to store the parsed
+ /// pools in this list
+ /// @throw isc::dhcp::DhcpConfigError if storage is null.
+ PdPoolListParser(const std::string&, PoolStoragePtr pools)
+ : local_pools_(new PoolStorage()), pools_(pools) {
+ if (!pools_) {
+ isc_throw(isc::dhcp::DhcpConfigError,
+ "PdPoolListParser pools storage may not be NULL");
+ }
}
/// @brief Parse configuration entries.
///
/// This function parses configuration entries and creates instances
- /// of option definitions.
+ /// of prefix delegation pools .
+ ///
+ /// @param pd_pool_list pointer to an element that holds entries
+ /// that define a prefix delegation pool.
///
- /// @param option_def_list pointer to an element that holds entries
- /// that define option definitions.
/// @throw DhcpConfigError if configuration parsing fails.
- void build(ConstElementPtr option_def_list) {
- // Clear existing items in the global storage.
- // We are going to replace all of them.
- option_def_intermediate.clearItems();
-
- if (!option_def_list) {
- isc_throw(DhcpConfigError, "parser error: a pointer to a list of"
- << " option definitions is NULL");
- }
+ void build(isc::data::ConstElementPtr pd_pool_list) {
+ // Make sure the local list is empty.
+ local_pools_.reset(new PoolStorage());
- BOOST_FOREACH(ConstElementPtr option_def, option_def_list->listValue()) {
- boost::shared_ptr<OptionDefParser>
- parser(new OptionDefParser("single-option-def"));
- parser->setStorage(&option_def_intermediate);
- parser->build(option_def);
- parser->commit();
+ // Make sure we have a configuration elements to parse.
+ if (!pd_pool_list) {
+ isc_throw(DhcpConfigError,
+ "PdPoolListParser: list of pool definitions is empty");
}
- }
-
- /// @brief Stores option definitions in the CfgMgr.
- void commit() {
- CfgMgr& cfg_mgr = CfgMgr::instance();
-
- cfg_mgr.deleteOptionDefs();
-
- // We need to move option definitions from the temporary
- // storage to the global storage.
- std::list<std::string> space_names =
- option_def_intermediate.getOptionSpaceNames();
- BOOST_FOREACH(std::string space_name, space_names) {
-
- BOOST_FOREACH(OptionDefinitionPtr def,
- *option_def_intermediate.getItems(space_name)) {
- // All option definitions should be initialized to non-NULL
- // values. The validation is expected to be made by the
- // OptionDefParser when creating an option definition.
- assert(def);
- cfg_mgr.addOptionDef(def, space_name);
- }
+ // Loop through the list of pd pools.
+ BOOST_FOREACH(ConstElementPtr pd_pool, pd_pool_list->listValue()) {
+ boost::shared_ptr<PdPoolParser>
+ // Create the PdPool parser.
+ parser(new PdPoolParser("pd-pool", local_pools_));
+ // Build the pool instance
+ parser->build(pd_pool);
+ // Commit the pool to the local list of pools.
+ parser->commit();
}
}
- /// @brief Create an OptionDefListParser object.
- ///
- /// @param param_name configuration entry holding option definitions.
+ /// @brief Commits the pools created to the external storage area.
///
- /// @return OptionDefListParser object.
- static DhcpConfigParser* factory(const std::string& param_name) {
- return (new OptionDefListParser(param_name));
+ /// Note that this method adds the local list of pools to the storage area
+ /// rather than replacing its contents. This permits other parsers to
+ /// contribute to the set of pools.
+ void commit() {
+ // local_pools_ holds the values produced by the build function.
+ // At this point parsing should have completed successfully so
+ // we can append new data to the supplied storage.
+ pools_->insert(pools_->end(), local_pools_->begin(),
+ local_pools_->end());
}
+private:
+ /// @brief storage for local pools
+ PoolStoragePtr local_pools_;
+
+ /// @brief External storage where pools are stored upon list commit.
+ PoolStoragePtr pools_;
};
-/// @brief this class parses a single subnet
+/// @brief This class parses a single IPv6 subnet.
///
-/// This class parses the whole subnet definition. It creates parsers
-/// for received configuration parameters as needed.
-class Subnet6ConfigParser : public DhcpConfigParser {
+/// This is the IPv6 derivation of the SubnetConfigParser class and it parses
+/// the whole subnet definition. It creates parsersfor received configuration
+/// parameters as needed.
+class Subnet6ConfigParser : public SubnetConfigParser {
public:
- /// @brief constructor
- Subnet6ConfigParser(const std::string& ) {
- // The parameter should always be "subnet", but we don't check
- // against that here in case some wants to reuse this parser somewhere.
- }
-
- /// @brief parses parameter value
- ///
- /// @param subnet pointer to the content of subnet definition
+ /// @brief Constructor
///
- /// @throw isc::DhcpConfigError if subnet configuration parsing failed.
- void build(ConstElementPtr subnet) {
-
- BOOST_FOREACH(ConfigPair param, subnet->mapValue()) {
- ParserPtr parser(createSubnet6ConfigParser(param.first));
- // The actual type of the parser is unknown here. We have to discover
- // the parser type here to invoke the corresponding setStorage function
- // on it. We discover parser type by trying to cast the parser to various
- // parser types and checking which one was successful. For this one
- // a setStorage and build methods are invoked.
-
- // Try uint32 type parser.
- if (!buildParser<Uint32Parser, Uint32Storage >(parser, uint32_values_,
- param.second) &&
- // Try string type parser.
- !buildParser<StringParser, StringStorage >(parser, string_values_,
- param.second) &&
- // Try pool parser.
- !buildParser<PoolParser, PoolStorage >(parser, pools_,
- param.second) &&
- // Try option data parser.
- !buildParser<OptionDataListParser, OptionStorage >(parser, options_,
- param.second)) {
- // Appropriate parsers are created in the createSubnet6ConfigParser
- // and they should be limited to those that we check here for. Thus,
- // if we fail to find a matching parser here it is a programming error.
- isc_throw(DhcpConfigError, "failed to find suitable parser");
- }
- }
-
- // In order to create new subnet we need to get the data out
- // of the child parsers first. The only way to do it is to
- // invoke commit on them because it will make them write
- // parsed data into storages we have supplied.
- // Note that triggering commits on child parsers does not
- // affect global data because we supplied pointers to storages
- // local to this object. Thus, even if this method fails
- // later on, the configuration remains consistent.
- BOOST_FOREACH(ParserPtr parser, parsers_) {
- parser->commit();
- }
-
- // Create a subnet.
- createSubnet();
+ /// @param ignored first parameter
+ /// stores global scope parameters, options, option defintions.
+ Subnet6ConfigParser(const std::string&)
+ :SubnetConfigParser("", globalContext()) {
}
/// @brief Adds the created subnet to a server's configuration.
+ /// @throw throws Unexpected if dynamic cast fails.
void commit() {
if (subnet_) {
- isc::dhcp::CfgMgr::instance().addSubnet6(subnet_);
+ Subnet6Ptr sub6ptr = boost::dynamic_pointer_cast<Subnet6>(subnet_);
+ if (!sub6ptr) {
+ // If we hit this, it is a programming error.
+ isc_throw(Unexpected,
+ "Invalid cast in Subnet4ConfigParser::commit");
+ }
+ isc::dhcp::CfgMgr::instance().addSubnet6(sub6ptr);
}
}
-private:
+protected:
- /// @brief Set storage for a parser and invoke build.
+ /// @brief creates parsers for entries in subnet definition
///
- /// This helper method casts the provided parser pointer to the specified
- /// type. If the cast is successful it sets the corresponding storage for
- /// this parser, invokes build on it and saves the parser.
+ /// @param config_id name of the entry
///
- /// @tparam T parser type to which parser argument should be cast.
- /// @tparam Y storage type for the specified parser type.
- /// @param parser parser on which build must be invoked.
- /// @param storage reference to a storage that will be set for a parser.
- /// @param subnet subnet element read from the configuration and being parsed.
- /// @return true if parser pointer was successfully cast to specialized
- /// parser type provided as Y.
- template<typename T, typename Y>
- bool buildParser(const ParserPtr& parser, Y& storage, const ConstElementPtr& subnet) {
- // We need to cast to T in order to set storage for the parser.
- boost::shared_ptr<T> cast_parser = boost::dynamic_pointer_cast<T>(parser);
- // It is common that this cast is not successful because we try to cast to all
- // supported parser types as we don't know the type of a parser in advance.
- if (cast_parser) {
- // Cast, successful so we go ahead with setting storage and actual parse.
- cast_parser->setStorage(&storage);
- parser->build(subnet);
- parsers_.push_back(parser);
- // We indicate that cast was successful so as the calling function
- // may skip attempts to cast to other parser types and proceed to
- // next element.
- return (true);
- }
- // It was not successful. Indicate that another parser type
- // should be tried.
- return (false);
- }
-
- /// @brief Append sub-options to an option.
- ///
- /// @param option_space a name of the encapsulated option space.
- /// @param option option instance to append sub-options to.
- void appendSubOptions(const std::string& option_space, OptionPtr& option) {
- // Only non-NULL options are stored in option container.
- // If this option pointer is NULL this is a serious error.
- assert(option);
-
- OptionDefinitionPtr def;
- if (option_space == "dhcp6" &&
- LibDHCP::isStandardOption(Option::V6, option->getType())) {
- def = LibDHCP::getOptionDef(Option::V6, option->getType());
- // Definitions for some of the standard options hasn't been
- // implemented so it is ok to leave here.
- if (!def) {
- return;
- }
+ /// @return parser object for specified entry name. Note the caller is
+ /// responsible for deleting the parser created.
+ /// @throw isc::dhcp::DhcpConfigError if trying to create a parser
+ /// for unknown config element
+ DhcpConfigParser* createSubnetConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ if ((config_id.compare("preferred-lifetime") == 0) ||
+ (config_id.compare("valid-lifetime") == 0) ||
+ (config_id.compare("renew-timer") == 0) ||
+ (config_id.compare("rebind-timer") == 0)) {
+ parser = new Uint32Parser(config_id, uint32_values_);
+ } else if ((config_id.compare("subnet") == 0) ||
+ (config_id.compare("interface") == 0) ||
+ (config_id.compare("interface-id") == 0)) {
+ parser = new StringParser(config_id, string_values_);
+ } else if (config_id.compare("pool") == 0) {
+ parser = new Pool6Parser(config_id, pools_);
+ } else if (config_id.compare("pd-pools") == 0) {
+ parser = new PdPoolListParser(config_id, pools_);
+ } else if (config_id.compare("option-data") == 0) {
+ parser = new OptionDataListParser(config_id, options_,
+ global_context_,
+ Dhcp6OptionDataParser::factory);
} else {
- const OptionDefContainerPtr defs =
- option_def_intermediate.getItems(option_space);
- const OptionDefContainerTypeIndex& idx = defs->get<1>();
- const OptionDefContainerTypeRange& range =
- idx.equal_range(option->getType());
- // There is no definition so we have to leave.
- if (std::distance(range.first, range.second) == 0) {
- return;
- }
-
- def = *range.first;
-
- // If the definition exists, it must be non-NULL.
- // Otherwise it is a programming error.
- assert(def);
+ isc_throw(NotImplemented,
+ "parser error: Subnet6 parameter not supported: " << config_id);
}
- // We need to get option definition for the particular option space
- // and code. This definition holds the information whether our
- // option encapsulates any option space.
- // Get the encapsulated option space name.
- std::string encapsulated_space = def->getEncapsulatedSpace();
- // If option space name is empty it means that our option does not
- // encapsulate any option space (does not include sub-options).
- if (!encapsulated_space.empty()) {
- // Get the sub-options that belong to the encapsulated
- // option space.
- const Subnet::OptionContainerPtr sub_opts =
- option_defaults.getItems(encapsulated_space);
- // Append sub-options to the option.
- BOOST_FOREACH(Subnet::OptionDescriptor desc, *sub_opts) {
- if (desc.option) {
- option->addOption(desc.option);
- }
- }
- }
+ return (parser);
}
- /// @brief Create a new subnet using a data from child parsers.
+
+ /// @brief Determines if the given option space name and code describe
+ /// a standard option for the DHCP6 server.
///
- /// @throw isc::dhcp::DhcpConfigError if subnet configuration parsing failed.
- void createSubnet() {
- std::string subnet_txt;
- try {
- subnet_txt = string_values_.getParam("subnet");
- } catch (DhcpConfigError) {
- // rethrow with precise error
- isc_throw(DhcpConfigError,
- "Mandatory subnet definition in subnet missing");
- }
+ /// @param option_space is the name of the option space to consider
+ /// @param code is the numeric option code to consider
+ /// @return returns true if the space and code are part of the server's
+ /// standard options.
+ bool isServerStdOption(std::string option_space, uint32_t code) {
+ return ((option_space.compare("dhcp6") == 0)
+ && LibDHCP::isStandardOption(Option::V6, code));
+ }
- // Remove any spaces or tabs.
- boost::erase_all(subnet_txt, " ");
- boost::erase_all(subnet_txt, "\t");
-
- // The subnet format is prefix/len. We are going to extract
- // the prefix portion of a subnet string to create IOAddress
- // object from it. IOAddress will be passed to the Subnet's
- // constructor later on. In order to extract the prefix we
- // need to get all characters preceding "/".
- size_t pos = subnet_txt.find("/");
- if (pos == string::npos) {
- isc_throw(DhcpConfigError,
- "Invalid subnet syntax (prefix/len expected):" << subnet_txt);
- }
+ /// @brief Returns the option definition for a given option code from
+ /// the DHCP6 server's standard set of options.
+ /// @param code is the numeric option code of the desired option definition.
+ /// @return returns a pointer the option definition
+ OptionDefinitionPtr getServerStdOptionDefinition (uint32_t code) {
+ return (LibDHCP::getOptionDef(Option::V6, code));
+ }
- // Try to create the address object. It also validates that
- // the address syntax is ok.
- IOAddress addr(subnet_txt.substr(0, pos));
- uint8_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+ /// @brief Issues a DHCP6 server specific warning regarding duplicate subnet
+ /// options.
+ ///
+ /// @param code is the numeric option code of the duplicate option
+ /// @param addr is the subnet address
+ /// @todo A means to know the correct logger and perhaps a common
+ /// message would allow this message to be emitted by the base class.
+ virtual void duplicate_option_warning(uint32_t code,
+ isc::asiolink::IOAddress& addr) {
+ LOG_WARN(dhcp6_logger, DHCP6_CONFIG_OPTION_DUPLICATE)
+ .arg(code).arg(addr.toText());
+ }
+ /// @brief Instantiates the IPv6 Subnet based on a given IPv6 address
+ /// and prefix length.
+ ///
+ /// @param addr is IPv6 prefix of the subnet.
+ /// @param len is the prefix length
+ void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) {
// Get all 'time' parameters using inheritance.
// If the subnet-specific value is defined then use it, else
// use the global value. The global value must always be
@@ -1476,182 +472,59 @@ private:
Triplet<uint32_t> pref = getParam("preferred-lifetime");
Triplet<uint32_t> valid = getParam("valid-lifetime");
- // Get interface name. If it is defined, then the subnet is available
- // directly over specified network interface.
- std::string iface;
+ // Get interface-id option content. For now we support string
+ // represenation only
+ std::string ifaceid;
try {
- iface = string_values_.getParam("interface");
- } catch (DhcpConfigError) {
- // iface not mandatory so swallow the exception
- }
-
- /// @todo: Convert this to logger once the parser is working reliably
- stringstream tmp;
- tmp << addr.toText() << "/" << (int)len
- << " with params t1=" << t1 << ", t2=" << t2 << ", pref="
- << pref << ", valid=" << valid;
-
- LOG_INFO(dhcp6_logger, DHCP6_CONFIG_NEW_SUBNET).arg(tmp.str());
-
- // Create a new subnet.
- subnet_.reset(new Subnet6(addr, len, t1, t2, pref, valid));
-
- // Add pools to it.
- for (PoolStorage::iterator it = pools_.begin(); it != pools_.end(); ++it) {
- subnet_->addPool(*it);
+ ifaceid = string_values_->getParam("interface-id");
+ } catch (const DhcpConfigError &) {
+ // interface-id is not mandatory
}
- // Configure interface, if defined
- if (!iface.empty()) {
- if (!IfaceMgr::instance().getIface(iface)) {
- isc_throw(DhcpConfigError, "Specified interface name " << iface
- << " for subnet " << subnet_->toText() << " is not present"
- << " in the system.");
+ // Specifying both interface for locally reachable subnets and
+ // interface id for relays is mutually exclusive. Need to test for
+ // this condition.
+ if (!ifaceid.empty()) {
+ std::string iface;
+ try {
+ iface = string_values_->getParam("interface");
+ } catch (const DhcpConfigError &) {
+ // iface not mandatory
}
- subnet_->setIface(iface);
- }
-
- // We are going to move configured options to the Subnet object.
- // Configured options reside in the container where options
- // are grouped by space names. Thus we need to get all space names
- // and iterate over all options that belong to them.
- std::list<std::string> space_names = options_.getOptionSpaceNames();
- BOOST_FOREACH(std::string option_space, space_names) {
- // Get all options within a particular option space.
- BOOST_FOREACH(Subnet::OptionDescriptor desc,
- *options_.getItems(option_space)) {
- // The pointer should be non-NULL. The validation is expected
- // to be performed by the OptionDataParser before adding an
- // option descriptor to the container.
- assert(desc.option);
- // We want to check whether an option with the particular
- // option code has been already added. If so, we want
- // to issue a warning.
- Subnet::OptionDescriptor existing_desc =
- subnet_->getOptionDescriptor("option_space",
- desc.option->getType());
- if (existing_desc.option) {
- LOG_WARN(dhcp6_logger, DHCP6_CONFIG_OPTION_DUPLICATE)
- .arg(desc.option->getType()).arg(addr.toText());
- }
- // Add sub-options (if any).
- appendSubOptions(option_space, desc.option);
- // In any case, we add the option to the subnet.
- subnet_->addOption(desc.option, false, option_space);
+ if (!iface.empty()) {
+ isc_throw(isc::dhcp::DhcpConfigError,
+ "parser error: interface (defined for locally reachable "
+ "subnets) and interface-id (defined for subnets reachable"
+ " via relays) cannot be defined at the same time for "
+ "subnet " << addr << "/" << (int)len);
}
}
- // Check all global options and add them to the subnet object if
- // they have been configured in the global scope. If they have been
- // configured in the subnet scope we don't add global option because
- // the one configured in the subnet scope always takes precedence.
- space_names = option_defaults.getOptionSpaceNames();
- BOOST_FOREACH(std::string option_space, space_names) {
- // Get all global options for the particular option space.
- BOOST_FOREACH(Subnet::OptionDescriptor desc,
- *option_defaults.getItems(option_space)) {
- // The pointer should be non-NULL. The validation is expected
- // to be performed by the OptionDataParser before adding an
- // option descriptor to the container.
- assert(desc.option);
- // Check if the particular option has been already added.
- // This would mean that it has been configured in the
- // subnet scope. Since option values configured in the
- // subnet scope take precedence over globally configured
- // values we don't add option from the global storage
- // if there is one already.
- Subnet::OptionDescriptor existing_desc =
- subnet_->getOptionDescriptor(option_space, desc.option->getType());
- if (!existing_desc.option) {
- // Add sub-options (if any).
- appendSubOptions(option_space, desc.option);
-
- subnet_->addOption(desc.option, false, option_space);
- }
- }
- }
- }
+ stringstream tmp;
+ tmp << addr << "/" << static_cast<int>(len)
+ << " with params t1=" << t1 << ", t2=" << t2 << ", pref="
+ << pref << ", valid=" << valid;
- /// @brief creates parsers for entries in subnet definition
- ///
- /// @param config_id name od the entry
- ///
- /// @return parser object for specified entry name
- /// @throw isc::dhcp::DhcpConfigError if trying to create a parser
- /// for unknown config element
- DhcpConfigParser* createSubnet6ConfigParser(const std::string& config_id) {
- FactoryMap factories;
-
- factories["preferred-lifetime"] = Uint32Parser::factory;
- factories["valid-lifetime"] = Uint32Parser::factory;
- factories["renew-timer"] = Uint32Parser::factory;
- factories["rebind-timer"] = Uint32Parser::factory;
- factories["subnet"] = StringParser::factory;
- factories["pool"] = PoolParser::factory;
- factories["option-data"] = OptionDataListParser::factory;
- factories["interface"] = StringParser::factory;
-
- FactoryMap::iterator f = factories.find(config_id);
- if (f == factories.end()) {
- // Used for debugging only.
- // return new DebugParser(config_id);
+ LOG_INFO(dhcp6_logger, DHCP6_CONFIG_NEW_SUBNET).arg(tmp.str());
- isc_throw(isc::dhcp::DhcpConfigError,
- "parser error: subnet6 parameter not supported: "
- << config_id);
- }
- return (f->second(config_id));
- }
+ // Create a new subnet.
+ Subnet6* subnet6 = new Subnet6(addr, len, t1, t2, pref, valid);
- /// @brief Returns value for a given parameter (after using inheritance)
- ///
- /// This method implements inheritance. For a given parameter name, it first
- /// checks if there is a global value for it and overwrites it with specific
- /// value if such value was defined in subnet.
- ///
- /// @param name name of the parameter
- /// @return triplet with the parameter name
- /// @throw DhcpConfigError when requested parameter is not present
- isc::dhcp::Triplet<uint32_t> getParam(const std::string& name) {
- uint32_t value = 0;
- try {
- // look for local value
- value = uint32_values_.getParam(name);
- } catch (DhcpConfigError) {
- try {
- // no local, use global value
- value = uint32_defaults.getParam(name);
- } catch (DhcpConfigError) {
- isc_throw(DhcpConfigError, "Mandatory parameter " << name
- << " missing (no global default and no subnet-"
- << "specific value)");
- }
+ // Configure interface-id for remote interfaces, if defined
+ if (!ifaceid.empty()) {
+ OptionBuffer tmp(ifaceid.begin(), ifaceid.end());
+ OptionPtr opt(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+ subnet6->setInterfaceId(opt);
}
- return (Triplet<uint32_t>(value));
+ subnet_.reset(subnet6);
}
- /// storage for subnet-specific uint32 values
- Uint32Storage uint32_values_;
-
- /// storage for subnet-specific integer values
- StringStorage string_values_;
-
- /// storage for pools belonging to this subnet
- PoolStorage pools_;
-
- /// storage for options belonging to this subnet
- OptionStorage options_;
-
- /// parsers are stored here
- ParserCollection parsers_;
-
- /// Pointer to the created subnet object.
- isc::dhcp::Subnet6Ptr subnet_;
};
-/// @brief this class parses a list of subnets
+
+/// @brief this class parses a list of DHCP6 subnets
///
/// This is a wrapper parser that handles the whole list of Subnet6
/// definitions. It iterates over all entries and creates Subnet6ConfigParser
@@ -1661,8 +534,9 @@ public:
/// @brief constructor
///
+ /// @param dummy first argument, always ignored. All parsers accept a
+ /// string parameter "name" as their first argument.
Subnets6ListConfigParser(const std::string&) {
- /// parameter name is ignored
}
/// @brief parses contents of the list
@@ -1672,12 +546,7 @@ public:
///
/// @param subnets_list pointer to a list of IPv6 subnets
void build(ConstElementPtr subnets_list) {
-
- // No need to define FactoryMap here. There's only one type
- // used: Subnet6ConfigParser
-
BOOST_FOREACH(ConstElementPtr subnet, subnets_list->listValue()) {
-
ParserPtr parser(new Subnet6ConfigParser("subnet"));
parser->build(subnet);
subnets_.push_back(parser);
@@ -1687,8 +556,8 @@ public:
/// @brief commits subnets definitions.
///
- /// Iterates over all Subnet6 parsers. Each parser contains definitions
- /// of a single subnet and its parameters and commits each subnet separately.
+ /// Iterates over all Subnet6 parsers. Each parser contains definitions of
+ /// a single subnet and its parameters and commits each subnet separately.
void commit() {
// @todo: Implement more subtle reconfiguration than toss
// the old one and replace with the new one.
@@ -1725,56 +594,78 @@ namespace dhcp {
///
/// @param config_id pointer to received global configuration entry
/// @return parser for specified global DHCPv6 parameter
-/// @throw NotImplemented if trying to create a parser for unknown config element
-DhcpConfigParser* createGlobalDhcpConfigParser(const std::string& config_id) {
- FactoryMap factories;
-
- factories["preferred-lifetime"] = Uint32Parser::factory;
- factories["valid-lifetime"] = Uint32Parser::factory;
- factories["renew-timer"] = Uint32Parser::factory;
- factories["rebind-timer"] = Uint32Parser::factory;
- factories["interface"] = InterfaceListConfigParser::factory;
- factories["subnet6"] = Subnets6ListConfigParser::factory;
- factories["option-data"] = OptionDataListParser::factory;
- factories["option-def"] = OptionDefListParser::factory;
- factories["version"] = StringParser::factory;
- factories["lease-database"] = DbAccessParser::factory;
-
- FactoryMap::iterator f = factories.find(config_id);
- if (f == factories.end()) {
- // Used for debugging only.
- // return new DebugParser(config_id);
-
+/// @throw NotImplemented if trying to create a parser for unknown config
+/// element
+DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ if ((config_id.compare("preferred-lifetime") == 0) ||
+ (config_id.compare("valid-lifetime") == 0) ||
+ (config_id.compare("renew-timer") == 0) ||
+ (config_id.compare("rebind-timer") == 0)) {
+ parser = new Uint32Parser(config_id,
+ globalContext()->uint32_values_);
+ } else if (config_id.compare("interfaces") == 0) {
+ parser = new InterfaceListConfigParser(config_id);
+ } else if (config_id.compare("subnet6") == 0) {
+ parser = new Subnets6ListConfigParser(config_id);
+ } else if (config_id.compare("option-data") == 0) {
+ parser = new OptionDataListParser(config_id,
+ globalContext()->options_,
+ globalContext(),
+ Dhcp6OptionDataParser::factory);
+ } else if (config_id.compare("option-def") == 0) {
+ parser = new OptionDefListParser(config_id,
+ globalContext()->option_defs_);
+ } else if (config_id.compare("version") == 0) {
+ parser = new StringParser(config_id,
+ globalContext()->string_values_);
+ } else if (config_id.compare("lease-database") == 0) {
+ parser = new DbAccessParser(config_id);
+ } else if (config_id.compare("hooks-libraries") == 0) {
+ parser = new HooksLibrariesParser(config_id);
+ } else {
isc_throw(NotImplemented,
- "Parser error: Global configuration parameter not supported: "
- << config_id);
+ "Parser error: Global configuration parameter not supported: "
+ << config_id);
}
- return (f->second(config_id));
+
+ return (parser);
}
-ConstElementPtr
-configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
+isc::data::ConstElementPtr
+configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) {
if (!config_set) {
ConstElementPtr answer = isc::config::createAnswer(1,
string("Can't parse NULL config"));
return (answer);
}
- /// @todo: append most essential info here (like "2 new subnets configured")
+ /// @todo: Append most essential info here (like "2 new subnets configured")
string config_details;
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND, DHCP6_CONFIG_START).arg(config_set->str());
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_COMMAND,
+ DHCP6_CONFIG_START).arg(config_set->str());
+
+ // Before starting any subnet operations, let's reset the subnet-id counter,
+ // so newly recreated configuration starts with first subnet-id equal 1.
+ Subnet::resetSubnetID();
// Some of the values specified in the configuration depend on
- // other values. Typically, the values in the subnet4 structure
+ // other values. Typically, the values in the subnet6 structure
// depend on the global values. Also, option values configuration
// must be performed after the option definitions configurations.
// Thus we group parsers and will fire them in the right order:
- // all parsers other than subnet4 and option-data parser,
- // option-data parser, subnet4 parser.
+ // all parsers other than subnet6 and option-data parser,
+ // option-data parser, subnet6 parser.
ParserCollection independent_parsers;
ParserPtr subnet_parser;
ParserPtr option_parser;
+ ParserPtr iface_parser;
+
+ // Some of the parsers alter state of the system that can't easily
+ // be undone. (Or alter it in a way such that undoing the change
+ // has the same risk of failure as doing the change.)
+ ParserPtr hooks_parser;
// The subnet parsers implement data inheritance by directly
// accessing global storage. For this reason the global data
@@ -1783,10 +674,7 @@ configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
// parsing operation fails after the global storage has been
// modified. We need to preserve the original global data here
// so as we can rollback changes when an error occurs.
- Uint32Storage uint32_local(uint32_defaults);
- StringStorage string_local(string_defaults);
- OptionStorage option_local(option_defaults);
- OptionDefStorage option_def_local(option_def_intermediate);
+ ParserContext original_context(*globalContext());
// answer will hold the result.
ConstElementPtr answer;
@@ -1803,15 +691,25 @@ configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
const std::map<std::string, ConstElementPtr>& values_map =
config_set->mapValue();
BOOST_FOREACH(config_pair, values_map) {
- ParserPtr parser(createGlobalDhcpConfigParser(config_pair.first));
+ ParserPtr parser(createGlobal6DhcpConfigParser(config_pair.first));
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PARSER_CREATED)
.arg(config_pair.first);
if (config_pair.first == "subnet6") {
subnet_parser = parser;
-
} else if (config_pair.first == "option-data") {
option_parser = parser;
-
+ } else if (config_pair.first == "hooks-libraries") {
+ // Executing the commit will alter currently loaded hooks
+ // libraries. Check if the supplied libraries are valid,
+ // but defer the commit until after everything else has
+ // committed.
+ hooks_parser = parser;
+ hooks_parser->build(config_pair.second);
+ } else if (config_pair.first == "interfaces") {
+ // The interface parser is independent from any other parser and
+ // can be run here before other parsers.
+ parser->build(config_pair.second);
+ iface_parser = parser;
} else {
// Those parsers should be started before other
// parsers so we can call build straight away.
@@ -1865,6 +763,17 @@ configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
if (subnet_parser) {
subnet_parser->commit();
}
+
+ if (iface_parser) {
+ iface_parser->commit();
+ }
+
+ // This occurs last as if it succeeds, there is no easy way to
+ // revert it. As a result, the failure to commit a subsequent
+ // change causes problems when trying to roll back.
+ if (hooks_parser) {
+ hooks_parser->commit();
+ }
}
catch (const isc::Exception& ex) {
LOG_ERROR(dhcp6_logger, DHCP6_PARSER_COMMIT_FAIL).arg(ex.what());
@@ -1884,10 +793,7 @@ configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
// Rollback changes as the configuration parsing failed.
if (rollback) {
- std::swap(uint32_defaults, uint32_local);
- std::swap(string_defaults, string_local);
- std::swap(option_defaults, option_local);
- std::swap(option_def_intermediate, option_def_local);
+ globalContext().reset(new ParserContext(original_context));
return (answer);
}
@@ -1898,5 +804,10 @@ configureDhcp6Server(Dhcpv6Srv&, ConstElementPtr config_set) {
return (answer);
}
+ParserContextPtr& globalContext() {
+ static ParserContextPtr global_context_ptr(new ParserContext(Option::V6));
+ return (global_context_ptr);
+}
+
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/bin/dhcp6/config_parser.h b/src/bin/dhcp6/config_parser.h
index 6d7a807..bd571af 100644
--- a/src/bin/dhcp6/config_parser.h
+++ b/src/bin/dhcp6/config_parser.h
@@ -20,6 +20,8 @@
#include <cc/data.h>
#include <exceptions/exceptions.h>
+#include <dhcpsrv/dhcp_parsers.h>
+
#include <string>
namespace isc {
@@ -29,9 +31,9 @@ class Dhcpv6Srv;
/// @brief Configures DHCPv6 server
///
-/// This function is called every time a new configuration is received. The extra
-/// parameter is a reference to DHCPv6 server component. It is currently not used
-/// and CfgMgr::instance() is accessed instead.
+/// This function is called every time a new configuration is received. The
+/// extra parameter is a reference to DHCPv6 server component. It is currently
+/// not used and CfgMgr::instance() is accessed instead.
///
/// This method does not throw. It catches all exceptions and returns them as
/// reconfiguration statuses. It may return the following response codes:
@@ -47,6 +49,11 @@ class Dhcpv6Srv;
isc::data::ConstElementPtr
configureDhcp6Server(Dhcpv6Srv& server, isc::data::ConstElementPtr config_set);
+/// @brief Returns the global context
+///
+/// @returns a reference to the global context
+ParserContextPtr& globalContext();
+
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/bin/dhcp6/ctrl_dhcp6_srv.cc b/src/bin/dhcp6/ctrl_dhcp6_srv.cc
index c3488e5..e42c83b 100644
--- a/src/bin/dhcp6/ctrl_dhcp6_srv.cc
+++ b/src/bin/dhcp6/ctrl_dhcp6_srv.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -20,21 +20,26 @@
#include <config/ccsession.h>
#include <dhcp/iface_mgr.h>
#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/cfgmgr.h>
#include <dhcp6/config_parser.h>
#include <dhcp6/ctrl_dhcp6_srv.h>
#include <dhcp6/dhcp6_log.h>
#include <dhcp6/spec_config.h>
#include <exceptions/exceptions.h>
+#include <hooks/hooks_manager.h>
#include <util/buffer.h>
#include <cassert>
#include <iostream>
+#include <string>
+#include <vector>
using namespace isc::asiolink;
using namespace isc::cc;
using namespace isc::config;
using namespace isc::data;
using namespace isc::dhcp;
+using namespace isc::hooks;
using namespace isc::log;
using namespace isc::util;
using namespace std;
@@ -100,7 +105,27 @@ ControlledDhcpv6Srv::dhcp6ConfigHandler(ConstElementPtr new_config) {
}
// Configure the server.
- return (configureDhcp6Server(*server_, merged_config));
+ ConstElementPtr answer = configureDhcp6Server(*server_, merged_config);
+
+ // Check that configuration was successful. If not, do not reopen sockets.
+ int rcode = 0;
+ parseAnswer(rcode, answer);
+ if (rcode != 0) {
+ return (answer);
+ }
+
+ // Configuration may change active interfaces. Therefore, we have to reopen
+ // sockets according to new configuration. This operation is not exception
+ // safe and we really don't want to emit exceptions to the callback caller.
+ // Instead, catch an exception and create appropriate answer.
+ try {
+ server_->openActiveSockets(server_->getPort());
+ } catch (const std::exception& ex) {
+ std::ostringstream err;
+ err << "failed to open sockets after server reconfiguration: " << ex.what();
+ answer = isc::config::createAnswer(1, err.str());
+ }
+ return (answer);
}
ConstElementPtr
@@ -120,6 +145,21 @@ ControlledDhcpv6Srv::dhcp6CommandHandler(const string& command, ConstElementPtr
ConstElementPtr answer = isc::config::createAnswer(0,
"Shutting down.");
return (answer);
+
+ } else if (command == "libreload") {
+ // TODO delete any stored CalloutHandles referring to the old libraries
+ // Get list of currently loaded libraries and reload them.
+ vector<string> loaded = HooksManager::getLibraryNames();
+ bool status = HooksManager::loadLibraries(loaded);
+ if (!status) {
+ LOG_ERROR(dhcp6_logger, DHCP6_HOOKS_LIBS_RELOAD_FAIL);
+ ConstElementPtr answer = isc::config::createAnswer(1,
+ "Failed to reload hooks libraries.");
+ return (answer);
+ }
+ ConstElementPtr answer = isc::config::createAnswer(0,
+ "Hooks libraries successfully reloaded.");
+ return (answer);
}
ConstElementPtr answer = isc::config::createAnswer(1,
@@ -172,8 +212,13 @@ void ControlledDhcpv6Srv::establishSession() {
try {
// Pull the full configuration out from the session.
configureDhcp6Server(*this, config_session_->getFullConfig());
- } catch (const DhcpConfigError& ex) {
+ // Configuration may disable or enable interfaces so we have to
+ // reopen sockets according to new configuration.
+ openActiveSockets(getPort());
+
+ } catch (const std::exception& ex) {
LOG_ERROR(dhcp6_logger, DHCP6_CONFIG_LOAD_FAIL).arg(ex.what());
+
}
/// Integrate the asynchronous I/O model of BIND 10 configuration
@@ -228,6 +273,5 @@ ControlledDhcpv6Srv::execDhcpv6ServerCommand(const std::string& command_id,
}
}
-
};
};
diff --git a/src/bin/dhcp6/dhcp6.dox b/src/bin/dhcp6/dhcp6.dox
index 4376a2a..c722472 100644
--- a/src/bin/dhcp6/dhcp6.dox
+++ b/src/bin/dhcp6/dhcp6.dox
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -90,6 +90,135 @@
simple as possible. In fact, currently the code has to call Subnet6->getT1() and
do not implement any fancy inheritance logic.
+ at section dhcpv6DDNSIntegration DHCPv6 Server Support for the Dynamic DNS Updates
+
+The DHCPv6 server supports processing of the DHCPv6 Client FQDN Option described in
+the RFC4704. This Option is sent by the DHCPv6 client to instruct the server to
+update the DNS mappings for the acquired lease. A client may send its fully
+qualified domain name, a partial name or it may choose that server will generate
+the name. In the last case, the client sends an empty domain-name field in the
+DHCPv6 Client FQDN Option.
+
+As described in RFC4704, client may choose that the server delegates the forward
+DNS update to the client and that the server performs the reverse update only. Current
+version of the DHCPv6 server does not support delegation of the forward update
+to the client. The implementation of this feature is planned for the future releases.
+
+The b10-dhcp-ddns process is responsible for the actual communication with the DNS
+server, i.e. to send DNS Update messages. The b10-dhcp6 module is responsible
+for generating so called @ref isc::dhcp_ddns::NameChangeRequest and sending it to the
+b10-dhcp-ddns module. The @ref isc::dhcp_ddns::NameChangeRequest object represents changes to the
+DNS bindings, related to acquisition, renewal or release of the lease. The bind10-dhcp6
+module implements the simple FIFO queue of the NameChangeRequest objects. The module
+logic, which processes the incoming DHCPv6 Client FQDN Options puts these requests
+into the FIFO queue.
+
+ at todo Currently the FIFO queue is not processed after the NameChangeRequests are
+generated and added to it. In the future implementation steps it is planned to create
+a code which will check if there are any outstanding requests in the queue and
+send them to the b10-dhcp-ddns module when server is idle waiting for DHCP messages.
+
+In the simplest case, when client gets one address from the server, a DHCPv6 server
+may generate 0, 1 or 2 NameChangeRequests during single message processing.
+Server generates no NameChangeRequests if it is not configured to update DNS
+ or it rejects the DNS update for any other reason.
+
+Server may generate 1 NameChangeRequest in a situation when a client acquires a
+new lease or it releases an existing lease. In the former case, the NameChangeRequest
+type is CHG_ADD, which indicates that the b10-dhcp-ddns module should add a new DNS
+binding for the client, and it is assumed that there is no DNS binding for this
+client already. In the latter case, the NameChangeRequest type is CHG_REMOVE to
+indicate to the b10-dhcp-ddns module that the existing DNS binding should be removed
+from the DNS. The binding consists of the forward and reverse mapping.
+A server may only remove the mapping which it had added. Therefore, the lease database
+holds an information which updates (no update, reverse only update, forward only update,
+both reverse and forward update) have been performed when the lease was acquired.
+Server checks this information to make a decision which mapping it is supposed to
+remove when a lease is released.
+
+Server may generate 2 NameChangeRequests in case the client is renewing a lease and
+it already has a DNS binding for that lease. Note, that renewal may be triggered
+as a result of sending a RENEW message as well as the REQUEST message. In both cases
+DHCPv6 server will check if there is an existing lease for the client which has sent
+a message, and it will check in the lease database if the DNS Updates had
+been performed for this client. If the notion of client's FQDN changes comparing to
+the information stored in the lease database, the DHCPv6 has to remove an existing
+binding from the DNS and then add a new binding according to the new FQDN information
+received from the client. If the FQDN sent in the message which triggered a renewal
+doesn't change (comparing to the information in the lease database) the NameChangeRequest
+is not generated.
+
+In the more complex scenarios, when server sends multiple IA_NA options, each holding
+multiple IAADDR options, server will generate more NameChangeRequests for a single
+message being processed. That is 0, 1, 2 for the individual IA_NA. Generation of
+the distinct NameChangeRequests for each IADDR is not supported yet.
+
+The DHCPv6 Client FQDN Option comprises "NOS" flags which communicate to the
+server what updates (if any) client expects the server to perform. Server
+may be configured to obey client's preference or do FQDN processing in a
+different way. If the server overrides client's preference it will communicate it
+by sending the DHCPv6 Client FQDN Option in its responses to a client, with
+appropriate flags set.
+
+ at todo Note, that current implementation doesn't allow configuration of the server's behaviour
+with respect to DNS Updates. This is planned for the future. The default behaviour is
+constituted by the set of constants defined in the (upper part of) dhcp6_srv.cc file.
+Once the configuration is implemented, these constants will be removed.
+
@todo Add section about setting up options and their definitions with bindctl.
+ at section dhcpv6OptionsParse Custom functions to parse message options
+
+The DHCPv6 server implementation provides a generic support to define option
+formats and set option values. A number of options formats have been defined
+for standard options in libdhcp++. However, the formats for vendor specific
+options are dynamically configured by the server's administrator and thus can't
+be stored in libdhcp++. Such option formats are stored in the
+ at c isc::dhcp::CfgMgr. The libdhcp++ provides functions for recursive parsing
+of options which may be encapsulated by other options up to the any level of
+encapsulation but these functions are unaware of the option formats defined
+in the @c isc::dhcp::CfgMgr because they belong to a different library.
+Therefore, the generic functions @c isc::dhcp::LibDHCP::unpackOptions4 and
+ at c isc::dhcp::LibDHCP::unpackOptions6 are only useful to parse standard
+options which definitions are provided in the libdhcp++. In order to overcome
+this problem a callback mechanism has been implemented in @c Option and @c Pkt6
+classes. By installing a callback function on the instance of the @c Pkt6 the
+server may provide a custom implementation of the options parsing algorithm.
+This callback function will take precedence over the @c LibDHCP::unpackOptions6
+and @c LibDHCP::unpackOptions4 functions. With this approach, the callback is
+implemented within the context of the server and it has access to all objects
+which define its configuration (including dynamically created option
+definitions).
+
+ at section dhcpv6Classifier DHCPv6 Client Classification
+
+Kea DHCPv6 server currently supports simplified client classification. It is called
+"simplified", because the incoming packets are classified based on the content
+of the vendor class (16) option. More flexible classification is planned, but there
+are no specific development dates agreed.
+
+For each incoming packet, @ref isc::dhcp::Dhcpv6Srv::classifyPacket() method is
+called. It attempts to extract content of the vendor class option and interprets
+as a name of the class. Although the RFC3315 says that the vendor class may
+contain more than one chunk of data, the existing code handles only one data
+block, because that is what actual devices use. For now, the code has been
+tested with two classes used in cable modem networks: eRouter1.0 and docsis3.0,
+but any other content of the vendor class option will be interpreted as a class
+name.
+
+In principle any given packet can belong to zero or more classes. As the current
+classifier is very modest, there's only one way to assign a class (based on vendor class
+option), the ability to assign more than one class to a packet is not yet exercised.
+Neverthless, there is such a possibility and it will be used in a near future. To
+check whether a packet belongs to given class, isc::dhcp::Pkt6::inClass method should
+be used.
+
+Currently there is no class behaviour coded in DHCPv6, hence no v6 equivalent of
+ at ref isc::dhcp::Dhcpv4Srv::classSpecificProcessing. Should any need for such a code arise,
+it will be conducted in an external hooks library.
+
+ @section dhcpv6Other Other DHCPv6 topics
+
+ For hooks API support in DHCPv6, see @ref dhcpv6Hooks.
+
*/
diff --git a/src/bin/dhcp6/dhcp6.spec b/src/bin/dhcp6/dhcp6.spec
index 1129aec..3810fad 100644
--- a/src/bin/dhcp6/dhcp6.spec
+++ b/src/bin/dhcp6/dhcp6.spec
@@ -3,16 +3,30 @@
"module_name": "Dhcp6",
"module_description": "DHCPv6 server daemon",
"config_data": [
- { "item_name": "interface",
+ {
+ "item_name": "hooks-libraries",
+ "item_type": "list",
+ "item_optional": true,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "hooks-library",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ }
+ },
+
+ { "item_name": "interfaces",
"item_type": "list",
"item_optional": false,
- "item_default": [ "all" ],
+ "item_default": [ "*" ],
"list_item_spec":
{
"item_name": "interface_name",
"item_type": "string",
"item_optional": false,
- "item_default": "all"
+ "item_default": "*"
}
} ,
@@ -199,6 +213,12 @@
"item_default": ""
},
+ { "item_name": "interface-id",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+
{ "item_name": "renew-timer",
"item_type": "integer",
"item_optional": false,
@@ -234,6 +254,38 @@
"item_default": ""
}
},
+ {
+ "item_name": "pd-pools",
+ "item_type": "list",
+ "item_optional": true,
+ "item_default": [],
+ "list_item_spec":
+ {
+ "item_name": "pd-pool",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "map_item_spec": [
+ {
+ "item_name": "prefix",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+ {
+ "item_name": "prefix-len",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 128
+ },
+ {
+ "item_name": "delegated-len",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 128
+ }]
+ }
+ },
{ "item_name": "option-data",
"item_type": "list",
"item_optional": false,
@@ -289,6 +341,12 @@
"item_optional": true
}
]
+ },
+
+ {
+ "command_name": "libreload",
+ "command_description": "Reloads the current hooks libraries.",
+ "command_args": []
}
]
}
diff --git a/src/bin/dhcp6/dhcp6_hooks.dox b/src/bin/dhcp6/dhcp6_hooks.dox
new file mode 100644
index 0000000..b21ad74
--- /dev/null
+++ b/src/bin/dhcp6/dhcp6_hooks.dox
@@ -0,0 +1,239 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ @page dhcpv6Hooks The Hooks API for the DHCPv6 Server
+
+ @section dhcpv6HooksIntroduction Introduction
+ BIND10 features an API (the "Hooks" API) that allows user-written code to
+ be integrated into BIND 10 and called at specific points in its processing.
+ An overview of the API and a tutorial for writing such code can be found in
+ the @ref hooksdgDevelopersGuide. Information for BIND 10 maintainers can be
+ found in the @ref hooksComponentDeveloperGuide.
+
+ This manual is more specialised and is aimed at developers of hook
+ code for the DHCPv6 server. It describes each hook point, what the callouts
+ attached to the hook are able to do, and the arguments passed to the
+ callouts. Each entry in this manual has the following information:
+
+ - Name of the hook point.
+ - Arguments for the callout. As well as the argument name and data type, the
+ information includes the direction, which can be one of:
+ - @b in - the server passes values to the callout but ignored any data
+ returned.
+ - @b out - the callout is expected to set this value.
+ - <b>in/out</b> - the server passes a value to the callout and uses whatever
+ value the callout sends back. Note that the callout may choose not to
+ do any modification, in which case the server will use whatever value
+ it sent to the callout.
+ - Description of the hook. This explains where in the processing the hook
+ is located, the possible actions a callout attached to this hook could take,
+ and a description of the data passed to the callouts.
+ - Skip flag action: the action taken by the server if a callout chooses to set
+ the "skip" flag.
+
+ at section dhcpv6HooksHookPoints Hooks in the DHCPv6 Server
+
+The following list is ordered by appearance of specific hook points during
+packet processing. Hook points that are not specific to packet processing
+(e.g. lease expiration) will be added to the end of this list.
+
+ @subsection dhcpv6HooksBuffer6Receive buffer6_receive
+
+ - @b Arguments:
+ - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when an incoming DHCPv6
+ packet is received and the data stored in a buffer. The sole argument -
+ query6 - contains a pointer to an isc::dhcp::Pkt6 object that contains
+ the received information stored in the data_ field. Basic information
+ like protocol, source/destination addresses and ports are set, but
+ the contents of the buffer have not yet been parsed. That means that
+ the options_ field (that will eventually contain a list of objects
+ representing the received options) is empty, so none of the methods
+ that operate on it (e.g., getOption()) will work. The primary purpose
+ of this early call is to offer the ability to modify incoming packets
+ in their raw form. Unless you need to access to the raw data, it is
+ usually better to install your callout on the pkt6_receive hook point.
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the
+ server will assume that the callout parsed the buffer and added then
+ necessary option objects to the options_ field; the server will not
+ do any parsing. If the callout sets the skip flag but does not parse
+ the buffer, the server will most probably drop the packet due to
+ the absence of mandatory options. If you want to drop the packet,
+ see the description of the skip flag in the pkt6_receive hook point.
+
+ @subsection dhcpv6HooksPkt6Receive pkt6_receive
+
+ - @b Arguments:
+ - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when an incoming DHCPv6
+ packet is received and its content is parsed. The sole argument -
+ query6 - contains a pointer to an isc::dhcp::Pkt6 object that contains
+ all information regarding incoming packet, including its source and
+ destination addresses, the interface over which it was received, a list
+ of all options present within and relay information. All fields of
+ the Pkt6 object can be modified at this time, except data_. (data_
+ contains the incoming packet as raw buffer. By the time this hook is
+ reached, that information has already been parsed and is available though
+ other fields in the Pkt6 object. For this reason, it doesn't make
+ sense to modify it.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server will
+ drop the packet and start processing the next one. The reason for the drop
+ will be logged if logging is set to the appropriate debug level.
+
+ at subsection dhcpv6HooksSubnet6Select subnet6_select
+
+ - @b Arguments:
+ - name: @b query6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+ - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in/out</b>
+ - name: @b subnet6collection, type: const isc::dhcp::Subnet6Collection *, direction: <b>in</b>
+
+ - @b Description: This callout is executed when a subnet is being
+ selected for the incoming packet. All parameters, addresses and
+ prefixes will be assigned from that subnet. A callout can select a
+ different subnet if it wishes so, the list of all subnets currently
+ configured being provided as 'subnet6collection'. The list itself must
+ not be modified.
+
+ - <b>Skip flag action</b>: If any callout installed on 'subnet6_select'
+ sets the skip flag, the server will not select any subnet. Packet processing
+ will continue, but will be severely limited (i.e. only global options
+ will be assigned).
+
+ at subsection dhcpv6HooksLease6Select lease6_select
+
+ - @b Arguments:
+ - name: @b subnet6, type: isc::dhcp::Subnet6Ptr, direction: <b>in</b>
+ - name: @b fake_allocation, type: bool, direction: <b>in</b>
+ - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed after the server engine
+ has selected a lease for client's request but before the lease
+ has been inserted into the database. Any modifications made to the
+ isc::dhcp::Lease6 object will be stored in the lease's record in the
+ database. The callout should make sure that any modifications are
+ sanity checked as the server will use that data as is with no further
+ checking.\n\n The server processes lease requests for SOLICIT and
+ REQUEST in a very similar way. The only major difference is that
+ for SOLICIT the lease is just selected; it is not inserted into
+ the database. It is possible to distinguish between SOLICIT and
+ REQUEST by checking value of the fake_allocation flag: a value of true
+ means that the lease won't be inserted into the database (SOLICIT),
+ a value of false means that it will (REQUEST).
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease6_select'
+ sets the skip flag, the server will not assign that particular lease.
+ Packet processing will continue and the client may get other addresses
+ or prefixes if it requested more than one address and/or prefix.
+
+ at subsection dhcpv6HooksLease6Renew lease6_renew
+
+ - @b Arguments:
+ - name: @b query6, type: isc::dhcp::PktPtr, direction: <b>in</b>
+ - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+ - name: @b ia_na, type: boost::shared_ptr<Option6IA>, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when the server engine is
+ about to renew an existing lease. The client's request is provided as
+ the query6 argument and the existing lease with the appropriate fields
+ already modified is given in the lease6 argument. The final argument,
+ ia_na, is the IA_NA option that will be sent back to the client.
+ Callouts installed on the lease6_renew may modify the content of
+ the lease6 object. Care should be taken however, as that modified
+ information will be written to the database without any further
+ checking. \n\n Although the envisaged usage assumes modification of T1,
+ T2, preferred and valid lifetimes only, other parameters associated
+ with the lease may be modified as well. The only exception is the addr_
+ field, which must not be modified as it is used by the database to
+ select the existing lease to be updated. Care should also be taken to
+ modify the ia_na argument to match any changes in the lease6 argument.
+ If a client sends more than one IA_NA option, callouts will be called
+ separately for each IA_NA instance. The callout will be called only
+ when the update is valid, i.e. conditions such as an invalid addresses
+ or invalid iaid renewal attempts will not trigger this hook point.
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease6_renew'
+ sets the skip flag, the server will not renew the lease. Under these
+ circumstances, the callout should modify the ia_na argument to reflect
+ this fact; otherwise the client will think the lease was renewed and continue
+ to operate under this assumption.
+
+ at subsection dhcpv6HooksLease6Release lease6_release
+
+ - @b Arguments:
+ - name: @b query6, type: isc::dhcp::PktPtr, direction: <b>in</b>
+ - name: @b lease6, type: isc::dhcp::Lease6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when the server engine is
+ about to release an existing lease. The client's request is provided
+ as the query6 argument and the existing lease is given in the lease6
+ argument. Although the lease6 structure may be modified, it doesn't
+ make sense to do so as it will be destroyed immediately the callouts
+ finish execution.
+
+ - <b>Skip flag action</b>: If any callout installed on 'lease6_release'
+ sets the skip flag, the server will not delete the lease, which will
+ remain in the database until it expires. However, the server will send out
+ the response back to the client as if it did.
+
+ at subsection dhcpv6HooksPkt6Send pkt6_send
+
+ - @b Arguments:
+ - name: @b response6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when server's response
+ is about to be send back to the client. The sole argument - response6 -
+ contains a pointer to an isc::dhcp::Pkt6 object that contains the
+ packet, with set source and destination addresses, interface over which
+ it will be send, list of all options and relay information. All fields
+ of the Pkt6 object can be modified at this time. It should be noted that
+ unless the callout sets the skip flag (see below), anything placed in the
+ bufferOut_ field will be overwritten when the callout returns.
+ (bufferOut_ is scratch space used for constructing the packet.)
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server
+ will assume that the callout did pack the transaction-id, message type and
+ option objects into the bufferOut_ field and will skip packing part.
+ Note that if the callout sets skip flag, but did not prepare the
+ output buffer, the server will send a zero sized message that will be
+ ignored by the client. If you want to drop the packet, please see
+ skip flag in the buffer6_send hook point.
+
+ at subsection dhcpv6HooksBuffer6Send buffer6_send
+
+ - @b Arguments:
+ - name: @b response6, type: isc::dhcp::Pkt6Ptr, direction: <b>in/out</b>
+
+ - @b Description: This callout is executed when server's response is
+ assembled into binary form and is about to be send back to the
+ client. The sole argument - response6 - contains a pointer to an
+ isc::dhcp::Pkt6 object that contains the packet, with set source and
+ destination addresses, interface over which it will be sent, list of
+ all options and relay information. All options are already encoded
+ in bufferOut_ field. It doesn't make sense to modify anything but the
+ contents of bufferOut_ at this time (although if it is a requirement
+ to modify that data, it will probably be found easier to modify the
+ option objects in a callout attached to the pkt6_send hook).
+
+ - <b>Skip flag action</b>: If any callout sets the skip flag, the server
+ will drop this response packet. However, the original request packet
+ from a client has been processed, so server's state has most likely changed
+ (e.g. lease was allocated). Setting this flag merely stops the change
+ being communicated to the client.
+
+*/
diff --git a/src/bin/dhcp6/dhcp6_log.h b/src/bin/dhcp6/dhcp6_log.h
index 23202da..244151c 100644
--- a/src/bin/dhcp6/dhcp6_log.h
+++ b/src/bin/dhcp6/dhcp6_log.h
@@ -38,6 +38,9 @@ const int DBG_DHCP6_COMMAND = DBGLVL_COMMAND;
// Trace basic operations within the code.
const int DBG_DHCP6_BASIC = DBGLVL_TRACE_BASIC;
+// Trace hook related operations
+const int DBG_DHCP6_HOOKS = DBGLVL_TRACE_BASIC;
+
// Trace detailed operations, including errors raised when processing invalid
// packets. (These are not logged at severities of WARN or higher for fear
// that a set of deliberately invalid packets set to the server could overwhelm
diff --git a/src/bin/dhcp6/dhcp6_messages.mes b/src/bin/dhcp6/dhcp6_messages.mes
index 6b61473..46449d8 100644
--- a/src/bin/dhcp6/dhcp6_messages.mes
+++ b/src/bin/dhcp6/dhcp6_messages.mes
@@ -1,4 +1,4 @@
-# Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -14,6 +14,11 @@
$NAMESPACE isc::dhcp
+% DHCP6_ACTIVATE_INTERFACE activating interface %1
+This message is printed when DHCPv6 server enabled an interface to be used
+to receive DHCPv6 traffic. IPv6 socket on this interface will be opened once
+Interface Manager starts up procedure of opening sockets.
+
% DHCP6_CCSESSION_STARTED control channel session started on socket %1
A debug message issued during startup after the IPv6 DHCP server has
successfully established a session with the BIND 10 control channel.
@@ -22,6 +27,10 @@ successfully established a session with the BIND 10 control channel.
This debug message is issued just before the IPv6 DHCP server attempts
to establish a session with the BIND 10 control channel.
+% DHCP6_CLASS_ASSIGNED client packet has been assigned to the following class(es): %1
+This debug message informs that incoming packet has been assigned to specified
+class or classes. This is a norma
+
% DHCP6_CLIENTID_MISSING mandatory client-id option is missing, message from %1 dropped
This error message indicates that the received message is being dropped
because it does not include the mandatory client-id option necessary for
@@ -65,34 +74,169 @@ This informational message is printed every time the IPv6 DHCP server
is started. It indicates what database backend type is being to store
lease and other information.
-% DHCP6_LEASE_ADVERT lease %1 advertised (client duid=%2, iaid=%3)
+% DHCP6_DDNS_CREATE_ADD_NAME_CHANGE_REQUEST created name change request: %1
+This debug message is logged when the new Name Change Request has been created
+to perform the DNS Update, which adds new RRs.
+
+% DHCP6_DDNS_CREATE_REMOVE_NAME_CHANGE_REQUEST created name change request: %1
+This debug message is logged when the new Name Change Request has been created
+to perform the DNS Update, which removes RRs from the DNS.
+
+% DHCP6_DDNS_LEASE_ASSIGN_FQDN_CHANGE FQDN for the allocated lease: %1 has changed. New values: hostname = %2, reverse mapping = %3, forward mapping = %4
+This debug message is logged when FQDN mapping for a particular lease has
+been changed by the recent Request message. This mapping will be changed in DNS.
+
+% DHCP6_DDNS_LEASE_RENEW_FQDN_CHANGE FQDN for the renewed lease: %1 has changed
+New values: hostname = %2, reverse mapping = %3, forward mapping = %4
+This debug message is logged when FQDN mapping for a particular lease has been
+changed by the recent Renew message. This mapping will be changed in DNS.
+
+% DHCP6_DEACTIVATE_INTERFACE deactivate interface %1
+This message is printed when DHCPv6 server disables an interface from being
+used to receive DHCPv6 traffic. Sockets on this interface will not be opened
+by the Interface Manager until interface is enabled.
+
+% DHCP6_DDNS_SEND_FQDN sending DHCPv6 Client FQDN Option to the client: %1
+This debug message is logged when server includes an DHCPv6 Client FQDN Option
+in its response to the client.
+
+% DHCP6_DDNS_RECEIVE_FQDN received DHCPv6 Client FQDN Option: %1
+This debug message is logged when server has found the DHCPv6 Client FQDN Option
+sent by a client and started processing it.
+
+% DHCP6_DDNS_REMOVE_EMPTY_HOSTNAME FQDN for the lease being deleted is empty: %1
+This error message is issued when a lease being deleted contains an indication
+that the DNS Update has been performed for it, but the FQDN is missing for this
+lease. This is an indication that someone may have messed up in the lease
+database.
+
+% DHCP6_DDNS_REMOVE_INVALID_HOSTNAME FQDN for the lease being deleted has invalid format: %1
+This error message is issued when a lease being deleted contains an indication
+that the DNS Update has been performed for it, but the FQDN held in the lease
+database has invalid format and can't be transformed to the canonical on-wire
+format.
+
+% DHCP6_HOOK_BUFFER_RCVD_SKIP received DHCPv6 buffer was dropped because a callout set the skip flag
+This debug message is printed when a callout installed on buffer6_receive
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+
+% DHCP6_HOOK_BUFFER_SEND_SKIP prepared DHCPv6 response was dropped because a callout set the skip flag
+This debug message is printed when a callout installed on buffer6_send
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+Server completed all the processing (e.g. may have assigned, updated
+or released leases), but the response will not be send to the client.
+
+% DHCP6_HOOK_LEASE6_RENEW_SKIP DHCPv6 lease was not renewed because a callout set the skip flag
+This debug message is printed when a callout installed on lease6_renew
+hook point set the skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to not renew a lease. If
+client requested renewal of multiples leases (by sending multiple IA
+options), the server will skip the renewal of the one in question and
+will proceed with other renewals as usual.
+
+% DHCP6_HOOK_LEASE6_RELEASE_NA_SKIP DHCPv6 address lease was not released because a callout set the skip flag
+This debug message is printed when a callout installed on the
+lease6_release hook point set the skip flag. For this particular hook
+point, the setting of the flag by a callout instructs the server to not
+release a lease. If a client requested the release of multiples leases
+(by sending multiple IA options), the server will retain this particular
+lease and proceed with other releases as usual.
+
+% DHCP6_HOOK_LEASE6_RELEASE_PD_SKIP DHCPv6 prefix lease was not released because a callout set the skip flag
+This debug message is printed when a callout installed on lease6_release
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to not release
+a lease. If client requested release of multiples leases (by sending
+multiple IA options), the server will retains this particular lease and
+will proceed with other renewals as usual.
+
+% DHCP6_HOOK_PACKET_RCVD_SKIP received DHCPv6 packet was dropped because a callout set the skip flag
+This debug message is printed when a callout installed on the pkt6_receive
+hook point set the skip flag. For this particular hook point, the
+setting of the flag by a callout instructs the server to drop the packet.
+
+% DHCP6_HOOK_PACKET_SEND_SKIP prepared DHCPv6 response was not sent because a callout set the skip flag
+This debug message is printed when a callout installed on the pkt6_send
+hook point set the skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to drop the packet. This
+effectively means that the client will not get any response, even though
+the server processed client's request and acted on it (e.g. possibly
+allocated a lease).
+
+% DHCP6_HOOK_SUBNET6_SELECT_SKIP no subnet was selected because a callout set the skip flag
+This debug message is printed when a callout installed on the
+subnet6_select hook point set the skip flag. For this particular hook
+point, the setting of the flag instructs the server not to choose a
+subnet, an action that severely limits further processing; the server
+will be only able to offer global options - no addresses or prefixes
+will be assigned.
+
+% DHCP6_HOOKS_LIBS_RELOAD_FAIL reload of hooks libraries failed
+A "libreload" command was issued to reload the hooks libraries but for
+some reason the reload failed. Other error messages issued from the
+hooks framework will indicate the nature of the problem.
+
+% DHCP6_LEASE_ADVERT address lease %1 advertised (client duid=%2, iaid=%3)
+This debug message indicates that the server successfully advertised
+an address lease. It is up to the client to choose one server out of the
+advertised servers and continue allocation with that server. This
+is a normal behavior and indicates successful operation.
+
+% DHCP6_PD_LEASE_ADVERT prefix lease %1/%2 advertised (client duid=%3, iaid=%4)
This debug message indicates that the server successfully advertised
-a lease. It is up to the client to choose one server out of the
+a prefix lease. It is up to the client to choose one server out of the
advertised servers and continue allocation with that server. This
is a normal behavior and indicates successful operation.
-% DHCP6_LEASE_ADVERT_FAIL failed to advertise a lease for client duid=%1, iaid=%2
-This message indicates that the server failed to advertise (in response to
-received SOLICIT) a lease for a given client. There may be many reasons for
-such failure. Each specific failure is logged in a separate log entry.
+% DHCP6_LEASE_ADVERT_FAIL failed to advertise an address lease for client duid=%1, iaid=%2
+This message indicates that in response to a received SOLICIT, the server
+failed to advertise a non-temporary lease for a given client. There may
+be many reasons for such failure. Each failure is logged in a separate
+log entry.
+
+% DHCP6_PD_LEASE_ADVERT_FAIL failed to advertise a prefix lease for client duid=%1, iaid=%2
+This message indicates that in response to a received SOLICIT, the
+server failed to advertise a prefix lease for the client. There may
+be many reasons for such failure. Each failure is logged in a separate
+log entry.
+
+% DHCP6_LEASE_ALLOC address lease %1 has been allocated (client duid=%2, iaid=%3)
+This debug message indicates that in response to a client's REQUEST
+message, the server successfully granted an non-temporary address
+lease. This is a normal behavior and indicates successful operation.
+
+% DHCP6_PD_LEASE_ALLOC prefix lease %1/%2 has been allocated (client duid=%3, iaid=%4)
+This debug message indicates that in response to a client's REQUEST
+message, the server successfully granted a prefix delegation lease. This
+is a normal behavior and indicates successful operation.
-% DHCP6_LEASE_ALLOC lease %1 has been allocated (client duid=%2, iaid=%3)
-This debug message indicates that the server successfully granted (in
-response to client's REQUEST message) a lease. This is a normal behavior
-and indicates successful operation.
+% DHCP6_LEASE_ALLOC_FAIL failed to grant an address lease for client duid=%1, iaid=%2
+This message indicates that in response to a received REQUEST, the server
+failed to grant a non-temporary address lease for the client. There may
+be many reasons for such failure. Each failure is logged in a separate
+log entry.
-% DHCP6_LEASE_ALLOC_FAIL failed to grant a lease for client duid=%1, iaid=%2
+% DHCP6_PD_LEASE_ALLOC_FAIL failed to grant a prefix lease for client duid=%1, iaid=%2
This message indicates that the server failed to grant (in response to
-received REQUEST) a lease for a given client. There may be many reasons for
-such failure. Each specific failure is logged in a separate log entry.
+received REQUEST) a prefix lease for a given client. There may be many reasons
+for such failure. Each failure is logged in a separate log entry.
-% DHCP6_LEASE_WITHOUT_DUID lease for address %1 does not have a DUID
-This error message indicates a database consistency failure. The lease
+% DHCP6_LEASE_NA_WITHOUT_DUID address lease for address %1 does not have a DUID
+This error message indicates a database consistency problem. The lease
database has an entry indicating that the given address is in use,
but the lease does not contain any client identification. This is most
likely due to a software error: please raise a bug report. As a temporary
workaround, manually remove the lease entry from the database.
+% DHCP6_LEASE_PD_WITHOUT_DUID prefix lease for address %1 does not have a DUID
+This error message indicates a database consistency failure. The lease
+database has an entry indicating that the given prefix is in use,
+but the lease does not contain any client identification. This is most
+likely due to a software error: please raise a bug report. As a temporary
+workaround, manually remove the lease entry from the database.
+
% DHCP6_NOT_RUNNING IPv6 DHCP server is not running
A warning message is issued when an attempt is made to shut down the
IPv6 DHCP server but it is not running.
@@ -101,10 +245,19 @@ IPv6 DHCP server but it is not running.
During startup the IPv6 DHCP server failed to detect any network
interfaces and is therefore shutting down.
+% DHCP6_NO_SOCKETS_OPEN no interface configured to listen to DHCP traffic
+This warning message is issued when current server configuration specifies
+no interfaces that server should listen on, or specified interfaces are not
+configured to receive the traffic.
+
% DHCP6_OPEN_SOCKET opening sockets on port %1
A debug message issued during startup, this indicates that the IPv6 DHCP
server is about to open sockets on the specified port.
+% DHCP6_OPEN_SOCKET_FAIL failed to create socket: %1
+A warning message issued when IfaceMgr fails to open and bind a socket. The reason
+for the failure is appended as an argument of the log message.
+
% DHCP6_PACKET_PARSE_FAIL failed to parse incoming packet
The IPv6 DHCP server has received a packet that it is unable to interpret.
@@ -159,10 +312,10 @@ actions and committal of changes failed. The message has been output in
response to a non-BIND 10 exception being raised. Additional messages
may give further information.
-The most likely cause of this is that the specification file for the server
-(which details the allowable contents of the configuration) is not correct for
-this version of BIND 10. This former may be the result of an interrupted
-installation of an update to BIND 10.
+The most likely cause of this is that the specification file for the
+server (which details the allowable contents of the configuration) is
+not correct for this version of BIND 10. This may be the result of an
+interrupted installation of an update to BIND 10.
% DHCP6_PARSER_FAIL failed to create or run parser for configuration element %1: %2
On receipt of message containing details to a change of its configuration,
@@ -172,38 +325,68 @@ parsing actions and committal of changes failed. The reason for the
failure is given in the message.
% DHCP6_PROCESS_IA_NA_REQUEST server is processing IA_NA option (duid=%1, iaid=%2, hint=%3)
-This is a debug message that indicates a processing of received IA_NA
-option. It may optionally contain an address that may be used by the server
-as a hint for possible requested address.
+This is a debug message that indicates the processing of a received
+IA_NA option. It may optionally contain an address that may be used by
+the server as a hint for possible requested address.
+
+% DHCP6_PROCESS_IA_PD_REQUEST server is processing IA_PD option (duid=%1, iaid=%2, hint=%3)
+This is a debug message that indicates a processing of received IA_PD
+option. It may optionally contain an prefix that may be used by the server
+as a hint for possible requested prefix.
% DHCP6_QUERY_DATA received packet length %1, data length %2, data is %3
A debug message listing the data received from the client or relay.
-% DHCP6_RELEASE address %1 belonging to client duid=%2, iaid=%3 was released properly.
+% DHCP6_RELEASE_NA address %1 belonging to client duid=%2, iaid=%3 was released properly
This debug message indicates that an address was released properly. It
is a normal operation during client shutdown.
-% DHCP6_RELEASE_FAIL failed to remove lease for address %1 for duid=%2, iaid=%3
-This error message indicates that the software failed to remove a
+% DHCP6_RELEASE_PD prefix %1 belonging to client duid=%2, iaid=%3 was released properly
+This debug message indicates that a prefix was released properly. It
+is a normal operation during client shutdown.
+
+% DHCP6_RELEASE_NA_FAIL failed to remove address lease for address %1 for duid=%2, iaid=%3
+This error message indicates that the software failed to remove an address
lease from the lease database. It probably due to an error during a
database operation: resolution will most likely require administrator
intervention (e.g. check if DHCP process has sufficient privileges to
update the database). It may also be triggered if a lease was manually
removed from the database during RELEASE message processing.
-% DHCP6_RELEASE_FAIL_WRONG_DUID client (duid=%1) tried to release address %2, but it belongs to client (duid=%3)
-This warning message indicates that client tried to release an address
+% DHCP6_RELEASE_PD_FAIL failed to remove prefix lease for address %1 for duid=%2, iaid=%3
+This error message indicates that the software failed to remove a prefix
+lease from the lease database. It probably due to an error during a
+database operation: resolution will most likely require administrator
+intervention (e.g. check if DHCP process has sufficient privileges to
+update the database). It may also be triggered if a lease was manually
+removed from the database during RELEASE message processing.
+
+% DHCP6_RELEASE_NA_FAIL_WRONG_DUID client (duid=%1) tried to release address %2, but it belongs to another client (duid=%3)
+This warning message indicates that a client tried to release an address
that belongs to a different client. This should not happen in normal
circumstances and may indicate a misconfiguration of the client. However,
since the client releasing the address will stop using it anyway, there
is a good chance that the situation will correct itself.
-% DHCP6_RELEASE_FAIL_WRONG_IAID client (duid=%1) tried to release address %2, but it used wrong IAID (expected %3, but got %4)
+% DHCP6_RELEASE_PD_FAIL_WRONG_DUID client (duid=%1) tried to release prefix %2, but it belongs to another client (duid=%3)
+This warning message indicates that client tried to release a prefix
+that belongs to a different client. This should not happen in normal
+circumstances and may indicate a misconfiguration of the client. However,
+since the client releasing the prefix will stop using it anyway, there
+is a good chance that the situation will correct itself.
+
+% DHCP6_RELEASE_NA_FAIL_WRONG_IAID client (duid=%1) tried to release address %2, but it used wrong IAID (expected %3, but got %4)
This warning message indicates that client tried to release an address
that does belong to it, but the address was expected to be in a different
IA (identity association) container. This probably means that the client's
support for multiple addresses is flawed.
+% DHCP6_RELEASE_PD_FAIL_WRONG_IAID client (duid=%1) tried to release prefix %2, but it used wrong IAID (expected %3, but got %4)
+This warning message indicates that client tried to release a prefix
+that does belong to it, but the address was expected to be in a different
+IA (identity association) container. This probably means that the client's
+support for multiple prefixes is flawed.
+
% DHCP6_RELEASE_MISSING_CLIENTID client (address=%1) sent RELEASE message without mandatory client-id
This warning message indicates that client sent RELEASE message without
mandatory client-id option. This is most likely caused by a buggy client
@@ -275,6 +458,9 @@ This debug message indicates that a shutdown of the IPv6 server has
been requested via a call to the 'shutdown' method of the core Dhcpv6Srv
object.
+% DHCP6_SOCKET_UNICAST server is about to open socket on address %1 on interface %2
+This is a debug message that inform that a unicast socket will be opened.
+
% DHCP6_SRV_CONSTRUCT_ERROR error creating Dhcpv6Srv object, reason: %1
This error message indicates that during startup, the construction of a
core component within the IPv6 DHCP server (the Dhcpv6 server object)
@@ -308,15 +494,43 @@ to a misconfiguration of the server. The packet processing will continue, but
the response will only contain generic configuration parameters and no
addresses or prefixes.
-% DHCP6_UNKNOWN_RELEASE received RELEASE from unknown client (duid=%1, iaid=%2)
-This warning message is printed when client attempts to release a lease,
-but no such lease is known by the server. See DHCP6_UNKNOWN_RENEW for
-possible reasons for such behavior.
+% DHCP6_UNKNOWN_MSG_RECEIVED received unknown message (type %d) on interface %2
+This debug message is printed when server receives a message of unknown type.
+That could either mean missing functionality or invalid or broken relay or client.
+The list of formally defined message types is available here:
+http://www.iana.org/assignments/dhcpv6-parameters.
+
+% DHCP6_UNKNOWN_RELEASE_NA received RELEASE from unknown client (IA_NA, duid=%1, iaid=%2)
+This warning message is printed when client attempts to release an address
+lease, but no such lease is known by the server. See the explanation
+of the status code DHCP6_UNKNOWN_RENEW_NA for possible reasons for
+such behavior.
+
+% DHCP6_UNKNOWN_RELEASE_PD received RELEASE from unknown client (IA_PD, duid=%1, iaid=%2)
+This warning message is printed when client attempts to release an prefix
+lease, but no such lease is known by the server. See the explanation
+of the status code DHCP6_UNKNOWN_RENEW_PD for possible reasons for
+such behavior.
+
+% DHCP6_UNKNOWN_RENEW_NA received unknown IA_NA RENEW from client (duid=%1, iaid=%2) in subnet %3
+This warning message is printed when client attempts to renew an address
+lease (in the IA_NA option) but no such lease is known by the server. It
+typically means that client has attempted to use its lease past its
+lifetime: causes of this include a adjustment of the client's date/time
+setting or poor support on the client for sleep/recovery. A properly
+implemented client will recover from such a situation by restarting the
+lease allocation process after receiving a negative reply from the server.
+
+An alternative cause could be that the server has lost its database
+recently and does not recognize its well-behaving clients. This is more
+probable if you see many such messages. Clients will recover from this,
+but they will most likely get a different IP addresses and experience
+a brief service interruption.
-% DHCP6_UNKNOWN_RENEW received RENEW from client (duid=%1, iaid=%2) in subnet %3
-This warning message is printed when client attempts to renew a lease,
-but no such lease is known by the server. It typically means that
-client has attempted to use its lease past its lifetime: causes of this
+% DHCP6_UNKNOWN_RENEW_PD received unknown IA_NA RENEW from client (duid=%1, iaid=%2) in subnet %3
+This warning message is printed when client attempts to renew an address lease
+(in IA_NA option), but no such lease is known by the server. It typically means
+that client has attempted to use its lease past its lifetime: causes of this
include a adjustment of the clients date/time setting or poor support
for sleep/recovery. A properly implemented client will recover from such
a situation by restarting the lease allocation process after receiving
diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc
index 75a5337..3dffa37 100644
--- a/src/bin/dhcp6/dhcp6_srv.cc
+++ b/src/bin/dhcp6/dhcp6_srv.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -15,29 +15,37 @@
#include <config.h>
#include <asiolink/io_address.h>
+#include <dhcp_ddns/ncr_msg.h>
#include <dhcp/dhcp6.h>
+#include <dhcp/docsis3_option_defs.h>
#include <dhcp/duid.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option6_addrlst.h>
+#include <dhcp/option6_client_fqdn.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
-#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
#include <dhcp/option_custom.h>
+#include <dhcp/option_vendor.h>
#include <dhcp/option_int_array.h>
#include <dhcp/pkt6.h>
#include <dhcp6/dhcp6_log.h>
#include <dhcp6/dhcp6_srv.h>
+#include <dhcpsrv/callout_handle_store.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/utils.h>
#include <exceptions/exceptions.h>
+#include <hooks/callout_handle.h>
+#include <hooks/hooks_manager.h>
+#include <util/encode/hex.h>
#include <util/io_utilities.h>
#include <util/range_utilities.h>
-#include <util/encode/hex.h>
+#include <boost/bind.hpp>
#include <boost/foreach.hpp>
#include <boost/tokenizer.hpp>
#include <boost/algorithm/string/erase.hpp>
@@ -49,13 +57,75 @@
using namespace isc;
using namespace isc::asiolink;
+using namespace isc::dhcp_ddns;
using namespace isc::dhcp;
+using namespace isc::hooks;
using namespace isc::util;
using namespace std;
+namespace {
+
+/// Structure that holds registered hook indexes
+struct Dhcp6Hooks {
+ int hook_index_buffer6_receive_;///< index for "buffer6_receive" hook point
+ int hook_index_pkt6_receive_; ///< index for "pkt6_receive" hook point
+ int hook_index_subnet6_select_; ///< index for "subnet6_select" hook point
+ int hook_index_lease6_renew_; ///< index for "lease6_renew" hook point
+ int hook_index_lease6_release_; ///< index for "lease6_release" hook point
+ int hook_index_pkt6_send_; ///< index for "pkt6_send" hook point
+ int hook_index_buffer6_send_; ///< index for "buffer6_send" hook point
+
+ /// Constructor that registers hook points for DHCPv6 engine
+ Dhcp6Hooks() {
+ hook_index_buffer6_receive_= HooksManager::registerHook("buffer6_receive");
+ hook_index_pkt6_receive_ = HooksManager::registerHook("pkt6_receive");
+ hook_index_subnet6_select_ = HooksManager::registerHook("subnet6_select");
+ hook_index_lease6_renew_ = HooksManager::registerHook("lease6_renew");
+ hook_index_lease6_release_ = HooksManager::registerHook("lease6_release");
+ hook_index_pkt6_send_ = HooksManager::registerHook("pkt6_send");
+ hook_index_buffer6_send_ = HooksManager::registerHook("buffer6_send");
+ }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+Dhcp6Hooks Hooks;
+
+}; // anonymous namespace
+
namespace isc {
namespace dhcp {
+namespace {
+
+// The following constants describe server's behavior with respect to the
+// DHCPv6 Client FQDN Option sent by a client. They will be removed
+// when DDNS parameters for DHCPv6 are implemented with the ticket #3034.
+
+// Should server always include the FQDN option in its response, regardless
+// if it has been requested in ORO (Disabled).
+const bool FQDN_ALWAYS_INCLUDE = false;
+// Enable AAAA RR update delegation to the client (Disabled).
+const bool FQDN_ALLOW_CLIENT_UPDATE = false;
+// Globally enable updates (Enabled).
+const bool FQDN_ENABLE_UPDATE = true;
+// The partial name generated for the client if empty name has been
+// supplied.
+const char* FQDN_GENERATED_PARTIAL_NAME = "myhost";
+// Do update, even if client requested no updates with N flag (Disabled).
+const bool FQDN_OVERRIDE_NO_UPDATE = false;
+// Server performs an update when client requested delegation (Enabled).
+const bool FQDN_OVERRIDE_CLIENT_UPDATE = true;
+// The fully qualified domain-name suffix if partial name provided by
+// a client.
+const char* FQDN_PARTIAL_SUFFIX = "example.com";
+// Should server replace the domain-name supplied by the client (Disabled).
+const bool FQDN_REPLACE_CLIENT_NAME = false;
+
+}
+
/// @brief file name of a server-id file
///
/// Server must store its duid in persistent storage that must not change
@@ -67,7 +137,8 @@ namespace dhcp {
static const char* SERVER_DUID_FILE = "b10-dhcp6-serverid";
Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
- : alloc_engine_(), serverid_(), shutdown_(true) {
+:alloc_engine_(), serverid_(), shutdown_(true), port_(port)
+{
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_START, DHCP6_OPEN_SOCKET).arg(port);
@@ -82,7 +153,12 @@ Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
LOG_ERROR(dhcp6_logger, DHCP6_NO_INTERFACES);
return;
}
- IfaceMgr::instance().openSockets6(port);
+ // Create error handler. This handler will be called every time
+ // the socket opening operation fails. We use this handler to
+ // log a warning.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&Dhcpv6Srv::ifaceMgrSocket6ErrorHandler, _1);
+ IfaceMgr::instance().openSockets6(port_, error_handler);
}
string duid_file = CfgMgr::instance().getDataDir() + "/" + string(SERVER_DUID_FILE);
@@ -100,12 +176,13 @@ Dhcpv6Srv::Dhcpv6Srv(uint16_t port)
LOG_WARN(dhcp6_logger, DHCP6_SERVERID_WRITE_FAIL)
.arg(duid_file);
}
-
}
// Instantiate allocation engine
alloc_engine_.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100));
+ /// @todo call loadLibraries() when handling configuration changes
+
} catch (const std::exception &e) {
LOG_ERROR(dhcp6_logger, DHCP6_SRV_CONSTRUCT_ERROR).arg(e.what());
return;
@@ -126,123 +203,291 @@ void Dhcpv6Srv::shutdown() {
shutdown_ = true;
}
+Pkt6Ptr Dhcpv6Srv::receivePacket(int timeout) {
+ return (IfaceMgr::instance().receive6(timeout));
+}
+
+void Dhcpv6Srv::sendPacket(const Pkt6Ptr& packet) {
+ IfaceMgr::instance().send(packet);
+}
+
bool Dhcpv6Srv::run() {
while (!shutdown_) {
- /// @todo: calculate actual timeout to the next event (e.g. lease
+ /// @todo Calculate actual timeout to the next event (e.g. lease
/// expiration) once we have lease database. The idea here is that
/// it is possible to do everything in a single process/thread.
/// For now, we are just calling select for 1000 seconds. There
/// were some issues reported on some systems when calling select()
/// with too large values. Unfortunately, I don't recall the details.
- int timeout = 1000;
+ //cppcheck-suppress variableScope This is temporary anyway
+ const int timeout = 1000;
// client's message and server's response
Pkt6Ptr query;
Pkt6Ptr rsp;
try {
- query = IfaceMgr::instance().receive6(timeout);
+ query = receivePacket(timeout);
} catch (const std::exception& e) {
LOG_ERROR(dhcp6_logger, DHCP6_PACKET_RECEIVE_FAIL).arg(e.what());
}
- if (query) {
+ // Timeout may be reached or signal received, which breaks select() with no packet received
+ if (!query) {
+ continue;
+ }
+
+ // In order to parse the DHCP options, the server needs to use some
+ // configuration information such as: existing option spaces, option
+ // definitions etc. This is the kind of information which is not
+ // available in the libdhcp, so we need to supply our own implementation
+ // of the option parsing function here, which would rely on the
+ // configuration data.
+ query->setCallback(boost::bind(&Dhcpv6Srv::unpackOptions, this, _1, _2,
+ _3, _4, _5));
+
+ bool skip_unpack = false;
+
+ // The packet has just been received so contains the uninterpreted wire
+ // data; execute callouts registered for buffer6_receive.
+ if (HooksManager::calloutsPresent(Hooks.hook_index_buffer6_receive_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("query6", query);
+
+ // Call callouts
+ HooksManager::callCallouts(Hooks.hook_index_buffer6_receive_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to parse the packet, so skip at this
+ // stage means that callouts did the parsing already, so server
+ // should skip parsing.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_BUFFER_RCVD_SKIP);
+ skip_unpack = true;
+ }
+
+ callout_handle->getArgument("query6", query);
+ }
+
+ // Unpack the packet information unless the buffer6_receive callouts
+ // indicated they did it
+ if (!skip_unpack) {
if (!query->unpack()) {
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
DHCP6_PACKET_PARSE_FAIL);
continue;
}
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PACKET_RECEIVED)
- .arg(query->getName());
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_QUERY_DATA)
- .arg(static_cast<int>(query->getType()))
- .arg(query->getBuffer().getLength())
- .arg(query->toText());
+ }
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PACKET_RECEIVED)
+ .arg(query->getName());
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA, DHCP6_QUERY_DATA)
+ .arg(static_cast<int>(query->getType()))
+ .arg(query->getBuffer().getLength())
+ .arg(query->toText());
+
+ // At this point the information in the packet has been unpacked into
+ // the various packet fields and option objects has been cretated.
+ // Execute callouts registered for packet6_receive.
+ if (HooksManager::calloutsPresent(Hooks.hook_index_pkt6_receive_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("query6", query);
+
+ // Call callouts
+ HooksManager::callCallouts(Hooks.hook_index_pkt6_receive_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to process the packet, so skip at this
+ // stage means drop.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_RCVD_SKIP);
+ continue;
+ }
- try {
- switch (query->getType()) {
- case DHCPV6_SOLICIT:
- rsp = processSolicit(query);
- break;
+ callout_handle->getArgument("query6", query);
+ }
- case DHCPV6_REQUEST:
- rsp = processRequest(query);
- break;
+ // Assign this packet to a class, if possible
+ classifyPacket(query);
- case DHCPV6_RENEW:
- rsp = processRenew(query);
- break;
-
- case DHCPV6_REBIND:
- rsp = processRebind(query);
+ try {
+ NameChangeRequestPtr ncr;
+ switch (query->getType()) {
+ case DHCPV6_SOLICIT:
+ rsp = processSolicit(query);
break;
- case DHCPV6_CONFIRM:
- rsp = processConfirm(query);
- break;
+ case DHCPV6_REQUEST:
+ rsp = processRequest(query);
+ break;
+
+ case DHCPV6_RENEW:
+ rsp = processRenew(query);
+ break;
+
+ case DHCPV6_REBIND:
+ rsp = processRebind(query);
+ break;
+
+ case DHCPV6_CONFIRM:
+ rsp = processConfirm(query);
+ break;
+
+ case DHCPV6_RELEASE:
+ rsp = processRelease(query);
+ break;
+
+ case DHCPV6_DECLINE:
+ rsp = processDecline(query);
+ break;
+
+ case DHCPV6_INFORMATION_REQUEST:
+ rsp = processInfRequest(query);
+ break;
+
+ default:
+ // We received a packet type that we do not recognize.
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_UNKNOWN_MSG_RECEIVED)
+ .arg(static_cast<int>(query->getType()))
+ .arg(query->getIface());
+ // Only action is to output a message if debug is enabled,
+ // and that will be covered by the debug statement before
+ // the "switch" statement.
+ ;
+ }
- case DHCPV6_RELEASE:
- rsp = processRelease(query);
- break;
+ } catch (const RFCViolation& e) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
+ .arg(query->getName())
+ .arg(query->getRemoteAddr().toText())
+ .arg(e.what());
+
+ } catch (const isc::Exception& e) {
+
+ // Catch-all exception (at least for ones based on the isc
+ // Exception class, which covers more or less all that
+ // are explicitly raised in the BIND 10 code). Just log
+ // the problem and ignore the packet. (The problem is logged
+ // as a debug message because debug is disabled by default -
+ // it prevents a DDOS attack based on the sending of problem
+ // packets.)
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_PACKET_PROCESS_FAIL)
+ .arg(query->getName())
+ .arg(query->getRemoteAddr().toText())
+ .arg(e.what());
+ }
- case DHCPV6_DECLINE:
- rsp = processDecline(query);
- break;
+ if (rsp) {
+ rsp->setRemoteAddr(query->getRemoteAddr());
+ rsp->setLocalAddr(query->getLocalAddr());
- case DHCPV6_INFORMATION_REQUEST:
- rsp = processInfRequest(query);
- break;
+ if (rsp->relay_info_.empty()) {
+ // Direct traffic, send back to the client directly
+ rsp->setRemotePort(DHCP6_CLIENT_PORT);
+ } else {
+ // Relayed traffic, send back to the relay agent
+ rsp->setRemotePort(DHCP6_SERVER_PORT);
+ }
- default:
- // Only action is to output a message if debug is enabled,
- // and that will be covered by the debug statement before
- // the "switch" statement.
- ;
+ rsp->setLocalPort(DHCP6_SERVER_PORT);
+ rsp->setIndex(query->getIndex());
+ rsp->setIface(query->getIface());
+
+ // Specifies if server should do the packing
+ bool skip_pack = false;
+
+ // Server's reply packet now has all options and fields set.
+ // Options are represented by individual objects, but the
+ // output wire data has not been prepared yet.
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_pkt6_send_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Set our response
+ callout_handle->setArgument("response6", rsp);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_pkt6_send_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to pack the packet (create wire data).
+ // That step will be skipped if any callout sets skip flag.
+ // It essentially means that the callout already did packing,
+ // so the server does not have to do it again.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_PACKET_SEND_SKIP);
+ skip_pack = true;
}
+ }
- } catch (const RFCViolation& e) {
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_REQUIRED_OPTIONS_CHECK_FAIL)
- .arg(query->getName())
- .arg(query->getRemoteAddr())
- .arg(e.what());
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA,
+ DHCP6_RESPONSE_DATA)
+ .arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
+
+ if (!skip_pack) {
+ try {
+ rsp->pack();
+ } catch (const std::exception& e) {
+ LOG_ERROR(dhcp6_logger, DHCP6_PACK_FAIL)
+ .arg(e.what());
+ continue;
+ }
- } catch (const isc::Exception& e) {
-
- // Catch-all exception (at least for ones based on the isc
- // Exception class, which covers more or less all that
- // are explicitly raised in the BIND 10 code). Just log
- // the problem and ignore the packet. (The problem is logged
- // as a debug message because debug is disabled by default -
- // it prevents a DDOS attack based on the sending of problem
- // packets.)
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_PACKET_PROCESS_FAIL)
- .arg(query->getName())
- .arg(query->getRemoteAddr())
- .arg(e.what());
}
- if (rsp) {
- rsp->setRemoteAddr(query->getRemoteAddr());
- rsp->setLocalAddr(query->getLocalAddr());
- rsp->setRemotePort(DHCP6_CLIENT_PORT);
- rsp->setLocalPort(DHCP6_SERVER_PORT);
- rsp->setIndex(query->getIndex());
- rsp->setIface(query->getIface());
+ try {
+
+ // Now all fields and options are constructed into output wire buffer.
+ // Option objects modification does not make sense anymore. Hooks
+ // can only manipulate wire buffer at this stage.
+ // Let's execute all callouts registered for buffer6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_buffer6_send_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete previously set arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass incoming packet as argument
+ callout_handle->setArgument("response6", rsp);
+
+ // Call callouts
+ HooksManager::callCallouts(Hooks.hook_index_buffer6_send_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to parse the packet, so skip at this
+ // stage means drop.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_BUFFER_SEND_SKIP);
+ continue;
+ }
+
+ callout_handle->getArgument("response6", rsp);
+ }
LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL_DATA,
DHCP6_RESPONSE_DATA)
.arg(static_cast<int>(rsp->getType())).arg(rsp->toText());
- if (rsp->pack()) {
- try {
- IfaceMgr::instance().send(rsp);
- } catch (const std::exception& e) {
- LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL).arg(e.what());
- }
- } else {
- LOG_ERROR(dhcp6_logger, DHCP6_PACK_FAIL);
- }
+ sendPacket(rsp);
+ } catch (const std::exception& e) {
+ LOG_ERROR(dhcp6_logger, DHCP6_PACKET_SEND_FAIL)
+ .arg(e.what());
}
+
+ // Send NameChangeRequests to the b10-dhcp-ddns module.
+ sendNameChangeRequests();
}
}
@@ -403,8 +648,13 @@ Dhcpv6Srv::copyDefaultOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
if (clientid) {
answer->addOption(clientid);
}
+ /// @todo: Should throw if there is no client-id (except anonymous INF-REQUEST)
+
+ // If this is a relayed message, we need to copy relay information
+ if (!question->relay_info_.empty()) {
+ answer->copyRelayInfo(question);
+ }
- // TODO: Should throw if there is no client-id (except anonymous INF-REQUEST)
}
void
@@ -444,6 +694,57 @@ Dhcpv6Srv::appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
}
}
+void
+Dhcpv6Srv::appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer) {
+ // Get the configured subnet suitable for the incoming packet.
+ Subnet6Ptr subnet = selectSubnet(question);
+ // Leave if there is no subnet matching the incoming packet.
+ // There is no need to log the error message here because
+ // it will be logged in the assignLease() when it fails to
+ // pick the suitable subnet. We don't want to duplicate
+ // error messages in such case.
+ if (!subnet) {
+ return;
+ }
+
+ // Try to get the vendor option
+ boost::shared_ptr<OptionVendor> vendor_req =
+ boost::dynamic_pointer_cast<OptionVendor>(question->getOption(D6O_VENDOR_OPTS));
+ if (!vendor_req) {
+ return;
+ }
+
+ // Let's try to get ORO within that vendor-option
+ /// @todo This is very specific to vendor-id=4491 (Cable Labs). Other vendors
+ /// may have different policies.
+ boost::shared_ptr<OptionUint16Array> oro =
+ boost::dynamic_pointer_cast<OptionUint16Array>(vendor_req->getOption(DOCSIS3_V6_ORO));
+
+ // Option ORO not found. Don't do anything then.
+ if (!oro) {
+ return;
+ }
+
+ uint32_t vendor_id = vendor_req->getVendorId();
+
+ boost::shared_ptr<OptionVendor> vendor_rsp(new OptionVendor(Option::V6, vendor_id));
+
+ // Get the list of options that client requested.
+ bool added = false;
+ const std::vector<uint16_t>& requested_opts = oro->getValues();
+ BOOST_FOREACH(uint16_t opt, requested_opts) {
+ Subnet::OptionDescriptor desc = subnet->getVendorOptionDescriptor(vendor_id, opt);
+ if (desc.option) {
+ vendor_rsp->addOption(desc.option);
+ added = true;
+ }
+ }
+
+ if (added) {
+ answer->addOption(vendor_rsp);
+ }
+}
+
OptionPtr
Dhcpv6Srv::createStatusCode(uint16_t code, const std::string& text) {
// @todo This function uses OptionCustom class to manage contents
@@ -458,10 +759,9 @@ Dhcpv6Srv::createStatusCode(uint16_t code, const std::string& text) {
assert(status_code_def);
// As there is no dedicated class to represent Status Code
- // the OptionCustom class should be returned here.
- boost::shared_ptr<OptionCustom> option_status =
- boost::dynamic_pointer_cast<
- OptionCustom>(status_code_def->optionFactory(Option::V6, D6O_STATUS_CODE));
+ // the OptionCustom class is used here instead.
+ OptionCustomPtr option_status =
+ OptionCustomPtr(new OptionCustom(*status_code_def, Option::V6));
assert(option_status);
// Set status code to 'code' (0 - means data field #0).
@@ -474,7 +774,7 @@ Dhcpv6Srv::createStatusCode(uint16_t code, const std::string& text) {
void
Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
RequirementLevel serverid) {
- Option::OptionCollection client_ids = pkt->getOptions(D6O_CLIENTID);
+ OptionCollection client_ids = pkt->getOptions(D6O_CLIENTID);
switch (clientid) {
case MANDATORY:
if (client_ids.size() != 1) {
@@ -495,7 +795,7 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
break;
}
- Option::OptionCollection server_ids = pkt->getOptions(D6O_SERVERID);
+ OptionCollection server_ids = pkt->getOptions(D6O_SERVERID);
switch (serverid) {
case FORBIDDEN:
if (!server_ids.empty()) {
@@ -523,27 +823,79 @@ Dhcpv6Srv::sanityCheck(const Pkt6Ptr& pkt, RequirementLevel clientid,
Subnet6Ptr
Dhcpv6Srv::selectSubnet(const Pkt6Ptr& question) {
- /// @todo: pass interface information only if received direct (non-relayed) message
+ Subnet6Ptr subnet;
+
+ if (question->relay_info_.empty()) {
+ // This is a direct (non-relayed) message
- // Try to find a subnet if received packet from a directly connected client
- Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(question->getIface());
- if (subnet) {
- return (subnet);
+ // Try to find a subnet if received packet from a directly connected client
+ subnet = CfgMgr::instance().getSubnet6(question->getIface());
+ if (!subnet) {
+ // If no subnet was found, try to find it based on remote address
+ subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
+ }
+ } else {
+
+ // This is a relayed message
+ OptionPtr interface_id = question->getAnyRelayOption(D6O_INTERFACE_ID,
+ Pkt6::RELAY_GET_FIRST);
+ if (interface_id) {
+ subnet = CfgMgr::instance().getSubnet6(interface_id);
+ }
+
+ if (!subnet) {
+ // If no interface-id was specified (or not configured on server), let's
+ // try address matching
+ IOAddress link_addr = question->relay_info_.back().linkaddr_;
+
+ // if relay filled in link_addr field, then let's use it
+ if (link_addr != IOAddress("::")) {
+ subnet = CfgMgr::instance().getSubnet6(link_addr);
+ }
+ }
}
- // If no subnet was found, try to find it based on remote address
- subnet = CfgMgr::instance().getSubnet6(question->getRemoteAddr());
+ // Let's execute all callouts registered for subnet6_receive
+ if (HooksManager::calloutsPresent(Hooks.hook_index_subnet6_select_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(question);
+
+ // We're reusing callout_handle from previous calls
+ callout_handle->deleteAllArguments();
+
+ // Set new arguments
+ callout_handle->setArgument("query6", question);
+ callout_handle->setArgument("subnet6", subnet);
+
+ // We pass pointer to const collection for performance reasons.
+ // Otherwise we would get a non-trivial performance penalty each
+ // time subnet6_select is called.
+ callout_handle->setArgument("subnet6collection", CfgMgr::instance().getSubnets6());
+
+ // Call user (and server-side) callouts
+ HooksManager::callCallouts(Hooks.hook_index_subnet6_select_, *callout_handle);
+
+ // Callouts decided to skip this step. This means that no subnet will be
+ // selected. Packet processing will continue, but it will be severly limited
+ // (i.e. only global options will be assigned)
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_SUBNET6_SELECT_SKIP);
+ return (Subnet6Ptr());
+ }
+
+ // Use whatever subnet was specified by the callout
+ callout_handle->getArgument("subnet6", subnet);
+ }
return (subnet);
}
void
-Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
+Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& fqdn) {
// We need to allocate addresses for all IA_NA options in the client's
// question (i.e. SOLICIT or REQUEST) message.
// @todo add support for IA_TA
- // @todo add support for IA_PD
// We need to select a subnet the client is connected in.
Subnet6Ptr subnet = selectSubnet(question);
@@ -588,26 +940,301 @@ Dhcpv6Srv::assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer) {
//
// @todo: expand this to cover IA_PD and IA_TA once we implement support for
// prefix delegation and temporary addresses.
- for (Option::OptionCollection::iterator opt = question->options_.begin();
+ for (OptionCollection::iterator opt = question->options_.begin();
opt != question->options_.end(); ++opt) {
switch (opt->second->getType()) {
case D6O_IA_NA: {
OptionPtr answer_opt = assignIA_NA(subnet, duid, question,
- boost::dynamic_pointer_cast<Option6IA>(opt->second));
+ boost::dynamic_pointer_cast<
+ Option6IA>(opt->second),
+ fqdn);
if (answer_opt) {
answer->addOption(answer_opt);
}
break;
}
+ case D6O_IA_PD: {
+ OptionPtr answer_opt = assignIA_PD(subnet, duid, question,
+ boost::dynamic_pointer_cast<
+ Option6IA>(opt->second));
+ if (answer_opt) {
+ answer->addOption(answer_opt);
+ }
+ }
default:
break;
}
}
}
+Option6ClientFqdnPtr
+Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question) {
+ // Get Client FQDN Option from the client's message. If this option hasn't
+ // been included, do nothing.
+ Option6ClientFqdnPtr fqdn = boost::dynamic_pointer_cast<
+ Option6ClientFqdn>(question->getOption(D6O_CLIENT_FQDN));
+ if (!fqdn) {
+ return (fqdn);
+ }
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_RECEIVE_FQDN).arg(fqdn->toText());
+
+
+ // Prepare the FQDN option which will be included in the response to
+ // the client.
+ Option6ClientFqdnPtr fqdn_resp(new Option6ClientFqdn(*fqdn));
+ // RFC 4704, section 6. - all flags set to 0.
+ fqdn_resp->resetFlags();
+
+ // Conditions when N flag has to be set to indicate that server will not
+ // perform DNS updates:
+ // 1. Updates are globally disabled,
+ // 2. Client requested no update and server respects it,
+ // 3. Client requested that the AAAA update is delegated to the client but
+ // server neither respects delegation of updates nor it is configured
+ // to send update on its own when client requested delegation.
+ if (!FQDN_ENABLE_UPDATE ||
+ (fqdn->getFlag(Option6ClientFqdn::FLAG_N) &&
+ !FQDN_OVERRIDE_NO_UPDATE) ||
+ (!fqdn->getFlag(Option6ClientFqdn::FLAG_S) &&
+ !FQDN_ALLOW_CLIENT_UPDATE && !FQDN_OVERRIDE_CLIENT_UPDATE)) {
+ fqdn_resp->setFlag(Option6ClientFqdn::FLAG_N, true);
+
+ // Conditions when S flag is set to indicate that server will perform
+ // DNS update on its own:
+ // 1. Client requested that server performs DNS update and DNS updates are
+ // globally enabled
+ // 2. Client requested that server delegates AAAA update to the client but
+ // server doesn't respect delegation and it is configured to perform
+ // an update on its own when client requested delegation.
+ } else if (fqdn->getFlag(Option6ClientFqdn::FLAG_S) ||
+ (!fqdn->getFlag(Option6ClientFqdn::FLAG_S) &&
+ !FQDN_ALLOW_CLIENT_UPDATE && FQDN_OVERRIDE_CLIENT_UPDATE)) {
+ fqdn_resp->setFlag(Option6ClientFqdn::FLAG_S, true);
+ }
+
+ // Server MUST set the O flag if it has overridden the client's setting
+ // of S flag.
+ if (fqdn->getFlag(Option6ClientFqdn::FLAG_S) !=
+ fqdn_resp->getFlag(Option6ClientFqdn::FLAG_S)) {
+ fqdn_resp->setFlag(Option6ClientFqdn::FLAG_O, true);
+ }
+
+ // If client supplied partial or empty domain-name, server should
+ // generate one.
+ if (fqdn->getDomainNameType() == Option6ClientFqdn::PARTIAL) {
+ std::ostringstream name;
+ if (fqdn->getDomainName().empty()) {
+ name << FQDN_GENERATED_PARTIAL_NAME;
+ } else {
+ name << fqdn->getDomainName();
+ }
+ name << "." << FQDN_PARTIAL_SUFFIX;
+ fqdn_resp->setDomainName(name.str(), Option6ClientFqdn::FULL);
+
+ // Server may be configured to replace a name supplied by a client,
+ // even if client supplied fully qualified domain-name.
+ } else if (FQDN_REPLACE_CLIENT_NAME) {
+ std::ostringstream name;
+ name << FQDN_GENERATED_PARTIAL_NAME << "." << FQDN_PARTIAL_SUFFIX;
+ fqdn_resp->setDomainName(name.str(), Option6ClientFqdn::FULL);
+
+ }
+
+ // Return the FQDN option which can be included in the server's response.
+ // Note that it doesn't have to be included, if client didn't request
+ // it using ORO and server is not configured to always include it.
+ return (fqdn_resp);
+}
+
+
+void
+Dhcpv6Srv::appendClientFqdn(const Pkt6Ptr& question,
+ Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& fqdn) {
+
+ // If FQDN is NULL, it means that client did not request DNS Update, plus
+ // server doesn't force updates.
+ if (!fqdn) {
+ return;
+ }
+
+ // Server sends back the FQDN option to the client if client has requested
+ // it using Option Request Option. However, server may be configured to
+ // send the FQDN option in its response, regardless whether client requested
+ // it or not.
+ bool include_fqdn = FQDN_ALWAYS_INCLUDE;
+ if (!include_fqdn) {
+ OptionUint16ArrayPtr oro = boost::dynamic_pointer_cast<
+ OptionUint16Array>(question->getOption(D6O_ORO));
+ if (oro) {
+ const std::vector<uint16_t>& values = oro->getValues();
+ for (int i = 0; i < values.size(); ++i) {
+ if (values[i] == D6O_CLIENT_FQDN) {
+ include_fqdn = true;
+ }
+ }
+ }
+ }
+
+ if (include_fqdn) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_SEND_FQDN).arg(fqdn->toText());
+ answer->addOption(fqdn);
+ }
+
+}
+
+void
+Dhcpv6Srv::createNameChangeRequests(const Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& opt_fqdn) {
+
+ // It is likely that client haven't included the FQDN option in the message
+ // and server is not configured to always update DNS. In such cases,
+ // FQDN option will be NULL. This is valid state, so we simply return.
+ if (!opt_fqdn) {
+ return;
+ }
+
+ // The response message instance is always required. For instance it
+ // holds the Client Identifier. It is a programming error if supplied
+ // message is NULL.
+ if (!answer) {
+ isc_throw(isc::Unexpected, "an instance of the object"
+ << " encapsulating server's message must not be"
+ << " NULL when creating DNS NameChangeRequest");
+ }
+
+ // Get the Client Id. It is mandatory and a function creating a response
+ // would have thrown an exception if it was missing. Thus throwning
+ // Unexpected if it is missing as it is a programming error.
+ OptionPtr opt_duid = answer->getOption(D6O_CLIENTID);
+ if (!opt_duid) {
+ isc_throw(isc::Unexpected,
+ "client identifier is required when creating a new"
+ " DNS NameChangeRequest");
+ }
+ DuidPtr duid = DuidPtr(new DUID(opt_duid->getData()));
+
+ // Get the FQDN in the on-wire format. It will be needed to compute
+ // DHCID.
+ OutputBuffer name_buf(1);
+ opt_fqdn->packDomainName(name_buf);
+ const uint8_t* name_data = static_cast<const uint8_t*>(name_buf.getData());
+ // @todo currently D2Dhcid accepts a vector holding FQDN.
+ // However, it will be faster if we used a pointer name_data.
+ std::vector<uint8_t> buf_vec(name_data, name_data + name_buf.getLength());
+ // Compute DHCID from Client Identifier and FQDN.
+ isc::dhcp_ddns::D2Dhcid dhcid(*duid, buf_vec);
+
+ // Get all IAs from the answer. For each IA, holding an address we will
+ // create a corresponding NameChangeRequest.
+ OptionCollection answer_ias = answer->getOptions(D6O_IA_NA);
+ for (OptionCollection::const_iterator answer_ia =
+ answer_ias.begin(); answer_ia != answer_ias.end(); ++answer_ia) {
+ // @todo IA_NA may contain multiple addresses. We should process
+ // each address individually. Currently we get only one.
+ Option6IAAddrPtr iaaddr = boost::static_pointer_cast<
+ Option6IAAddr>(answer_ia->second->getOption(D6O_IAADDR));
+ // We need an address to create a name-to-address mapping.
+ // If address is missing for any reason, go to the next IA.
+ if (!iaaddr) {
+ continue;
+ }
+ // Create new NameChangeRequest. Use the domain name from the FQDN.
+ // This is an FQDN included in the response to the client, so it
+ // holds a fully qualified domain-name already (not partial).
+ // Get the IP address from the lease. Also, use the S flag to determine
+ // if forward change should be performed. This flag will always be
+ // set if server has taken responsibility for the forward update.
+ NameChangeRequest ncr(isc::dhcp_ddns::CHG_ADD,
+ opt_fqdn->getFlag(Option6ClientFqdn::FLAG_S),
+ true, opt_fqdn->getDomainName(),
+ iaaddr->getAddress().toText(),
+ dhcid, 0, iaaddr->getValid());
+ // Add the request to the queue. This queue will be read elsewhere in
+ // the code and all requests from this queue will be sent to the
+ // D2 module.
+ name_change_reqs_.push(ncr);
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_CREATE_ADD_NAME_CHANGE_REQUEST).arg(ncr.toText());
+ }
+}
+
+void
+Dhcpv6Srv::createRemovalNameChangeRequest(const Lease6Ptr& lease) {
+ // If we haven't performed a DNS Update when lease was acquired,
+ // there is nothing to do here.
+ if (!lease->fqdn_fwd_ && !lease->fqdn_rev_) {
+ return;
+ }
+
+ // When lease was added into a database the host name should have
+ // been added. The hostname can be empty if someone messed up in the
+ // lease data base and removed the hostname.
+ if (lease->hostname_.empty()) {
+ LOG_ERROR(dhcp6_logger, DHCP6_DDNS_REMOVE_EMPTY_HOSTNAME)
+ .arg(lease->addr_.toText());
+ return;
+ }
+
+ // If hostname is non-empty, try to convert it to wire format so as
+ // DHCID can be computed from it. This may throw an exception if hostname
+ // has invalid format. Again, this should be only possible in case of
+ // manual intervention in the database. Note that the last parameter
+ // passed to the writeFqdn function forces conversion of the FQDN
+ // to lower case. This is required by the RFC4701, section 3.5.
+ // The DHCID computation is further in this function.
+ std::vector<uint8_t> hostname_wire;
+ try {
+ OptionDataTypeUtil::writeFqdn(lease->hostname_, hostname_wire, true);
+ } catch (const Exception& ex) {
+ LOG_ERROR(dhcp6_logger, DHCP6_DDNS_REMOVE_INVALID_HOSTNAME)
+ .arg(lease->hostname_);
+ return;
+ }
+
+ // DUID must have been checked already by the caller of this function.
+ // Let's be on the safe side and make sure it is non-NULL and throw
+ // an exception if it is NULL.
+ if (!lease->duid_) {
+ isc_throw(isc::Unexpected, "DUID must be set when creating"
+ << " NameChangeRequest for DNS records removal for "
+ << lease->addr_);
+
+ }
+ isc::dhcp_ddns::D2Dhcid dhcid(*lease->duid_, hostname_wire);
+
+ // Create a NameChangeRequest to remove the entry.
+ NameChangeRequest ncr(isc::dhcp_ddns::CHG_REMOVE,
+ lease->fqdn_fwd_, lease->fqdn_rev_,
+ lease->hostname_,
+ lease->addr_.toText(),
+ dhcid, 0, lease->valid_lft_);
+ name_change_reqs_.push(ncr);
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_CREATE_REMOVE_NAME_CHANGE_REQUEST).arg(ncr.toText());
+
+}
+
+void
+Dhcpv6Srv::sendNameChangeRequests() {
+ while (!name_change_reqs_.empty()) {
+ // @todo Once next NameChangeRequest is picked from the queue
+ // we should send it to the b10-dhcp_ddns module. Currently we
+ // just drop it.
+ name_change_reqs_.pop();
+ }
+}
+
+
OptionPtr
Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
- Pkt6Ptr question, boost::shared_ptr<Option6IA> ia) {
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia,
+ const Option6ClientFqdnPtr& fqdn) {
// If there is no subnet selected for handling this IA_NA, the only thing to do left is
// to say that we are sorry, but the user won't get an address. As a convenience, we
// use a different status text to indicate that (compare to the same status code,
@@ -646,17 +1273,54 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
// it should include this hint. That will help us during the actual lease
// allocation.
bool fake_allocation = false;
- if (question->getType() == DHCPV6_SOLICIT) {
+ if (query->getType() == DHCPV6_SOLICIT) {
/// @todo: Check if we support rapid commit
fake_allocation = true;
}
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // At this point, we have to make make some decisions with respect to the
+ // FQDN option that we have generated as a result of receiving client's
+ // FQDN. In particular, we have to get to know if the DNS update will be
+ // performed or not. It is possible that option is NULL, which is valid
+ // condition if client didn't request DNS updates and server didn't force
+ // the update.
+ bool do_fwd = false;
+ bool do_rev = false;
+ if (fqdn) {
+ // Flag S must not coexist with flag N being set to 1, so if S=1
+ // server takes responsibility for both reverse and forward updates.
+ // Otherwise, we have to check N.
+ if (fqdn->getFlag(Option6ClientFqdn::FLAG_S)) {
+ do_fwd = true;
+ do_rev = true;
+ } else if (!fqdn->getFlag(Option6ClientFqdn::FLAG_N)) {
+ do_rev = true;
+ }
+ }
+ // Set hostname only in case any of the updates is being performed.
+ std::string hostname;
+ if (do_fwd || do_rev) {
+ hostname = fqdn->getDomainName();
+ }
+
// Use allocation engine to pick a lease for this client. Allocation engine
// will try to honour the hint, but it is just a hint - some other address
// may be used instead. If fake_allocation is set to false, the lease will
// be inserted into the LeaseMgr as well.
- Lease6Ptr lease = alloc_engine_->allocateAddress6(subnet, duid, ia->getIAID(),
- hint, fake_allocation);
+ Lease6Collection leases = alloc_engine_->allocateLeases6(subnet, duid,
+ ia->getIAID(),
+ hint, Lease::TYPE_NA,
+ do_fwd, do_rev,
+ hostname,
+ fake_allocation,
+ callout_handle);
+ /// @todo: Handle more than one lease
+ Lease6Ptr lease;
+ if (!leases.empty()) {
+ lease = *leases.begin();
+ }
// Create IA_NA that we will put in the response.
// Do not use OptionDefinition to create option's instance so
@@ -685,6 +1349,29 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
// It would be possible to insert status code=0(success) as well,
// but this is considered waste of bandwidth as absence of status
// code is considered a success.
+
+ // Allocation engine may have returned an existing lease. If so, we
+ // have to check that the FQDN settings we provided are the same
+ // that were set. If they aren't, we will have to remove existing
+ // DNS records and update the lease with the new settings.
+ if ((lease->hostname_ != hostname) || (lease->fqdn_fwd_ != do_fwd) ||
+ (lease->fqdn_rev_ != do_rev)) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_LEASE_ASSIGN_FQDN_CHANGE)
+ .arg(lease->toText())
+ .arg(hostname)
+ .arg(do_rev ? "true" : "false")
+ .arg(do_fwd ? "true" : "false");
+
+ // Schedule removal of the existing lease.
+ createRemovalNameChangeRequest(lease);
+ // Set the new lease properties and update.
+ lease->hostname_ = hostname;
+ lease->fqdn_fwd_ = do_fwd;
+ lease->fqdn_rev_ = do_rev;
+ LeaseMgrFactory::instance().updateLease6(lease);
+ }
+
} else {
// Allocation engine did not allocate a lease. The engine logged
// cause of that failure. The only thing left is to insert
@@ -702,8 +1389,110 @@ Dhcpv6Srv::assignIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
}
OptionPtr
+Dhcpv6Srv::assignIA_PD(const Subnet6Ptr& subnet, const DuidPtr& duid,
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia) {
+
+ // Create IA_PD that we will put in the response.
+ // Do not use OptionDefinition to create option's instance so
+ // as we can initialize IAID using a constructor.
+ boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_PD, ia->getIAID()));
+
+ // If there is no subnet selected for handling this IA_PD, the only thing to
+ // do left is to say that we are sorry, but the user won't get an address.
+ // As a convenience, we use a different status text to indicate that
+ // (compare to the same status code, but different wording below)
+ if (!subnet) {
+
+ // Insert status code NoAddrsAvail.
+ ia_rsp->addOption(createStatusCode(STATUS_NoPrefixAvail,
+ "Sorry, no subnet available."));
+ return (ia_rsp);
+ }
+
+ // Check if the client sent us a hint in his IA_PD. Clients may send an
+ // address in their IA_NA options as a suggestion (e.g. the last address
+ // they used before).
+ boost::shared_ptr<Option6IAPrefix> hintOpt =
+ boost::dynamic_pointer_cast<Option6IAPrefix>(ia->getOption(D6O_IAPREFIX));
+ IOAddress hint("::");
+ if (hintOpt) {
+ hint = hintOpt->getAddress();
+ }
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PROCESS_IA_PD_REQUEST)
+ .arg(duid ? duid->toText() : "(no-duid)").arg(ia->getIAID())
+ .arg(hintOpt ? hint.toText() : "(no hint)");
+
+ // "Fake" allocation is processing of SOLICIT message. We pretend to do an
+ // allocation, but we do not put the lease in the database. That is ok,
+ // because we do not guarantee that the user will get that exact lease. If
+ // the user selects this server to do actual allocation (i.e. sends REQUEST)
+ // it should include this hint. That will help us during the actual lease
+ // allocation.
+ bool fake_allocation = (query->getType() == DHCPV6_SOLICIT);
+
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Use allocation engine to pick a lease for this client. Allocation engine
+ // will try to honour the hint, but it is just a hint - some other address
+ // may be used instead. If fake_allocation is set to false, the lease will
+ // be inserted into the LeaseMgr as well.
+ Lease6Collection leases = alloc_engine_->allocateLeases6(subnet, duid,
+ ia->getIAID(),
+ hint, Lease::TYPE_PD,
+ false, false,
+ string(),
+ fake_allocation,
+ callout_handle);
+
+ if (!leases.empty()) {
+
+ ia_rsp->setT1(subnet->getT1());
+ ia_rsp->setT2(subnet->getT2());
+
+ for (Lease6Collection::iterator l = leases.begin();
+ l != leases.end(); ++l) {
+
+ // We have a lease! Let's wrap its content into IA_PD option
+ // with IAADDR suboption.
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, fake_allocation ?
+ DHCP6_PD_LEASE_ADVERT : DHCP6_PD_LEASE_ALLOC)
+ .arg((*l)->addr_.toText())
+ .arg(static_cast<int>((*l)->prefixlen_))
+ .arg(duid ? duid->toText() : "(no-duid)")
+ .arg(ia->getIAID());
+
+ boost::shared_ptr<Option6IAPrefix>
+ addr(new Option6IAPrefix(D6O_IAPREFIX, (*l)->addr_,
+ (*l)->prefixlen_, (*l)->preferred_lft_,
+ (*l)->valid_lft_));
+ ia_rsp->addOption(addr);
+ }
+
+ // It would be possible to insert status code=0(success) as well,
+ // but this is considered waste of bandwidth as absence of status
+ // code is considered a success.
+
+ } else {
+ // Allocation engine did not allocate a lease. The engine logged
+ // cause of that failure. The only thing left is to insert
+ // status code to pass the sad news to the client.
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, fake_allocation ?
+ DHCP6_PD_LEASE_ADVERT_FAIL : DHCP6_PD_LEASE_ALLOC_FAIL)
+ .arg(duid ? duid->toText() : "(no-duid)")
+ .arg(ia->getIAID());
+
+ ia_rsp->addOption(createStatusCode(STATUS_NoPrefixAvail,
+ "Sorry, no prefixes could be allocated."));
+ }
+ return (ia_rsp);
+}
+
+OptionPtr
Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
- Pkt6Ptr /* question */, boost::shared_ptr<Option6IA> ia) {
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia,
+ const Option6ClientFqdnPtr& fqdn) {
if (!subnet) {
// There's no subnet select for this client. There's nothing to renew.
boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_NA, ia->getIAID()));
@@ -719,7 +1508,8 @@ Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
return (ia_rsp);
}
- Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(*duid, ia->getIAID(),
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ *duid, ia->getIAID(),
subnet->getID());
if (!lease) {
@@ -732,7 +1522,7 @@ Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
"Sorry, no known leases for this duid/iaid."));
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_UNKNOWN_RENEW)
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_UNKNOWN_RENEW_NA)
.arg(duid->toText())
.arg(ia->getIAID())
.arg(subnet->toText());
@@ -740,13 +1530,53 @@ Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
return (ia_rsp);
}
+ // Keep the old data in case the callout tells us to skip update
+ Lease6 old_data = *lease;
+
+ // At this point, we have to make make some decisions with respect to the
+ // FQDN option that we have generated as a result of receiving client's
+ // FQDN. In particular, we have to get to know if the DNS update will be
+ // performed or not. It is possible that option is NULL, which is valid
+ // condition if client didn't request DNS updates and server didn't force
+ // the update.
+ bool do_fwd = false;
+ bool do_rev = false;
+ if (fqdn) {
+ if (fqdn->getFlag(Option6ClientFqdn::FLAG_S)) {
+ do_fwd = true;
+ do_rev = true;
+ } else if (!fqdn->getFlag(Option6ClientFqdn::FLAG_N)) {
+ do_rev = true;
+ }
+ }
+
+ std::string hostname;
+ if (do_fwd || do_rev) {
+ hostname = fqdn->getDomainName();
+ }
+
+ // If the new FQDN settings have changed for the lease, we need to
+ // delete any existing FQDN records for this lease.
+ if ((lease->hostname_ != hostname) || (lease->fqdn_fwd_ != do_fwd) ||
+ (lease->fqdn_rev_ != do_rev)) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL,
+ DHCP6_DDNS_LEASE_RENEW_FQDN_CHANGE)
+ .arg(lease->toText())
+ .arg(hostname)
+ .arg(do_rev ? "true" : "false")
+ .arg(do_fwd ? "true" : "false");
+
+ createRemovalNameChangeRequest(lease);
+ }
+
lease->preferred_lft_ = subnet->getPreferred();
lease->valid_lft_ = subnet->getValid();
lease->t1_ = subnet->getT1();
lease->t2_ = subnet->getT2();
lease->cltt_ = time(NULL);
-
- LeaseMgrFactory::instance().updateLease6(lease);
+ lease->hostname_ = hostname;
+ lease->fqdn_fwd_ = do_fwd;
+ lease->fqdn_rev_ = do_rev;
// Create empty IA_NA option with IAID matching the request.
boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_NA, ia->getIAID()));
@@ -758,11 +1588,153 @@ Dhcpv6Srv::renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
lease->addr_, lease->preferred_lft_,
lease->valid_lft_));
ia_rsp->addOption(addr);
+
+ bool skip = false;
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_lease6_renew_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass the original packet
+ callout_handle->setArgument("query6", query);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease6", lease);
+
+ // Pass the IA option to be sent in response
+ callout_handle->setArgument("ia_na", ia_rsp);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease6_renew_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to actually renew the lease, so skip at this
+ // stage means "keep the old lease as it is".
+ if (callout_handle->getSkip()) {
+ skip = true;
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_LEASE6_RENEW_SKIP);
+ }
+ }
+
+ if (!skip) {
+ LeaseMgrFactory::instance().updateLease6(lease);
+ } else {
+ // Copy back the original date to the lease. For MySQL it doesn't make
+ // much sense, but for memfile, the Lease6Ptr points to the actual lease
+ // in memfile, so the actual update is performed when we manipulate fields
+ // of returned Lease6Ptr, the actual updateLease6() is no-op.
+ *lease = old_data;
+ }
+
+ return (ia_rsp);
+}
+
+OptionPtr
+Dhcpv6Srv::renewIA_PD(const Subnet6Ptr& subnet, const DuidPtr& duid,
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia) {
+
+ // Let's create a IA_NA response and fill it in later
+ boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_PD, ia->getIAID()));
+
+ if (!subnet) {
+ // Insert status code NoBinding
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "Sorry, no known leases for this duid/iaid."));
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_RENEW_UNKNOWN_SUBNET)
+ .arg(duid->toText())
+ .arg(ia->getIAID());
+
+ return (ia_rsp);
+ }
+
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_PD,
+ *duid, ia->getIAID(),
+ subnet->getID());
+
+ if (!lease) {
+ // Client is renewing a lease that we don't know about.
+
+ // Insert status code NoBinding
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "Sorry, no known leases for this duid/iaid."));
+
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_UNKNOWN_RENEW_PD)
+ .arg(duid->toText())
+ .arg(ia->getIAID())
+ .arg(subnet->toText());
+
+ return (ia_rsp);
+ }
+
+ // Keep the old data in case the callout tells us to skip update.
+ Lease6 old_data = *lease;
+
+ // Do the actual lease update
+ lease->preferred_lft_ = subnet->getPreferred();
+ lease->valid_lft_ = subnet->getValid();
+ lease->t1_ = subnet->getT1();
+ lease->t2_ = subnet->getT2();
+ lease->cltt_ = time(NULL);
+
+ // Also update IA_PD container with proper T1, T2 values
+ ia_rsp->setT1(subnet->getT1());
+ ia_rsp->setT2(subnet->getT2());
+
+ boost::shared_ptr<Option6IAPrefix>
+ prefix(new Option6IAPrefix(D6O_IAPREFIX, lease->addr_,
+ lease->prefixlen_, lease->preferred_lft_,
+ lease->valid_lft_));
+ ia_rsp->addOption(prefix);
+
+ bool skip = false;
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_lease6_renew_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass the original packet
+ callout_handle->setArgument("query6", query);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease6", lease);
+
+ // Pass the IA option to be sent in response
+ callout_handle->setArgument("ia_pd", ia_rsp);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease6_renew_,
+ *callout_handle);
+
+ // Remember hook's instruction whether we want to skip update or not
+ skip = callout_handle->getSkip();
+ }
+
+ if (!skip) {
+ LeaseMgrFactory::instance().updateLease6(lease);
+ } else {
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to actually renew the lease, so skip at this
+ // stage means "keep the old lease as it is".
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_LEASE6_RENEW_SKIP);
+
+ // Copy back the original date to the lease. For MySQL it doesn't make
+ // much sense, but for memfile, the Lease6Ptr points to the actual lease
+ // in memfile, so the actual update is performed when we manipulate
+ // fields of returned Lease6Ptr, the actual updateLease6() is no-op.
+ *lease = old_data;
+ }
+
return (ia_rsp);
}
void
-Dhcpv6Srv::renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply) {
+Dhcpv6Srv::renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply,
+ const Option6ClientFqdnPtr& fqdn) {
// We need to renew addresses for all IA_NA options in the client's
// RENEW message.
@@ -801,17 +1773,31 @@ Dhcpv6Srv::renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply) {
}
DuidPtr duid(new DUID(opt_duid->getData()));
- for (Option::OptionCollection::iterator opt = renew->options_.begin();
+ for (OptionCollection::iterator opt = renew->options_.begin();
opt != renew->options_.end(); ++opt) {
switch (opt->second->getType()) {
+
case D6O_IA_NA: {
OptionPtr answer_opt = renewIA_NA(subnet, duid, renew,
- boost::dynamic_pointer_cast<Option6IA>(opt->second));
+ boost::dynamic_pointer_cast<
+ Option6IA>(opt->second),
+ fqdn);
+ if (answer_opt) {
+ reply->addOption(answer_opt);
+ }
+ break;
+ }
+
+ case D6O_IA_PD: {
+ OptionPtr answer_opt = renewIA_PD(subnet, duid, renew,
+ boost::dynamic_pointer_cast<
+ Option6IA>(opt->second));
if (answer_opt) {
reply->addOption(answer_opt);
}
break;
}
+
default:
break;
}
@@ -848,8 +1834,13 @@ Dhcpv6Srv::releaseLeases(const Pkt6Ptr& release, Pkt6Ptr& reply) {
}
DuidPtr duid(new DUID(opt_duid->getData()));
+ // Let's set the status to be success by default. We can override it with
+ // error status if needed. The important thing to understand here is that
+ // the global status code may be set to success only if all IA options were
+ // handled properly. Therefore the releaseIA_NA and releaseIA_PD options
+ // may turn the status code to some error, but can't turn it back to success.
int general_status = STATUS_Success;
- for (Option::OptionCollection::iterator opt = release->options_.begin();
+ for (OptionCollection::iterator opt = release->options_.begin();
opt != release->options_.end(); ++opt) {
switch (opt->second->getType()) {
case D6O_IA_NA: {
@@ -860,7 +1851,14 @@ Dhcpv6Srv::releaseLeases(const Pkt6Ptr& release, Pkt6Ptr& reply) {
}
break;
}
- // @todo: add support for IA_PD
+ case D6O_IA_PD: {
+ OptionPtr answer_opt = releaseIA_PD(duid, release, general_status,
+ boost::dynamic_pointer_cast<Option6IA>(opt->second));
+ if (answer_opt) {
+ reply->addOption(answer_opt);
+ }
+ break;
+ }
// @todo: add support for IA_TA
default:
// remaining options are stateless and thus ignored in this context
@@ -876,7 +1874,7 @@ Dhcpv6Srv::releaseLeases(const Pkt6Ptr& release, Pkt6Ptr& reply) {
}
OptionPtr
-Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
+Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, const Pkt6Ptr& query,
int& general_status, boost::shared_ptr<Option6IA> ia) {
// Release can be done in one of two ways:
// Approach 1: extract address from client's IA_NA and see if it belongs
@@ -894,12 +1892,13 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
(ia->getOption(D6O_IAADDR));
if (!release_addr) {
ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
- "You did not include address in your RELEASE"));
+ "You did not include an address in your RELEASE"));
general_status = STATUS_NoBinding;
return (ia_rsp);
}
- Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(release_addr->getAddress());
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ release_addr->getAddress());
if (!lease) {
// client releasing a lease that we don't know about.
@@ -909,7 +1908,7 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
"Sorry, no known leases for this duid/iaid, can't release."));
general_status = STATUS_NoBinding;
- LOG_INFO(dhcp6_logger, DHCP6_UNKNOWN_RELEASE)
+ LOG_INFO(dhcp6_logger, DHCP6_UNKNOWN_RELEASE_NA)
.arg(duid->toText())
.arg(ia->getIAID());
@@ -921,7 +1920,7 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
// have mandatory DUID information attached. Someone was messing with our
// database.
- LOG_ERROR(dhcp6_logger, DHCP6_LEASE_WITHOUT_DUID)
+ LOG_ERROR(dhcp6_logger, DHCP6_LEASE_NA_WITHOUT_DUID)
.arg(release_addr->getAddress().toText());
general_status = STATUS_UnspecFail;
@@ -931,9 +1930,9 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
}
if (*duid != *(lease->duid_)) {
- // Sorry, it's not your address. You can't release it.
- LOG_INFO(dhcp6_logger, DHCP6_RELEASE_FAIL_WRONG_DUID)
+ // Sorry, it's not your address. You can't release it.
+ LOG_INFO(dhcp6_logger, DHCP6_RELEASE_NA_FAIL_WRONG_DUID)
.arg(duid->toText())
.arg(release_addr->getAddress().toText())
.arg(lease->duid_->toText());
@@ -946,7 +1945,7 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
if (ia->getIAID() != lease->iaid_) {
// This address belongs to this client, but to a different IA
- LOG_WARN(dhcp6_logger, DHCP6_RELEASE_FAIL_WRONG_IAID)
+ LOG_WARN(dhcp6_logger, DHCP6_RELEASE_PD_FAIL_WRONG_IAID)
.arg(duid->toText())
.arg(release_addr->getAddress().toText())
.arg(lease->iaid_)
@@ -960,13 +1959,47 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
// It is not necessary to check if the address matches as we used
// getLease6(addr) method that is supposed to return a proper lease.
+ bool skip = false;
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_lease6_release_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass the original packet
+ callout_handle->setArgument("query6", query);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease6", lease);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease6_release_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to send the packet, so skip at this
+ // stage means "drop response".
+ if (callout_handle->getSkip()) {
+ skip = true;
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_LEASE6_RELEASE_NA_SKIP);
+ }
+ }
+
// Ok, we've passed all checks. Let's release this address.
+ bool success = false; // was the removal operation succeessful?
+
+ if (!skip) {
+ success = LeaseMgrFactory::instance().deleteLease(lease->addr_);
+ }
+
+ // Here the success should be true if we removed lease successfully
+ // and false if skip flag was set or the removal failed for whatever reason
- if (!LeaseMgrFactory::instance().deleteLease(lease->addr_)) {
+ if (!success) {
ia_rsp->addOption(createStatusCode(STATUS_UnspecFail,
"Server failed to release a lease"));
- LOG_ERROR(dhcp6_logger, DHCP6_RELEASE_FAIL)
+ LOG_ERROR(dhcp6_logger, DHCP6_RELEASE_NA_FAIL)
.arg(lease->addr_.toText())
.arg(duid->toText())
.arg(lease->iaid_);
@@ -974,7 +2007,7 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
return (ia_rsp);
} else {
- LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_RELEASE)
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_RELEASE_NA)
.arg(lease->addr_.toText())
.arg(duid->toText())
.arg(lease->iaid_);
@@ -982,10 +2015,157 @@ Dhcpv6Srv::releaseIA_NA(const DuidPtr& duid, Pkt6Ptr /* question */,
ia_rsp->addOption(createStatusCode(STATUS_Success,
"Lease released. Thank you, please come again."));
+ // Check if a lease has flags indicating that the FQDN update has
+ // been performed. If so, create NameChangeRequest which removes
+ // the entries.
+ createRemovalNameChangeRequest(lease);
+
return (ia_rsp);
}
}
+OptionPtr
+Dhcpv6Srv::releaseIA_PD(const DuidPtr& duid, const Pkt6Ptr& query,
+ int& general_status, boost::shared_ptr<Option6IA> ia) {
+ // Release can be done in one of two ways:
+ // Approach 1: extract address from client's IA_NA and see if it belongs
+ // to this particular client.
+ // Approach 2: find a subnet for this client, get a lease for
+ // this subnet/duid/iaid and check if its content matches to what the
+ // client is asking us to release.
+ //
+ // This method implements approach 1.
+
+ // That's our response. We will fill it in as we check the lease to be
+ // released.
+ boost::shared_ptr<Option6IA> ia_rsp(new Option6IA(D6O_IA_PD, ia->getIAID()));
+
+ boost::shared_ptr<Option6IAPrefix> release_prefix =
+ boost::dynamic_pointer_cast<Option6IAPrefix>(ia->getOption(D6O_IAPREFIX));
+ if (!release_prefix) {
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "You did not include a prefix in your RELEASE"));
+ general_status = STATUS_NoBinding;
+ return (ia_rsp);
+ }
+
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_PD,
+ release_prefix->getAddress());
+
+ if (!lease) {
+ // Client releasing a lease that we don't know about.
+
+ // Insert status code NoBinding.
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "Sorry, no known leases for this duid/iaid, can't release."));
+ general_status = STATUS_NoBinding;
+
+ LOG_INFO(dhcp6_logger, DHCP6_UNKNOWN_RELEASE_PD)
+ .arg(duid->toText())
+ .arg(ia->getIAID());
+
+ return (ia_rsp);
+ }
+
+ if (!lease->duid_) {
+ // Something is gravely wrong here. We do have a lease, but it does not
+ // have mandatory DUID information attached. Someone was messing with our
+ // database.
+ LOG_ERROR(dhcp6_logger, DHCP6_LEASE_PD_WITHOUT_DUID)
+ .arg(release_prefix->getAddress().toText());
+
+ general_status = STATUS_UnspecFail;
+ ia_rsp->addOption(createStatusCode(STATUS_UnspecFail,
+ "Database consistency check failed when trying to RELEASE"));
+ return (ia_rsp);
+ }
+
+ if (*duid != *(lease->duid_)) {
+ // Sorry, it's not your address. You can't release it.
+ LOG_INFO(dhcp6_logger, DHCP6_RELEASE_PD_FAIL_WRONG_DUID)
+ .arg(duid->toText())
+ .arg(release_prefix->getAddress().toText())
+ .arg(lease->duid_->toText());
+
+ general_status = STATUS_NoBinding;
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "This address does not belong to you, you can't release it"));
+ return (ia_rsp);
+ }
+
+ if (ia->getIAID() != lease->iaid_) {
+ // This address belongs to this client, but to a different IA
+ LOG_WARN(dhcp6_logger, DHCP6_RELEASE_PD_FAIL_WRONG_IAID)
+ .arg(duid->toText())
+ .arg(release_prefix->getAddress().toText())
+ .arg(lease->iaid_)
+ .arg(ia->getIAID());
+ ia_rsp->addOption(createStatusCode(STATUS_NoBinding,
+ "This is your address, but you used wrong IAID"));
+ general_status = STATUS_NoBinding;
+ return (ia_rsp);
+ }
+
+ // It is not necessary to check if the address matches as we used
+ // getLease6(addr) method that is supposed to return a proper lease.
+
+ bool skip = false;
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::calloutsPresent(Hooks.hook_index_lease6_release_)) {
+ CalloutHandlePtr callout_handle = getCalloutHandle(query);
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass the original packet
+ callout_handle->setArgument("query6", query);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease6", lease);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease6_release_, *callout_handle);
+
+ skip = callout_handle->getSkip();
+ }
+
+ // Ok, we've passed all checks. Let's release this prefix.
+ bool success = false; // was the removal operation succeessful?
+
+ if (!skip) {
+ success = LeaseMgrFactory::instance().deleteLease(lease->addr_);
+ } else {
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to send the packet, so skip at this
+ // stage means "drop response".
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_HOOKS, DHCP6_HOOK_LEASE6_RELEASE_PD_SKIP);
+ }
+
+ // Here the success should be true if we removed lease successfully
+ // and false if skip flag was set or the removal failed for whatever reason
+
+ if (!success) {
+ ia_rsp->addOption(createStatusCode(STATUS_UnspecFail,
+ "Server failed to release a lease"));
+
+ LOG_ERROR(dhcp6_logger, DHCP6_RELEASE_PD_FAIL)
+ .arg(lease->addr_.toText())
+ .arg(duid->toText())
+ .arg(lease->iaid_);
+ general_status = STATUS_UnspecFail;
+ } else {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_RELEASE_PD)
+ .arg(lease->addr_.toText())
+ .arg(duid->toText())
+ .arg(lease->iaid_);
+
+ ia_rsp->addOption(createStatusCode(STATUS_Success,
+ "Lease released. Thank you, please come again."));
+ }
+
+ return (ia_rsp);
+}
+
Pkt6Ptr
Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
@@ -996,8 +2176,14 @@ Dhcpv6Srv::processSolicit(const Pkt6Ptr& solicit) {
copyDefaultOptions(solicit, advertise);
appendDefaultOptions(solicit, advertise);
appendRequestedOptions(solicit, advertise);
+ appendRequestedVendorOptions(solicit, advertise);
- assignLeases(solicit, advertise);
+ Option6ClientFqdnPtr fqdn = processClientFqdn(solicit);
+ assignLeases(solicit, advertise, fqdn);
+ appendClientFqdn(solicit, advertise, fqdn);
+ // Note, that we don't create NameChangeRequests here because we don't
+ // perform DNS Updates for Solicit. Client must send Request to update
+ // DNS.
return (advertise);
}
@@ -1012,8 +2198,12 @@ Dhcpv6Srv::processRequest(const Pkt6Ptr& request) {
copyDefaultOptions(request, reply);
appendDefaultOptions(request, reply);
appendRequestedOptions(request, reply);
+ appendRequestedVendorOptions(request, reply);
- assignLeases(request, reply);
+ Option6ClientFqdnPtr fqdn = processClientFqdn(request);
+ assignLeases(request, reply, fqdn);
+ appendClientFqdn(request, reply, fqdn);
+ createNameChangeRequests(reply, fqdn);
return (reply);
}
@@ -1029,13 +2219,17 @@ Dhcpv6Srv::processRenew(const Pkt6Ptr& renew) {
appendDefaultOptions(renew, reply);
appendRequestedOptions(renew, reply);
- renewLeases(renew, reply);
+ Option6ClientFqdnPtr fqdn = processClientFqdn(renew);
+ renewLeases(renew, reply, fqdn);
+ appendClientFqdn(renew, reply, fqdn);
+ createNameChangeRequests(reply, fqdn);
return reply;
}
Pkt6Ptr
Dhcpv6Srv::processRebind(const Pkt6Ptr& rebind) {
+
/// @todo: Implement this
Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, rebind->getTransid()));
return reply;
@@ -1060,7 +2254,10 @@ Dhcpv6Srv::processRelease(const Pkt6Ptr& release) {
releaseLeases(release, reply);
- return reply;
+ // @todo If client sent a release and we should remove outstanding
+ // DNS records.
+
+ return (reply);
}
Pkt6Ptr
@@ -1077,5 +2274,191 @@ Dhcpv6Srv::processInfRequest(const Pkt6Ptr& infRequest) {
return reply;
}
+void
+Dhcpv6Srv::openActiveSockets(const uint16_t port) {
+ IfaceMgr::instance().closeSockets();
+
+ // Get the reference to the collection of interfaces. This reference should be
+ // valid as long as the program is run because IfaceMgr is a singleton.
+ // Therefore we can safely iterate over instances of all interfaces and modify
+ // their flags. Here we modify flags which indicate wheter socket should be
+ // open for a particular interface or not.
+ const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+ for (IfaceMgr::IfaceCollection::const_iterator iface = ifaces.begin();
+ iface != ifaces.end(); ++iface) {
+ Iface* iface_ptr = IfaceMgr::instance().getIface(iface->getName());
+ if (iface_ptr == NULL) {
+ isc_throw(isc::Unexpected, "Interface Manager returned NULL"
+ << " instance of the interface when DHCPv6 server was"
+ << " trying to reopen sockets after reconfiguration");
+ }
+ if (CfgMgr::instance().isActiveIface(iface->getName())) {
+ iface_ptr->inactive6_ = false;
+ LOG_INFO(dhcp6_logger, DHCP6_ACTIVATE_INTERFACE)
+ .arg(iface->getFullName());
+
+ } else {
+ // For deactivating interface, it should be sufficient to log it
+ // on the debug level because it is more useful to know what
+ // interface is activated which is logged on the info level.
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC,
+ DHCP6_DEACTIVATE_INTERFACE).arg(iface->getName());
+ iface_ptr->inactive6_ = true;
+
+ }
+
+ iface_ptr->clearUnicasts();
+
+ const IOAddress* unicast = CfgMgr::instance().getUnicast(iface->getName());
+ if (unicast) {
+ LOG_INFO(dhcp6_logger, DHCP6_SOCKET_UNICAST).arg(unicast->toText())
+ .arg(iface->getName());
+ iface_ptr->addUnicast(*unicast);
+ }
+ }
+ // Let's reopen active sockets. openSockets6 will check internally whether
+ // sockets are marked active or inactive.
+ // @todo Optimization: we should not reopen all sockets but rather select
+ // those that have been affected by the new configuration.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&Dhcpv6Srv::ifaceMgrSocket6ErrorHandler, _1);
+ if (!IfaceMgr::instance().openSockets6(port, error_handler)) {
+ LOG_WARN(dhcp6_logger, DHCP6_NO_SOCKETS_OPEN);
+ }
+}
+
+size_t
+Dhcpv6Srv::unpackOptions(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
+ size_t* relay_msg_offset,
+ size_t* relay_msg_len) {
+ size_t offset = 0;
+ size_t length = buf.size();
+
+ OptionDefContainer option_defs;
+ if (option_space == "dhcp6") {
+ // Get the list of stdandard option definitions.
+ option_defs = LibDHCP::getOptionDefs(Option::V6);
+ } else if (!option_space.empty()) {
+ OptionDefContainerPtr option_defs_ptr =
+ CfgMgr::instance().getOptionDefs(option_space);
+ if (option_defs_ptr != NULL) {
+ option_defs = *option_defs_ptr;
+ }
+ }
+
+ // Get the search index #1. It allows to search for option definitions
+ // using option code.
+ const OptionDefContainerTypeIndex& idx = option_defs.get<1>();
+
+ // The buffer being read comprises a set of options, each starting with
+ // a two-byte type code and a two-byte length field.
+ while (offset + 4 <= length) {
+ uint16_t opt_type = isc::util::readUint16(&buf[offset]);
+ offset += 2;
+
+ uint16_t opt_len = isc::util::readUint16(&buf[offset]);
+ offset += 2;
+
+ if (offset + opt_len > length) {
+ // @todo: consider throwing exception here.
+ return (offset);
+ }
+
+ if (opt_type == D6O_RELAY_MSG && relay_msg_offset && relay_msg_len) {
+ // remember offset of the beginning of the relay-msg option
+ *relay_msg_offset = offset;
+ *relay_msg_len = opt_len;
+
+ // do not create that relay-msg option
+ offset += opt_len;
+ continue;
+ }
+
+ // Get all definitions with the particular option code. Note that option
+ // code is non-unique within this container however at this point we
+ // expect to get one option definition with the particular code. If more
+ // are returned we report an error.
+ const OptionDefContainerTypeRange& range = idx.equal_range(opt_type);
+ // Get the number of returned option definitions for the option code.
+ size_t num_defs = distance(range.first, range.second);
+
+ OptionPtr opt;
+ if (num_defs > 1) {
+ // Multiple options of the same code are not supported right now!
+ isc_throw(isc::Unexpected, "Internal error: multiple option definitions"
+ " for option type " << opt_type << " returned. Currently it is not"
+ " supported to initialize multiple option definitions"
+ " for the same option code. This will be supported once"
+ " support for option spaces is implemented");
+ } else if (num_defs == 0) {
+ // @todo Don't crash if definition does not exist because only a few
+ // option definitions are initialized right now. In the future
+ // we will initialize definitions for all options and we will
+ // remove this elseif. For now, return generic option.
+ opt = OptionPtr(new Option(Option::V6, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len));
+ opt->setEncapsulatedSpace("dhcp6");
+ } else {
+ // The option definition has been found. Use it to create
+ // the option instance from the provided buffer chunk.
+ const OptionDefinitionPtr& def = *(range.first);
+ assert(def);
+ opt = def->optionFactory(Option::V6, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len,
+ boost::bind(&Dhcpv6Srv::unpackOptions, this, _1, _2,
+ _3, _4, _5));
+ }
+ // add option to options
+ options.insert(std::make_pair(opt_type, opt));
+ offset += opt_len;
+ }
+
+ return (offset);
+}
+
+void
+Dhcpv6Srv::ifaceMgrSocket6ErrorHandler(const std::string& errmsg) {
+ // Log the reason for socket opening failure and return.
+ LOG_WARN(dhcp6_logger, DHCP6_OPEN_SOCKET_FAIL).arg(errmsg);
+}
+
+void Dhcpv6Srv::classifyPacket(const Pkt6Ptr& pkt) {
+
+ boost::shared_ptr<OptionCustom> vclass =
+ boost::dynamic_pointer_cast<OptionCustom>(pkt->getOption(D6O_VENDOR_CLASS));
+
+ if (!vclass) {
+ return;
+ }
+
+ string classes = "";
+
+ // DOCSIS specific section
+ if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
+ .find(DOCSIS3_CLASS_MODEM) != std::string::npos) {
+ pkt->addClass(DOCSIS3_CLASS_MODEM);
+ classes += string(DOCSIS3_CLASS_MODEM) + " ";
+ } else
+ if (vclass->readString(VENDOR_CLASS_STRING_INDEX)
+ .find(DOCSIS3_CLASS_EROUTER) != std::string::npos) {
+ pkt->addClass(DOCSIS3_CLASS_EROUTER);
+ classes += string(DOCSIS3_CLASS_EROUTER) + " ";
+ }else
+ {
+ // Otherwise use the string as is
+ classes += vclass->readString(VENDOR_CLASS_STRING_INDEX);
+ pkt->addClass(vclass->readString(VENDOR_CLASS_STRING_INDEX));
+ }
+
+ if (!classes.empty()) {
+ LOG_DEBUG(dhcp6_logger, DBG_DHCP6_BASIC, DHCP6_CLASS_ASSIGNED)
+ .arg(classes);
+ }
+}
+
};
};
diff --git a/src/bin/dhcp6/dhcp6_srv.h b/src/bin/dhcp6/dhcp6_srv.h
index c7b1f0f..594d833 100644
--- a/src/bin/dhcp6/dhcp6_srv.h
+++ b/src/bin/dhcp6/dhcp6_srv.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -15,18 +15,22 @@
#ifndef DHCPV6_SRV_H
#define DHCPV6_SRV_H
+#include <dhcp_ddns/ncr_msg.h>
#include <dhcp/dhcp6.h>
#include <dhcp/duid.h>
#include <dhcp/option.h>
+#include <dhcp/option6_client_fqdn.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option_definition.h>
#include <dhcp/pkt6.h>
#include <dhcpsrv/alloc_engine.h>
#include <dhcpsrv/subnet.h>
+#include <hooks/callout_handle.h>
#include <boost/noncopyable.hpp>
#include <iostream>
+#include <queue>
namespace isc {
namespace dhcp {
@@ -78,7 +82,7 @@ public:
///
/// Main server processing loop. Receives incoming packets, verifies
/// their correctness, generates appropriate answer (if needed) and
- /// transmits respones.
+ /// transmits responses.
///
/// @return true, if being shut down gracefully, fail if experienced
/// critical error.
@@ -87,6 +91,32 @@ public:
/// @brief Instructs the server to shut down.
void shutdown();
+ /// @brief Get UDP port on which server should listen.
+ ///
+ /// Typically, server listens on UDP port 547. Other ports are only
+ /// used for testing purposes.
+ ///
+ /// This accessor must be public because sockets are reopened from the
+ /// static configuration callback handler. This callback handler invokes
+ /// @c ControlledDhcpv4Srv::openActiveSockets which requires port parameter
+ /// which has to be retrieved from the @c ControlledDhcpv4Srv object.
+ /// They are retrieved using this public function.
+ ///
+ /// @return UDP port on which server should listen.
+ uint16_t getPort() const {
+ return (port_);
+ }
+
+ /// @brief Open sockets which are marked as active in @c CfgMgr.
+ ///
+ /// This function reopens sockets according to the current settings in the
+ /// Configuration Manager. It holds the list of the interfaces which server
+ /// should listen on. This function will open sockets on these interfaces
+ /// only. This function is not exception safe.
+ ///
+ /// @param port UDP port on which server should listen.
+ static void openActiveSockets(const uint16_t port);
+
protected:
/// @brief verifies if specified packet meets RFC requirements
@@ -105,7 +135,7 @@ protected:
///
/// Processes received SOLICIT message and verifies that its sender
/// should be served. In particular IA, TA and PD options are populated
- /// with to-be assinged addresses, temporary addresses and delegated
+ /// with to-be assigned addresses, temporary addresses and delegated
/// prefixes, respectively. In the usual 4 message exchange, server is
/// expected to respond with ADVERTISE message. However, if client
/// requests rapid-commit and server supports it, REPLY will be sent
@@ -121,7 +151,7 @@ protected:
///
/// Processes incoming REQUEST message and verifies that its sender
/// should be served. In particular IA, TA and PD options are populated
- /// with assinged addresses, temporary addresses and delegated
+ /// with assigned addresses, temporary addresses and delegated
/// prefixes, respectively. Uses LeaseMgr to allocate or update existing
/// leases.
///
@@ -176,19 +206,39 @@ protected:
/// @brief Processes IA_NA option (and assigns addresses if necessary).
///
/// Generates response to IA_NA. This typically includes selecting (and
- /// allocating a lease in case of REQUEST) a lease and creating
+ /// allocating a lease in case of REQUEST) an address lease and creating
/// IAADDR option. In case of allocation failure, it may contain
/// status code option with non-zero status, denoting cause of the
/// allocation failure.
///
/// @param subnet subnet the client is connected to
/// @param duid client's duid
- /// @param question client's message (typically SOLICIT or REQUEST)
+ /// @param query client's message (typically SOLICIT or REQUEST)
/// @param ia pointer to client's IA_NA option (client's request)
+ /// @param fqdn A DHCPv6 Client FQDN %Option generated in a response to the
+ /// FQDN option sent by a client.
/// @return IA_NA option (server's response)
OptionPtr assignIA_NA(const isc::dhcp::Subnet6Ptr& subnet,
const isc::dhcp::DuidPtr& duid,
- isc::dhcp::Pkt6Ptr question,
+ const isc::dhcp::Pkt6Ptr& query,
+ Option6IAPtr ia,
+ const Option6ClientFqdnPtr& fqdn);
+
+ /// @brief Processes IA_PD option (and assigns prefixes if necessary).
+ ///
+ /// Generates response to IA_PD. This typically includes selecting (and
+ /// allocating in the case of REQUEST) a prefix lease and creating an
+ /// IAPREFIX option. In case of an allocation failure, it may contain a
+ /// status code option with non-zero status denoting the cause of the
+ /// allocation failure.
+ ///
+ /// @param subnet subnet the client is connected to
+ /// @param duid client's duid
+ /// @param query client's message (typically SOLICIT or REQUEST)
+ /// @param ia pointer to client's IA_PD option (client's request)
+ /// @return IA_PD option (server's response)
+ OptionPtr assignIA_PD(const Subnet6Ptr& subnet, const DuidPtr& duid,
+ const Pkt6Ptr& query,
boost::shared_ptr<Option6IA> ia);
/// @brief Renews specific IA_NA option
@@ -199,11 +249,27 @@ protected:
///
/// @param subnet subnet the sender belongs to
/// @param duid client's duid
- /// @param question client's message
+ /// @param query client's message
/// @param ia IA_NA option that is being renewed
+ /// @param fqdn DHCPv6 Client FQDN Option included in the server's response
/// @return IA_NA option (server's response)
OptionPtr renewIA_NA(const Subnet6Ptr& subnet, const DuidPtr& duid,
- Pkt6Ptr question, boost::shared_ptr<Option6IA> ia);
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia,
+ const Option6ClientFqdnPtr& fqdn);
+
+ /// @brief Renews specific IA_PD option
+ ///
+ /// Generates response to IA_PD in Renew. This typically includes finding a
+ /// lease that corresponds to the received prefix. If no such lease is
+ /// found, an IA_PD response is generated with an appropriate status code.
+ ///
+ /// @param subnet subnet the sender belongs to
+ /// @param duid client's duid
+ /// @param query client's message
+ /// @param ia IA_PD option that is being renewed
+ /// @return IA_PD option (server's response)
+ OptionPtr renewIA_PD(const Subnet6Ptr& subnet, const DuidPtr& duid,
+ const Pkt6Ptr& query, boost::shared_ptr<Option6IA> ia);
/// @brief Releases specific IA_NA option
///
@@ -218,11 +284,27 @@ protected:
/// release process fails.
///
/// @param duid client's duid
- /// @param question client's message
+ /// @param query client's message
/// @param general_status a global status (it may be updated in case of errors)
- /// @param ia IA_NA option that is being renewed
+ /// @param ia IA_NA option that is being released
/// @return IA_NA option (server's response)
- OptionPtr releaseIA_NA(const DuidPtr& duid, Pkt6Ptr question,
+ OptionPtr releaseIA_NA(const DuidPtr& duid, const Pkt6Ptr& query,
+ int& general_status,
+ boost::shared_ptr<Option6IA> ia);
+
+ /// @brief Releases specific IA_PD option
+ ///
+ /// Generates response to IA_PD in Release message. This covers finding and
+ /// removal of a lease that corresponds to the received prefix(es). If no such
+ /// lease is found, an IA_PD response is generated with an appropriate
+ /// status code.
+ ///
+ /// @param duid client's duid
+ /// @param query client's message
+ /// @param general_status a global status (it may be updated in case of errors)
+ /// @param ia IA_PD option that is being released
+ /// @return IA_PD option (server's response)
+ OptionPtr releaseIA_PD(const DuidPtr& duid, const Pkt6Ptr& query,
int& general_status,
boost::shared_ptr<Option6IA> ia);
@@ -254,6 +336,15 @@ protected:
/// @param answer server's message (options will be added here)
void appendRequestedOptions(const Pkt6Ptr& question, Pkt6Ptr& answer);
+ /// @brief Appends requested vendor options to server's answer.
+ ///
+ /// This is mostly useful for Cable Labs options for now, but the method
+ /// is easily extensible to other vendors.
+ ///
+ /// @param question client's message
+ /// @param answer server's message (vendor options will be added here)
+ void appendRequestedVendorOptions(const Pkt6Ptr& question, Pkt6Ptr& answer);
+
/// @brief Assigns leases.
///
/// It supports addresses (IA_NA) only. It does NOT support temporary
@@ -262,7 +353,94 @@ protected:
///
/// @param question client's message (with requested IA_NA)
/// @param answer server's message (IA_NA options will be added here)
- void assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer);
+ /// @param fqdn an FQDN option generated in a response to the client's
+ /// FQDN option.
+ void assignLeases(const Pkt6Ptr& question, Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& fqdn);
+
+ /// @brief Processes Client FQDN Option.
+ ///
+ /// This function retrieves DHCPv6 Client FQDN %Option (if any) from the
+ /// packet sent by a client and takes necessary actions upon this option.
+ /// Received option comprises flags field which controls what DNS updates
+ /// server should do. Server may override client's preference based on
+ /// the current configuration. Server indicates that it has overridden
+ /// the preference by storing DHCPv6 Client Fqdn %Option with the
+ /// appropriate flags in the response to a client. This option is also
+ /// used to communicate the client's domain-name which should be sent
+ /// to the DNS in the update. Again, server may act upon the received
+ /// domain-name, i.e. if the provided domain-name is partial it should
+ /// generate the fully qualified domain-name.
+ ///
+ /// All the logic required to form appropriate answer to the client is
+ /// held in this function.
+ ///
+ /// @param question Client's message.
+ ///
+ /// @return FQDN option produced in the response to the client's message.
+ Option6ClientFqdnPtr processClientFqdn(const Pkt6Ptr& question);
+
+ /// @brief Adds DHCPv6 Client FQDN %Option to the server response.
+ ///
+ /// This function will add the specified FQDN option into the server's
+ /// response when FQDN is not NULL and server is either configured to
+ /// always include the FQDN in the response or client requested it using
+ /// %Option Request %Option.
+ /// This function is exception safe.
+ ///
+ /// @param question A message received from the client.
+ /// @param [out] answer A server's response where FQDN option will be added.
+ /// @param fqdn A DHCPv6 Client FQDN %Option to be added to the server's
+ /// response to a client.
+ void appendClientFqdn(const Pkt6Ptr& question,
+ Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& fqdn);
+
+ /// @brief Creates a number of @c isc::dhcp_ddns::NameChangeRequest objects
+ /// based on the DHCPv6 Client FQDN %Option.
+ ///
+ /// The @c isc::dhcp_ddns::NameChangeRequest class encapsulates the request
+ /// from the DHCPv6 server to the DHCP-DDNS module to perform DNS Update.
+ /// The FQDN option carries response to the client about DNS updates that
+ /// server intents to perform for the DNS client. Based on this, the
+ /// function will create zero or more @c isc::dhcp_ddns::NameChangeRequest
+ /// objects and store them in the internal queue. Requests created by this
+ /// function are only adding or updating DNS records. In order to generate
+ /// requests for DNS records removal, use @c createRemovalNameChangeRequest.
+ ///
+ /// @todo Add support for multiple IAADDR options in the IA_NA.
+ ///
+ /// @param answer A message beging sent to the Client.
+ /// @param fqdn_answer A DHCPv6 Client FQDN %Option which is included in the
+ /// response message sent to a client.
+ void createNameChangeRequests(const Pkt6Ptr& answer,
+ const Option6ClientFqdnPtr& fqdn_answer);
+
+ /// @brief Creates a @c isc::dhcp_ddns::NameChangeRequest which requests
+ /// removal of DNS entries for a particular lease.
+ ///
+ /// This function should be called upon removal of the lease from the lease
+ /// database, i.e, when client sent Release or Decline message. It will
+ /// create a single @c isc::dhcp_ddns::NameChangeRequest which removes the
+ /// existing DNS records for the lease, which server is responsible for.
+ /// Note that this function will not remove the entries which server hadn't
+ /// added. This is the case, when client performs forward DNS update on its
+ /// own.
+ ///
+ /// @param lease A lease for which the the removal of corresponding DNS
+ /// records will be performed.
+ void createRemovalNameChangeRequest(const Lease6Ptr& lease);
+
+ /// @brief Sends all outstanding NameChangeRequests to bind10-d2 module.
+ ///
+ /// The purpose of this function is to pick all outstanding
+ /// NameChangeRequests from the FIFO queue and send them to b10-dhcp-ddns
+ /// module.
+ ///
+ /// @todo Currently this function simply removes all requests from the
+ /// queue but doesn't send them anywhere. In the future, the
+ /// NameChangeSender will be used to deliver requests to the other module.
+ void sendNameChangeRequests();
/// @brief Attempts to renew received addresses
///
@@ -272,7 +450,10 @@ protected:
/// as IA_NA/IAADDR to reply packet.
/// @param renew client's message asking for renew
/// @param reply server's response
- void renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply);
+ /// @param fqdn A DHCPv6 Client FQDN %Option generated in the response to the
+ /// client's FQDN option.
+ void renewLeases(const Pkt6Ptr& renew, Pkt6Ptr& reply,
+ const Option6ClientFqdnPtr& fqdn);
/// @brief Attempts to release received addresses
///
@@ -321,7 +502,58 @@ protected:
/// @return string representation
static std::string duidToString(const OptionPtr& opt);
+
+ /// @brief dummy wrapper around IfaceMgr::receive6
+ ///
+ /// This method is useful for testing purposes, where its replacement
+ /// simulates reception of a packet. For that purpose it is protected.
+ virtual Pkt6Ptr receivePacket(int timeout);
+
+ /// @brief dummy wrapper around IfaceMgr::send()
+ ///
+ /// This method is useful for testing purposes, where its replacement
+ /// simulates transmission of a packet. For that purpose it is protected.
+ virtual void sendPacket(const Pkt6Ptr& pkt);
+
+ /// @brief Implements a callback function to parse options in the message.
+ ///
+ /// @param buf a A buffer holding options in on-wire format.
+ /// @param option_space A name of the option space which holds definitions
+ /// of to be used to parse options in the packets.
+ /// @param [out] options A reference to the collection where parsed options
+ /// will be stored.
+ /// @param relay_msg_offset Reference to a size_t structure. If specified,
+ /// offset to beginning of relay_msg option will be stored in it.
+ /// @param relay_msg_len reference to a size_t structure. If specified,
+ /// length of the relay_msg option will be stored in it.
+ /// @return An offset to the first byte after last parsed option.
+ size_t unpackOptions(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
+ size_t* relay_msg_offset,
+ size_t* relay_msg_len);
+
+ /// @brief Assigns incoming packet to zero or more classes.
+ ///
+ /// @note For now, the client classification is very simple. It just uses
+ /// content of the vendor-class-identifier option as a class. The resulting
+ /// class will be stored in packet (see @ref isc::dhcp::Pkt6::classes_ and
+ /// @ref isc::dhcp::Pkt6::inClass).
+ ///
+ /// @param pkt packet to be classified
+ void classifyPacket(const Pkt6Ptr& pkt);
+
private:
+
+ /// @brief Implements the error handler for socket open failure.
+ ///
+ /// This callback function is installed on the @c isc::dhcp::IfaceMgr
+ /// when IPv6 sockets are being open. When socket fails to open for
+ /// any reason, this function is called. It simply logs the error message.
+ ///
+ /// @param errmsg An error message containing a cause of the failure.
+ static void ifaceMgrSocket6ErrorHandler(const std::string& errmsg);
+
/// @brief Allocation Engine.
/// Pointer to the allocation engine that we are currently using
/// It must be a pointer, because we will support changing engines
@@ -334,6 +566,15 @@ private:
/// Indicates if shutdown is in progress. Setting it to true will
/// initiate server shutdown procedure.
volatile bool shutdown_;
+
+ /// UDP port number on which server listens.
+ uint16_t port_;
+
+protected:
+
+ /// Holds a list of @c isc::dhcp_ddns::NameChangeRequest objects, which
+ /// are waiting for sending to b10-dhcp-ddns module.
+ std::queue<isc::dhcp_ddns::NameChangeRequest> name_change_reqs_;
};
}; // namespace isc::dhcp
diff --git a/src/bin/dhcp6/tests/.gitignore b/src/bin/dhcp6/tests/.gitignore
index e170d18..f4ca783 100644
--- a/src/bin/dhcp6/tests/.gitignore
+++ b/src/bin/dhcp6/tests/.gitignore
@@ -1 +1,4 @@
/dhcp6_unittests
+/marker_file.h
+/test_data_files_config.h
+/test_libraries.h
diff --git a/src/bin/dhcp6/tests/Makefile.am b/src/bin/dhcp6/tests/Makefile.am
index a2e2ed0..f548ec2 100644
--- a/src/bin/dhcp6/tests/Makefile.am
+++ b/src/bin/dhcp6/tests/Makefile.am
@@ -27,7 +27,8 @@ AM_CPPFLAGS += -I$(top_srcdir)/src/bin
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
-CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
+CLEANFILES = $(builddir)/interfaces.txt $(builddir)/logger_lockfile
+CLEANFILES += $(builddir)/load_marker.txt $(builddir)/unload_marker.txt
AM_CXXFLAGS = $(B10_CXXFLAGS)
if USE_CLANGPP
@@ -35,27 +36,53 @@ if USE_CLANGPP
AM_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_STATIC_LINK
-AM_LDFLAGS = -static
-endif
-
TESTS_ENVIRONMENT = \
$(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
TESTS =
if HAVE_GTEST
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
-TESTS += dhcp6_unittests
+lib_LTLIBRARIES = libco1.la libco2.la
+
+libco1_la_SOURCES = callout_library_1.cc callout_library_common.h
+libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco1_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+libco2_la_SOURCES = callout_library_2.cc callout_library_common.h
+libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco2_la_CPPFLAGS = $(AM_CPPFLAGS)
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+TESTS += dhcp6_unittests
dhcp6_unittests_SOURCES = dhcp6_unittests.cc
dhcp6_unittests_SOURCES += dhcp6_srv_unittest.cc
+dhcp6_unittests_SOURCES += fqdn_unittest.cc
+dhcp6_unittests_SOURCES += hooks_unittest.cc
+dhcp6_unittests_SOURCES += dhcp6_test_utils.cc dhcp6_test_utils.h
dhcp6_unittests_SOURCES += ctrl_dhcp6_srv_unittest.cc
dhcp6_unittests_SOURCES += config_parser_unittest.cc
+dhcp6_unittests_SOURCES += marker_file.cc
dhcp6_unittests_SOURCES += ../dhcp6_srv.h ../dhcp6_srv.cc
dhcp6_unittests_SOURCES += ../dhcp6_log.h ../dhcp6_log.cc
dhcp6_unittests_SOURCES += ../ctrl_dhcp6_srv.cc
dhcp6_unittests_SOURCES += ../config_parser.cc ../config_parser.h
-nodist_dhcp6_unittests_SOURCES = ../dhcp6_messages.h ../dhcp6_messages.cc
+dhcp6_unittests_SOURCES += wireshark.cc
+
+nodist_dhcp6_unittests_SOURCES = ../dhcp6_messages.h ../dhcp6_messages.cc
+nodist_dhcp6_unittests_SOURCES += marker_file.h test_libraries.h
dhcp6_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
dhcp6_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
@@ -64,7 +91,9 @@ dhcp6_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
+dhcp6_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
dhcp6_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
diff --git a/src/bin/dhcp6/tests/callout_library_1.cc b/src/bin/dhcp6/tests/callout_library_1.cc
new file mode 100644
index 0000000..471bb6f
--- /dev/null
+++ b/src/bin/dhcp6/tests/callout_library_1.cc
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 1;
+#include "callout_library_common.h"
diff --git a/src/bin/dhcp6/tests/callout_library_2.cc b/src/bin/dhcp6/tests/callout_library_2.cc
new file mode 100644
index 0000000..b0b4637
--- /dev/null
+++ b/src/bin/dhcp6/tests/callout_library_2.cc
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests. See callout_common.cc for details.
+
+static const int LIBRARY_NUMBER = 2;
+#include "callout_library_common.h"
diff --git a/src/bin/dhcp6/tests/callout_library_common.h b/src/bin/dhcp6/tests/callout_library_common.h
new file mode 100644
index 0000000..cbabcda
--- /dev/null
+++ b/src/bin/dhcp6/tests/callout_library_common.h
@@ -0,0 +1,82 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Marker file callout library
+///
+/// This is the source of a test library for the DHCP parser and configuration
+/// tests.
+///
+/// To check that they libraries are loaded and unloaded correctly, the load
+/// and unload functions in this library maintain two marker files - the load
+/// marker file and the unload marker file. The functions append a single
+/// line to the file, creating the file if need be. In this way, the test code
+/// can determine whether the load/unload functions have been run and, if so,
+/// in what order.
+///
+/// This file is the common library file for the tests. It will not compile
+/// by itself - it is included into each callout library which specifies the
+/// missing constant LIBRARY_NUMBER before the inclusion.
+
+#include <hooks/hooks.h>
+#include "marker_file.h"
+
+#include <fstream>
+
+using namespace isc::hooks;
+using namespace std;
+
+extern "C" {
+
+/// @brief Append digit to marker file
+///
+/// If the marker file does not exist, create it. Then append the single
+/// digit (given by the constant LIBRARY_NUMBER) defined earlier to it and
+/// close the file.
+///
+/// @param name Name of the file to open
+///
+/// @return 0 on success, non-zero on error.
+int
+appendDigit(const char* name) {
+ // Open the file and check if successful.
+ fstream file(name, fstream::out | fstream::app);
+ if (!file.good()) {
+ return (1);
+ }
+
+ // Add the library number to it and close.
+ file << LIBRARY_NUMBER;
+ file.close();
+
+ return (0);
+}
+
+// Framework functions
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int
+load(LibraryHandle&) {
+ return (appendDigit(LOAD_MARKER_FILE));
+}
+
+int
+unload() {
+ return (appendDigit(UNLOAD_MARKER_FILE));
+}
+
+};
diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc
index f4ebf0c..179f460 100644
--- a/src/bin/dhcp6/tests/config_parser_unittest.cc
+++ b/src/bin/dhcp6/tests/config_parser_unittest.cc
@@ -22,27 +22,50 @@
#include <dhcp/option_int.h>
#include <dhcp6/config_parser.h>
#include <dhcp6/dhcp6_srv.h>
+#include <dhcpsrv/addr_utilities.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/subnet.h>
+#include <hooks/hooks_manager.h>
+
+#include "test_data_files_config.h"
+#include "test_libraries.h"
+#include "marker_file.h"
#include <boost/foreach.hpp>
#include <gtest/gtest.h>
#include <fstream>
#include <iostream>
+#include <fstream>
#include <sstream>
+#include <string>
+#include <vector>
#include <arpa/inet.h>
+#include <unistd.h>
-using namespace std;
using namespace isc;
-using namespace isc::dhcp;
using namespace isc::asiolink;
-using namespace isc::data;
using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
+using namespace std;
namespace {
+std::string specfile(const std::string& name) {
+ return (std::string(DHCP6_SRC_DIR) + "/" + name);
+}
+
+/// @brief Tests that the spec file is valid.
+/// Verifies that the DHCP6 configuration specification file is valid.
+TEST(Dhcp6SpecTest, basicSpec) {
+ ASSERT_NO_THROW(isc::config::
+ moduleSpecFromFile(specfile("dhcp6.spec")));
+}
+
class Dhcp6ParserTest : public ::testing::Test {
public:
Dhcp6ParserTest() :rcode_(-1), srv_(0) {
@@ -66,11 +89,27 @@ public:
<< " while the test assumes that it doesn't, to execute"
<< " some negative scenarios. Can't continue this test.";
}
+
+ // Reset configuration for each test.
+ resetConfiguration();
+ }
+
+ // Check that no hooks libraries are loaded. This is a pre-condition for
+ // a number of tests, so is checked in one place. As this uses an
+ // ASSERT call - and it is not clear from the documentation that Gtest
+ // predicates can be used in a constructor - the check is placed in SetUp.
+ void SetUp() {
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_TRUE(libraries.empty());
}
~Dhcp6ParserTest() {
// Reset configuration database after each test.
resetConfiguration();
+
+ // ... and delete the hooks library marker files if present
+ unlink(LOAD_MARKER_FILE);
+ unlink(UNLOAD_MARKER_FILE);
};
// Checks if config_result (result of DHCP server configuration) has
@@ -133,7 +172,7 @@ public:
std::string>& params)
{
std::ostringstream stream;
- stream << "{ \"interface\": [ \"all\" ],"
+ stream << "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -169,50 +208,80 @@ public:
return (stream.str());
}
- /// @brief Reset configuration database.
+ /// @brief Parse and Execute configuration
///
- /// This function resets configuration data base by
- /// removing all subnets and option-data. Reset must
- /// be performed after each test to make sure that
- /// contents of the database do not affect result of
- /// subsequent tests.
- void resetConfiguration() {
+ /// Parses a configuration and executes a configuration of the server.
+ /// If the operation fails, the current test will register a failure.
+ ///
+ /// @param config Configuration to parse
+ /// @param operation Operation being performed. In the case of an error,
+ /// the error text will include the string "unable to <operation>.".
+ ///
+ /// @return true if the configuration succeeded, false if not. In the
+ /// latter case, a failure will have been added to the current test.
+ bool
+ executeConfiguration(const std::string& config, const char* operation) {
ConstElementPtr status;
-
- string config = "{ \"interface\": [ \"all\" ],"
- "\"preferred-lifetime\": 3000,"
- "\"rebind-timer\": 2000, "
- "\"renew-timer\": 1000, "
- "\"valid-lifetime\": 4000, "
- "\"subnet6\": [ ], "
- "\"option-def\": [ ], "
- "\"option-data\": [ ] }";
-
try {
ElementPtr json = Element::fromJSON(config);
status = configureDhcp6Server(srv_, json);
} catch (const std::exception& ex) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. The following configuration was used"
- << " to reset database: " << std::endl
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The following configuration was used: " << std::endl
<< config << std::endl
<< " and the following error message was returned:"
<< ex.what() << std::endl;
+ return (false);
}
- // status object must not be NULL
+ // The status object must not be NULL
if (!status) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. Configuration function returned"
- << " NULL pointer" << std::endl;
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned a null pointer.";
+ return (false);
}
+
+ // Store the answer if we need it.
+
+ // Returned value should be 0 (configuration success)
comment_ = parseAnswer(rcode_, status);
- // returned value should be 0 (configuration success)
if (rcode_ != 0) {
- FAIL() << "Fatal error: unable to reset configuration database"
- << " after the test. Configuration function returned"
- << " error code " << rcode_ << std::endl;
+ string reason = "";
+ if (comment_) {
+ reason = string(" (") + comment_->stringValue() + string(")");
+ }
+ ADD_FAILURE() << "Unable to " << operation << ". "
+ << "The configuration function returned error code "
+ << rcode_ << reason;
+ return (false);
}
+
+ return (true);
+ }
+
+ /// @brief Reset configuration database.
+ ///
+ /// This function resets configuration data base by removing all subnets
+ /// option-data, and hooks libraries. The reset must be performed after each
+ /// test to make sure that contents of the database do not affect the
+ /// results of subsequent tests.
+ void resetConfiguration() {
+ string config = "{ \"interfaces\": [ \"all\" ],"
+ "\"hooks-libraries\": [ ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000, "
+ "\"subnet6\": [ ], "
+ "\"option-def\": [ ], "
+ "\"option-data\": [ ] }";
+ static_cast<void>(executeConfiguration(config,
+ "reset configuration database"));
+ // The default setting is to listen on all interfaces. In order to
+ // properly test interface configuration we disable listening on
+ // all interfaces before each test and later check that this setting
+ // has been overriden by the configuration used in the test.
+ CfgMgr::instance().deleteActiveIfaces();
}
/// @brief Test invalid option parameter value.
@@ -277,13 +346,11 @@ public:
expected_data_len));
}
- int rcode_;
- Dhcpv6Srv srv_;
-
- ConstElementPtr comment_;
-
- string valid_iface_;
- string bogus_iface_;
+ int rcode_; ///< Return code (see @ref isc::config::parseAnswer)
+ Dhcpv6Srv srv_; ///< Instance of the Dhcp6Srv used during tests
+ ConstElementPtr comment_; ///< Comment (see @ref isc::config::parseAnswer)
+ string valid_iface_; ///< Valid network interface name (present in system)
+ string bogus_iface_; ///< invalid network interface name (not in system)
};
// Goal of this test is a verification if a very simple config update
@@ -324,7 +391,7 @@ TEST_F(Dhcp6ParserTest, emptySubnet) {
ConstElementPtr status;
EXPECT_NO_THROW(status = configureDhcp6Server(srv_,
- Element::fromJSON("{ \"interface\": [ \"all\" ],"
+ Element::fromJSON("{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -343,7 +410,7 @@ TEST_F(Dhcp6ParserTest, subnetGlobalDefaults) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -369,15 +436,197 @@ TEST_F(Dhcp6ParserTest, subnetGlobalDefaults) {
EXPECT_EQ(2000, subnet->getT2());
EXPECT_EQ(3000, subnet->getPreferred());
EXPECT_EQ(4000, subnet->getValid());
+
+ // Check that subnet-id is 1
+ EXPECT_EQ(1, subnet->getID());
+}
+
+// Goal of this test is to verify that multiple subnets get unique
+// subnet-ids. Also, test checks that it's possible to do reconfiguration
+// multiple times.
+TEST_F(Dhcp6ParserTest, multipleSubnets) {
+ ConstElementPtr x;
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:2::/80\" ],"
+ " \"subnet\": \"2001:db8:2::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:3::/80\" ],"
+ " \"subnet\": \"2001:db8:3::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:4::/80\" ],"
+ " \"subnet\": \"2001:db8:4::/64\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ int cnt = 0; // Number of reconfigurations
+
+ do {
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Check subnet-ids of each subnet (it should be monotonously increasing)
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ EXPECT_EQ(2, subnets->at(1)->getID());
+ EXPECT_EQ(3, subnets->at(2)->getID());
+ EXPECT_EQ(4, subnets->at(3)->getID());
+
+ // Repeat reconfiguration process 10 times and check that the subnet-id
+ // is set to the same value. Technically, just two iterations would be
+ // sufficient, but it's nice to have a test that exercises reconfiguration
+ // a bit.
+ } while (++cnt < 10);
}
+// Goal of this test is to verify that a previously configured subnet can be
+// deleted in subsequent reconfiguration.
+TEST_F(Dhcp6ParserTest, reconfigureRemoveSubnet) {
+ ConstElementPtr x;
+
+ // All four subnets
+ string config4 = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:2::/80\" ],"
+ " \"subnet\": \"2001:db8:2::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:3::/80\" ],"
+ " \"subnet\": \"2001:db8:3::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:4::/80\" ],"
+ " \"subnet\": \"2001:db8:4::/64\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Three subnets (the last one removed)
+ string config_first3 = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:2::/80\" ],"
+ " \"subnet\": \"2001:db8:2::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:3::/80\" ],"
+ " \"subnet\": \"2001:db8:3::/64\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // Second subnet removed
+ string config_second_removed = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:3::/80\" ],"
+ " \"subnet\": \"2001:db8:3::/64\" "
+ " },"
+ " {"
+ " \"pool\": [ \"2001:db8:4::/80\" ],"
+ " \"subnet\": \"2001:db8:4::/64\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // CASE 1: Configure 4 subnets, then reconfigure and remove the
+ // last one.
+
+ ElementPtr json = Element::fromJSON(config4);
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(4, subnets->size()); // We expect 4 subnets
+
+ // Do the reconfiguration (the last subnet is removed)
+ json = Element::fromJSON(config_first3);
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ subnets = CfgMgr::instance().getSubnets6();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 3 subnets now (4th is removed)
+
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ EXPECT_EQ(2, subnets->at(1)->getID());
+ EXPECT_EQ(3, subnets->at(2)->getID());
+
+ /// CASE 2: Configure 4 subnets, then reconfigure and remove one
+ /// from in between (not first, not last)
+
+#if 0
+ /// @todo: Uncomment subnet removal test as part of #3281.
+ json = Element::fromJSON(config4);
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ // Do reconfiguration
+ json = Element::fromJSON(config_second_removed);
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+
+ subnets = CfgMgr::instance().getSubnets6();
+ ASSERT_TRUE(subnets);
+ ASSERT_EQ(3, subnets->size()); // We expect 4 subnets
+
+ EXPECT_EQ(1, subnets->at(0)->getID());
+ // The second subnet (with subnet-id = 2) is no longer there
+ EXPECT_EQ(3, subnets->at(1)->getID());
+ EXPECT_EQ(4, subnets->at(2)->getID());
+#endif
+}
+
+
+
// This test checks if it is possible to override global values
// on a per subnet basis.
TEST_F(Dhcp6ParserTest, subnetLocal) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -415,7 +664,7 @@ TEST_F(Dhcp6ParserTest, subnetInterface) {
// There should be at least one interface
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -448,7 +697,7 @@ TEST_F(Dhcp6ParserTest, subnetInterfaceBogus) {
// There should be at least one interface
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -479,7 +728,7 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -500,13 +749,111 @@ TEST_F(Dhcp6ParserTest, interfaceGlobal) {
EXPECT_EQ(1, rcode_);
}
+
+// This test checks if it is possible to define a subnet with an
+// interface-id option defined.
+TEST_F(Dhcp6ParserTest, subnetInterfaceId) {
+
+ const string valid_interface_id = "foobar";
+ const string bogus_interface_id = "blah";
+
+ // There should be at least one interface
+
+ const string config = "{ "
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+ " \"interface-id\": \"" + valid_interface_id + "\","
+ " \"subnet\": \"2001:db8:1::/64\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // Returned value should be 0 (configuration success)
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(0, rcode_);
+
+ // Try to get a subnet based on bogus interface-id option
+ OptionBuffer tmp(bogus_interface_id.begin(), bogus_interface_id.end());
+ OptionPtr ifaceid(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+ Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(ifaceid);
+ EXPECT_FALSE(subnet);
+
+ // Now try to get subnet for valid interface-id value
+ tmp = OptionBuffer(valid_interface_id.begin(), valid_interface_id.end());
+ ifaceid.reset(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+ subnet = CfgMgr::instance().getSubnet6(ifaceid);
+ ASSERT_TRUE(subnet);
+ EXPECT_TRUE(ifaceid->equal(subnet->getInterfaceId()));
+}
+
+
+// This test checks if it is not allowed to define global interface
+// parameter.
+TEST_F(Dhcp6ParserTest, interfaceIdGlobal) {
+
+ const string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"interface-id\": \"foobar\"," // Not valid. Can be defined in subnet only
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+ " \"subnet\": \"2001:db8:1::/64\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // Returned value should be 1 (parse error)
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(1, rcode_);
+}
+
+// This test checks if it is not possible to define a subnet with an
+// interface (i.e. local subnet) and interface-id (remote subnet) defined.
+TEST_F(Dhcp6ParserTest, subnetInterfaceAndInterfaceId) {
+
+ const string config = "{ \"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::1 - 2001:db8:1::ffff\" ],"
+ " \"interface\": \"" + valid_iface_ + "\","
+ " \"interface-id\": \"foobar\","
+ " \"subnet\": \"2001:db8:1::/64\" } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ ConstElementPtr status;
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // Returned value should be 1 (configuration error)
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(1, rcode_);
+
+}
+
+
+
// Test verifies that a subnet with pool values that do not belong to that
// pool are rejected.
TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
ConstElementPtr status;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -530,11 +877,13 @@ TEST_F(Dhcp6ParserTest, poolOutOfSubnet) {
// Goal of this test is to verify if pools can be defined
// using prefix/length notation. There is no separate test for min-max
// notation as it was tested in several previous tests.
+// Note this test also verifies that subnets can be configured without
+// prefix delegation pools.
TEST_F(Dhcp6ParserTest, poolPrefixLen) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -560,6 +909,294 @@ TEST_F(Dhcp6ParserTest, poolPrefixLen) {
EXPECT_EQ(4000, subnet->getValid());
}
+// Goal of this test is to verify the basic parsing of a prefix delegation
+// pool. It uses a single, valid pd pool.
+TEST_F(Dhcp6ParserTest, pdPoolBasics) {
+
+ ConstElementPtr x;
+
+ // Define a single valid pd pool.
+ string config =
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 64, "
+ " \"delegated-len\": 128"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }";
+
+ // Convert the JSON string into Elements.
+ ElementPtr json;
+ ASSERT_NO_THROW(json = Element::fromJSON(config));
+
+ // Verify that DHCP6 configuration processing succeeds.
+ // Returned value must be non-empty ConstElementPtr to config result.
+ // rcode should be 0 which indicates successful configuration processing.
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ EXPECT_EQ(0, rcode_);
+
+ // Test that we can retrieve the subnet.
+ Subnet6Ptr subnet = CfgMgr::
+ instance().getSubnet6(IOAddress("2001:db8:1::5"));
+
+ ASSERT_TRUE(subnet);
+
+ // Fetch the collection of PD pools. It should have 1 entry.
+ PoolCollection pc;
+ ASSERT_NO_THROW(pc = subnet->getPools(Lease::TYPE_PD));
+ EXPECT_EQ(1, pc.size());
+
+ // Get a pointer to the pd pool instance, and verify its contents.
+ Pool6Ptr p6;
+ ASSERT_NO_THROW(p6 = boost::dynamic_pointer_cast<Pool6>(pc[0]));
+ ASSERT_TRUE(p6);
+ EXPECT_EQ("2001:db8:1::", p6->getFirstAddress().toText());
+ EXPECT_EQ(128, p6->getLength());
+
+ // prefix-len is not directly accessible after pool construction, so
+ // verify that it was interpreted correctly by checking the last address
+ // value.
+ isc::asiolink::IOAddress prefixAddress("2001:db8:1::");
+ EXPECT_EQ(lastAddrInPrefix(prefixAddress, 64), p6->getLastAddress());
+}
+
+// Goal of this test is verify that a list of PD pools can be configured.
+// It also verifies that a subnet may be configured with both regular pools
+// and pd pools.
+TEST_F(Dhcp6ParserTest, pdPoolList) {
+
+ ConstElementPtr x;
+
+ const char* prefixes[] = {
+ "2001:db8:1:1::",
+ "2001:db8:1:2::",
+ "2001:db8:1:3::"
+ };
+
+ string config =
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1:04::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/40\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1:01::\", "
+ " \"prefix-len\": 72, "
+ " \"delegated-len\": 80"
+ " },"
+ " { \"prefix\": \"2001:db8:1:02::\", "
+ " \"prefix-len\": 72, "
+ " \"delegated-len\": 88"
+ " },"
+ " { \"prefix\": \"2001:db8:1:03::\", "
+ " \"prefix-len\": 72, "
+ " \"delegated-len\": 96"
+ " }"
+ "],"
+ "\"valid-lifetime\": 4000 }"
+ "] }";
+
+ // Convert the JSON string into Elements.
+ ElementPtr json = Element::fromJSON(config);
+ ASSERT_NO_THROW(json = Element::fromJSON(config));
+
+ // Verify that DHCP6 configuration processing succeeds.
+ // Returned value must be non-empty ConstElementPtr to config result.
+ // rcode should be 0 which indicates successful configuration processing.
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ EXPECT_EQ(0, rcode_);
+
+ // Test that we can retrieve the subnet.
+ Subnet6Ptr subnet = CfgMgr::
+ instance().getSubnet6(IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(subnet);
+
+ // Fetch the collection of NA pools. It should have 1 entry.
+ PoolCollection pc;
+ ASSERT_NO_THROW(pc = subnet->getPools(Lease::TYPE_NA));
+ EXPECT_EQ(1, pc.size());
+
+ // Fetch the collection of PD pools. It should have 3 entries.
+ ASSERT_NO_THROW(pc = subnet->getPools(Lease::TYPE_PD));
+ EXPECT_EQ(3, pc.size());
+
+ // Loop through the pools and verify their contents.
+ for (int i = 0; i < 3; i++) {
+ Pool6Ptr p6;
+ ASSERT_NO_THROW(p6 = boost::dynamic_pointer_cast<Pool6>(pc[i]));
+ ASSERT_TRUE(p6);
+ EXPECT_EQ(prefixes[i], p6->getFirstAddress().toText());
+ EXPECT_EQ((80 + (i * 8)), p6->getLength());
+ }
+}
+
+// Goal of this test is to verify the a whole prefix can be delegated and that
+// a whole subnet can be delegated.
+TEST_F(Dhcp6ParserTest, subnetAndPrefixDelegated) {
+
+ ConstElementPtr x;
+
+ // Define a single valid pd pool.
+ string config =
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 64, "
+ " \"delegated-len\": 64"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }";
+
+ // Convert the JSON string into Elements.
+ ElementPtr json;
+ ASSERT_NO_THROW(json = Element::fromJSON(config));
+
+ // Verify that DHCP6 configuration processing succeeds.
+ // Returned value must be non-empty ConstElementPtr to config result.
+ // rcode should be 0 which indicates successful configuration processing.
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ EXPECT_EQ(0, rcode_);
+
+ // Test that we can retrieve the subnet.
+ Subnet6Ptr subnet = CfgMgr::
+ instance().getSubnet6(IOAddress("2001:db8:1::5"));
+
+ ASSERT_TRUE(subnet);
+
+ // Fetch the collection of PD pools. It should have 1 entry.
+ PoolCollection pc;
+ ASSERT_NO_THROW(pc = subnet->getPools(Lease::TYPE_PD));
+ EXPECT_EQ(1, pc.size());
+
+ // Get a pointer to the pd pool instance, and verify its contents.
+ Pool6Ptr p6;
+ ASSERT_NO_THROW(p6 = boost::dynamic_pointer_cast<Pool6>(pc[0]));
+ ASSERT_TRUE(p6);
+ EXPECT_EQ("2001:db8:1::", p6->getFirstAddress().toText());
+ EXPECT_EQ(64, p6->getLength());
+
+ // prefix-len is not directly accessible after pool construction, so
+ // verify that it was interpreted correctly by checking the last address
+ // value.
+ isc::asiolink::IOAddress prefixAddress("2001:db8:1::");
+ EXPECT_EQ(lastAddrInPrefix(prefixAddress, 64), p6->getLastAddress());
+}
+
+
+// Goal of this test is check for proper handling of invalid prefix delegation
+// pool configuration. It uses an array of invalid configurations to check
+// a variety of configuration errors.
+TEST_F(Dhcp6ParserTest, invalidPdPools) {
+
+ ConstElementPtr x;
+
+ const char *config[] = {
+ // No prefix.
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { "
+ " \"prefix-len\": 64, "
+ " \"delegated-len\": 128"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }",
+ // No prefix-len.
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1::\", "
+ " \"delegated-len\": 128"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }",
+ // No delegated-len.
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 64 "
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }",
+ // Delegated length is too short.
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:1::\", "
+ " \"prefix-len\": 128, "
+ " \"delegated-len\": 64"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }",
+ // Pool is not within the subnet.
+ "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"subnet\": \"2001:db8:1::/64\","
+ " \"pd-pools\": ["
+ " { \"prefix\": \"2001:db8:77::\", "
+ " \"prefix-len\": 64, "
+ " \"delegated-len\": 128"
+ " } ],"
+ "\"valid-lifetime\": 4000 }"
+ "] }"
+ };
+
+ ElementPtr json;
+ int num_msgs = sizeof(config)/sizeof(char*);
+ for (int i = 0; i < num_msgs; i++) {
+ // Convert JSON string to Elements.
+ ASSERT_NO_THROW(json = Element::fromJSON(config[i]));
+
+ // Configuration processing should fail without a throw.
+ ASSERT_NO_THROW(x = configureDhcp6Server(srv_, json));
+
+ // Returned value must be non-empty ConstElementPtr to config result.
+ // rcode should be 1 which indicates configuration error.
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ EXPECT_EQ(1, rcode_);
+ }
+}
+
// The goal of this test is to check whether an option definition
// that defines an option carrying an IPv6 address can be created.
TEST_F(Dhcp6ParserTest, optionDefIpv6Address) {
@@ -1054,7 +1691,7 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) {
// configuration does not include options configuration.
TEST_F(Dhcp6ParserTest, optionDataDefaults) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
@@ -1136,7 +1773,7 @@ TEST_F(Dhcp6ParserTest, optionDataTwoSpaces) {
// The definition is not required for the option that
// belongs to the 'dhcp6' option space as it is the
// standard option.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1214,7 +1851,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
// at the very end (when all other parameters are configured).
// Starting stage 1. Configure sub-options and their definitions.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1263,7 +1900,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
// the configuration from the stage 2 is repeated because BIND
// configuration manager sends whole configuration for the lists
// where at least one element is being modified or added.
- config = "{ \"interface\": [ \"all\" ],"
+ config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1357,7 +1994,7 @@ TEST_F(Dhcp6ParserTest, optionDataEncapsulate) {
// for multiple subnets.
TEST_F(Dhcp6ParserTest, optionDataInMultipleSubnets) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -1592,6 +2229,121 @@ TEST_F(Dhcp6ParserTest, stdOptionData) {
EXPECT_EQ(1516, optionIA->getT2());
}
+// This test checks if vendor options can be specified in the config file
+// (in hex format), and later retrieved from configured subnet
+TEST_F(Dhcp6ParserTest, vendorOptionsHex) {
+
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"option-one\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 100,"
+ " \"data\": \"AB CDEF0105\","
+ " \"csv-format\": False"
+ " },"
+ " {"
+ " \"name\": \"option-two\","
+ " \"space\": \"vendor-1234\","
+ " \"code\": 100,"
+ " \"data\": \"1234\","
+ " \"csv-format\": False"
+ " } ],"
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\""
+ " } ]"
+ "}";
+
+ ConstElementPtr status;
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Options should be now available for the subnet.
+ Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(subnet);
+
+ // Try to get the option from the vendor space 4491
+ Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(4491, 100);
+ ASSERT_TRUE(desc1.option);
+ EXPECT_EQ(100, desc1.option->getType());
+ // Try to get the option from the vendor space 1234
+ Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(1234, 100);
+ ASSERT_TRUE(desc2.option);
+ EXPECT_EQ(100, desc1.option->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ Subnet::OptionDescriptor desc3 = subnet->getVendorOptionDescriptor(5678, 38);
+ ASSERT_FALSE(desc3.option);
+}
+
+// This test checks if vendor options can be specified in the config file,
+// (in csv format), and later retrieved from configured subnet
+TEST_F(Dhcp6ParserTest, vendorOptionsCsv) {
+
+ // This configuration string is to configure two options
+ // sharing the code 1 and belonging to the different vendor spaces.
+ // (different vendor-id values).
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 100,"
+ " \"data\": \"this is a string vendor-opt\","
+ " \"csv-format\": True"
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"vendor-4491\","
+ " \"encapsulate\": \"\""
+ " } ],"
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/80\" ],"
+ " \"subnet\": \"2001:db8:1::/64\""
+ " } ]"
+ "}";
+
+ ConstElementPtr status;
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+ ASSERT_TRUE(status);
+ checkResult(status, 0);
+
+ // Options should be now available for the subnet.
+ Subnet6Ptr subnet = CfgMgr::instance().getSubnet6(IOAddress("2001:db8:1::5"));
+ ASSERT_TRUE(subnet);
+
+ // Try to get the option from the vendor space 4491
+ Subnet::OptionDescriptor desc1 = subnet->getVendorOptionDescriptor(4491, 100);
+ ASSERT_TRUE(desc1.option);
+ EXPECT_EQ(100, desc1.option->getType());
+
+ // Try to get the non-existing option from the non-existing
+ // option space and expect that option is not returned.
+ Subnet::OptionDescriptor desc2 = subnet->getVendorOptionDescriptor(5678, 100);
+ ASSERT_FALSE(desc2.option);
+}
+
+/// @todo add tests similar to vendorOptionsCsv and vendorOptionsHex, but for
+/// vendor options defined in a subnet.
+
// The goal of this test is to verify that the standard option can
// be configured to encapsulate multiple other options.
TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
@@ -1600,7 +2352,7 @@ TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
// In the first stahe we create definitions of suboptions
// that we will add to the base option.
// Let's create some dummy options: foo and foo2.
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1653,7 +2405,7 @@ TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
// We add our dummy options to this option space and thus
// they should be included as sub-options in the 'vendor-opts'
// option.
- config = "{ \"interface\": [ \"all\" ],"
+ config = "{ \"interfaces\": [ \"*\" ],"
"\"rebind-timer\": 2000,"
"\"renew-timer\": 1000,"
"\"option-data\": [ {"
@@ -1752,4 +2504,225 @@ TEST_F(Dhcp6ParserTest, stdOptionDataEncapsulate) {
EXPECT_FALSE(desc.option->getOption(112));
}
+// Tests of the hooks libraries configuration. All tests have the pre-
+// condition (checked in the test fixture's SetUp() method) that no hooks
+// libraries are loaded at the start of the tests.
+
+
+// Helper function to return a configuration containing an arbitrary number
+// of hooks libraries.
+std::string
+buildHooksLibrariesConfig(const std::vector<std::string>& libraries) {
+ const string quote("\"");
+
+ // Create the first part of the configuration string.
+ string config =
+ "{ \"interfaces\": [ \"all\" ],"
+ "\"hooks-libraries\": [";
+
+ // Append the libraries (separated by commas if needed)
+ for (int i = 0; i < libraries.size(); ++i) {
+ if (i > 0) {
+ config += string(", ");
+ }
+ config += (quote + libraries[i] + quote);
+ }
+
+ // Append the remainder of the configuration.
+ config += string(
+ "],"
+ "\"rebind-timer\": 2000,"
+ "\"renew-timer\": 1000,"
+ "\"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"vendor-opts-space\","
+ " \"code\": 110,"
+ " \"data\": \"1234\","
+ " \"csv-format\": True"
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"space\": \"vendor-opts-space\","
+ " \"code\": 111,"
+ " \"data\": \"192.168.2.1\","
+ " \"csv-format\": True"
+ " } ],"
+ "\"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 110,"
+ " \"type\": \"uint32\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"vendor-opts-space\","
+ " \"encapsulate\": \"\""
+ " },"
+ " {"
+ " \"name\": \"foo2\","
+ " \"code\": 111,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"vendor-opts-space\","
+ " \"encapsulate\": \"\""
+ " } ]"
+ "}");
+
+ return (config);
+}
+
+// Convenience function for creating hooks library configuration with one or
+// two character string constants.
+std::string
+buildHooksLibrariesConfig(const char* library1 = NULL,
+ const char* library2 = NULL) {
+ std::vector<std::string> libraries;
+ if (library1 != NULL) {
+ libraries.push_back(string(library1));
+ if (library2 != NULL) {
+ libraries.push_back(string(library2));
+ }
+ }
+ return (buildHooksLibrariesConfig(libraries));
+}
+
+
+// The goal of this test is to verify the configuration of hooks libraries if
+// none are specified.
+TEST_F(Dhcp6ParserTest, NoHooksLibraries) {
+ // Parse a configuration containing no names.
+ string config = buildHooksLibrariesConfig();
+ if (!executeConfiguration(config,
+ "set configuration with no hooks libraries")) {
+ FAIL() << "Unable to execute configuration";
+
+ } else {
+ // No libraries should be loaded at the end of the test.
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+ }
+}
+
+// Verify parsing fails with one library that will fail validation.
+TEST_F(Dhcp6ParserTest, InvalidLibrary) {
+ // Parse a configuration containing a failing library.
+ string config = buildHooksLibrariesConfig(NOT_PRESENT_LIBRARY);
+
+ ConstElementPtr status;
+ ElementPtr json = Element::fromJSON(config);
+ ASSERT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // The status object must not be NULL
+ ASSERT_TRUE(status);
+
+ // Returned value should not be 0
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_NE(0, rcode_);
+}
+
+// Verify the configuration of hooks libraries with two being specified.
+TEST_F(Dhcp6ParserTest, LibrariesSpecified) {
+ // Marker files should not be present.
+ EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Set up the configuration with two libraries and load them.
+ string config = buildHooksLibrariesConfig(CALLOUT_LIBRARY_1,
+ CALLOUT_LIBRARY_2);
+ ASSERT_TRUE(executeConfiguration(config,
+ "load two valid libraries"));
+
+ // Expect two libraries to be loaded in the correct order (load marker file
+ // is present, no unload marker file).
+ std::vector<std::string> libraries = HooksManager::getLibraryNames();
+ ASSERT_EQ(2, libraries.size());
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Unload the libraries. The load file should not have changed, but
+ // the unload one should indicate the unload() functions have been run.
+ config = buildHooksLibrariesConfig();
+ ASSERT_TRUE(executeConfiguration(config, "unloading libraries"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+
+ // Expect the hooks system to say that none are loaded.
+ libraries = HooksManager::getLibraryNames();
+ EXPECT_TRUE(libraries.empty());
+
+}
+
+
+// This test verifies that it is possible to select subset of interfaces on
+// which server should listen.
+TEST_F(Dhcp6ParserTest, selectedInterfaces) {
+
+ // Make sure there is no garbage interface configuration in the CfgMgr.
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+ ConstElementPtr status;
+
+ string config = "{ \"interfaces\": [ \"eth0\", \"eth1\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // returned value must be 1 (values error)
+ // as the pool does not belong to that subnet
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(0, rcode_);
+
+ // eth0 and eth1 were explicitly selected. eth2 was not.
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+ EXPECT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+// This test verifies that it is possible to configure the server to listen on
+// all interfaces.
+TEST_F(Dhcp6ParserTest, allInterfaces) {
+
+ // Make sure there is no garbage interface configuration in the CfgMgr.
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth2"));
+
+ ConstElementPtr status;
+
+ // This configuration specifies two interfaces on which server should listen
+ // bu also includes keyword 'all'. This keyword switches server into the
+ // mode when it listens on all interfaces regardless of what interface names
+ // were specified in the "interfaces" parameter.
+ string config = "{ \"interfaces\": [ \"eth0\", \"eth1\", \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"valid-lifetime\": 4000 }";
+
+
+ ElementPtr json = Element::fromJSON(config);
+
+ EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
+
+ // returned value must be 1 (values error)
+ // as the pool does not belong to that subnet
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ EXPECT_EQ(0, rcode_);
+
+ // All interfaces should be now active.
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth0"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth1"));
+ EXPECT_TRUE(CfgMgr::instance().isActiveIface("eth2"));
+}
+
+
};
diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
index 9209de6..6dfb981 100644
--- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
+++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
@@ -14,30 +14,37 @@
#include <config.h>
+#include <config/ccsession.h>
#include <dhcp/dhcp6.h>
#include <dhcp6/ctrl_dhcp6_srv.h>
-#include <config/ccsession.h>
+#include <hooks/hooks_manager.h>
+
+#include "marker_file.h"
+#include "test_libraries.h"
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
-#include <iostream>
#include <fstream>
+#include <iostream>
#include <sstream>
#include <arpa/inet.h>
+#include <unistd.h>
using namespace std;
using namespace isc;
-using namespace isc::dhcp;
using namespace isc::asiolink;
-using namespace isc::data;
using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+using namespace isc::hooks;
namespace {
class NakedControlledDhcpv6Srv: public ControlledDhcpv6Srv {
- // "naked" DHCPv6 server, exposes internal fields
+ // "Naked" DHCPv6 server, exposes internal fields
public:
NakedControlledDhcpv6Srv():ControlledDhcpv6Srv(DHCP6_SERVER_PORT + 10000) { }
};
@@ -45,10 +52,25 @@ public:
class CtrlDhcpv6SrvTest : public ::testing::Test {
public:
CtrlDhcpv6SrvTest() {
+ reset();
}
~CtrlDhcpv6SrvTest() {
+ reset();
};
+
+ /// @brief Reset hooks data
+ ///
+ /// Resets the data for the hooks-related portion of the test by ensuring
+ /// that no libraries are loaded and that any marker files are deleted.
+ void reset() {
+ // Unload any previously-loaded libraries.
+ HooksManager::unloadLibraries();
+
+ // Get rid of any marker files.
+ static_cast<void>(unlink(LOAD_MARKER_FILE));
+ static_cast<void>(unlink(UNLOAD_MARKER_FILE));
+ }
};
TEST_F(CtrlDhcpv6SrvTest, commands) {
@@ -62,12 +84,12 @@ TEST_F(CtrlDhcpv6SrvTest, commands) {
ElementPtr params(new isc::data::MapElement());
int rcode = -1;
- // case 1: send bogus command
+ // Case 1: send bogus command
ConstElementPtr result = ControlledDhcpv6Srv::execDhcpv6ServerCommand("blah", params);
ConstElementPtr comment = parseAnswer(rcode, result);
EXPECT_EQ(1, rcode); // expect failure (no such command as blah)
- // case 2: send shutdown command without any parameters
+ // Case 2: send shutdown command without any parameters
result = ControlledDhcpv6Srv::execDhcpv6ServerCommand("shutdown", params);
comment = parseAnswer(rcode, result);
EXPECT_EQ(0, rcode); // expect success
@@ -76,10 +98,54 @@ TEST_F(CtrlDhcpv6SrvTest, commands) {
ConstElementPtr x(new isc::data::IntElement(pid));
params->set("pid", x);
- // case 3: send shutdown command with 1 parameter: pid
+ // Case 3: send shutdown command with 1 parameter: pid
result = ControlledDhcpv6Srv::execDhcpv6ServerCommand("shutdown", params);
comment = parseAnswer(rcode, result);
EXPECT_EQ(0, rcode); // Expect success
}
-} // end of anonymous namespace
+// Check that the "libreload" command will reload libraries
+
+TEST_F(CtrlDhcpv6SrvTest, libreload) {
+ // Ensure no marker files to start with.
+ ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
+ ASSERT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Load two libraries
+ std::vector<std::string> libraries;
+ libraries.push_back(CALLOUT_LIBRARY_1);
+ libraries.push_back(CALLOUT_LIBRARY_2);
+ HooksManager::loadLibraries(libraries);
+
+ // Check they are loaded.
+ std::vector<std::string> loaded_libraries =
+ HooksManager::getLibraryNames();
+ ASSERT_TRUE(libraries == loaded_libraries);
+
+ // ... which also included checking that the marker file created by the
+ // load functions exists and holds the correct value (of "12" - the
+ // first library appends "1" to the file, the second appends "2"). Also
+ // check that the unload marker file does not yet exist.
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "12"));
+ EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
+
+ // Now execute the "libreload" command. This should cause the libraries
+ // to unload and to reload.
+
+ // Use empty parameters list
+ ElementPtr params(new isc::data::MapElement());
+ int rcode = -1;
+
+ ConstElementPtr result =
+ ControlledDhcpv6Srv::execDhcpv6ServerCommand("libreload", params);
+ ConstElementPtr comment = parseAnswer(rcode, result);
+ EXPECT_EQ(0, rcode); // Expect success
+
+ // Check that the libraries have unloaded and reloaded. The libraries are
+ // unloaded in the reverse order to which they are loaded. When they load,
+ // they should append information to the loading marker file.
+ EXPECT_TRUE(checkMarkerFile(UNLOAD_MARKER_FILE, "21"));
+ EXPECT_TRUE(checkMarkerFile(LOAD_MARKER_FILE, "1212"));
+}
+
+} // End of anonymous namespace
diff --git a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
index a83b9e8..e8f5e79 100644
--- a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
+++ b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc
@@ -15,24 +15,30 @@
#include <config.h>
#include <asiolink/io_address.h>
-#include <config/ccsession.h>
#include <dhcp/dhcp6.h>
#include <dhcp/duid.h>
#include <dhcp/option.h>
-#include <dhcp/option_custom.h>
#include <dhcp/option6_addrlst.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/iface_mgr.h>
#include <dhcp6/config_parser.h>
-#include <dhcp6/dhcp6_srv.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/docsis3_option_defs.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/lease_mgr_factory.h>
#include <dhcpsrv/utils.h>
#include <util/buffer.h>
#include <util/range_utilities.h>
+#include <hooks/server_hooks.h>
+#include <dhcp6/tests/dhcp6_test_utils.h>
+#include <boost/pointer_cast.hpp>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
#include <unistd.h>
@@ -41,296 +47,27 @@
#include <sstream>
using namespace isc;
+using namespace isc::test;
using namespace isc::asiolink;
-using namespace isc::config;
-using namespace isc::data;
using namespace isc::dhcp;
using namespace isc::util;
+using namespace isc::hooks;
using namespace std;
-// namespace has to be named, because friends are defined in Dhcpv6Srv class
-// Maybe it should be isc::test?
namespace {
-class NakedDhcpv6Srv: public Dhcpv6Srv {
- // "naked" Interface Manager, exposes internal members
-public:
- NakedDhcpv6Srv(uint16_t port) : Dhcpv6Srv(port) {
- // Open the "memfile" database for leases
- std::string memfile = "type=memfile";
- LeaseMgrFactory::create(memfile);
- }
-
- virtual ~NakedDhcpv6Srv() {
- // Close the lease database
- LeaseMgrFactory::destroy();
- }
-
- using Dhcpv6Srv::processSolicit;
- using Dhcpv6Srv::processRequest;
- using Dhcpv6Srv::processRenew;
- using Dhcpv6Srv::processRelease;
- using Dhcpv6Srv::createStatusCode;
- using Dhcpv6Srv::selectSubnet;
- using Dhcpv6Srv::sanityCheck;
- using Dhcpv6Srv::loadServerID;
- using Dhcpv6Srv::writeServerID;
-};
-
-static const char* DUID_FILE = "server-id-test.txt";
-
-// test fixture for any tests requiring blank/empty configuration
-// serves as base class for additional tests
-class NakedDhcpv6SrvTest : public ::testing::Test {
-public:
-
- NakedDhcpv6SrvTest() : rcode_(-1) {
- // it's ok if that fails. There should not be such a file anyway
- unlink(DUID_FILE);
- }
-
- // Generate IA_NA option with specified parameters
- boost::shared_ptr<Option6IA> generateIA(uint32_t iaid, uint32_t t1, uint32_t t2) {
- boost::shared_ptr<Option6IA> ia =
- boost::shared_ptr<Option6IA>(new Option6IA(D6O_IA_NA, iaid));
- ia->setT1(t1);
- ia->setT2(t2);
- return (ia);
- }
-
- // Generate client-id option
- OptionPtr generateClientId(size_t duid_size = 32) {
-
- OptionBuffer clnt_duid(duid_size);
- for (int i = 0; i < duid_size; i++) {
- clnt_duid[i] = 100 + i;
- }
-
- duid_ = DuidPtr(new DUID(clnt_duid));
-
- return (OptionPtr(new Option(Option::V6, D6O_CLIENTID,
- clnt_duid.begin(),
- clnt_duid.begin() + duid_size)));
- }
-
- // Checks if server response (ADVERTISE or REPLY) includes proper server-id.
- void checkServerId(const Pkt6Ptr& rsp, const OptionPtr& expected_srvid) {
- // check that server included its server-id
- OptionPtr tmp = rsp->getOption(D6O_SERVERID);
- EXPECT_EQ(tmp->getType(), expected_srvid->getType() );
- ASSERT_EQ(tmp->len(), expected_srvid->len() );
- EXPECT_TRUE(tmp->getData() == expected_srvid->getData());
- }
-
- // Checks if server response (ADVERTISE or REPLY) includes proper client-id.
- void checkClientId(const Pkt6Ptr& rsp, const OptionPtr& expected_clientid) {
- // check that server included our own client-id
- OptionPtr tmp = rsp->getOption(D6O_CLIENTID);
- ASSERT_TRUE(tmp);
- EXPECT_EQ(expected_clientid->getType(), tmp->getType());
- ASSERT_EQ(expected_clientid->len(), tmp->len());
-
- // check that returned client-id is valid
- EXPECT_TRUE(expected_clientid->getData() == tmp->getData());
- }
-
- // Checks if server response is a NAK
- void checkNakResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type,
- uint32_t expected_transid,
- uint16_t expected_status_code) {
- // Check if we get response at all
- checkResponse(rsp, expected_message_type, expected_transid);
-
- // Check that IA_NA was returned
- OptionPtr option_ia_na = rsp->getOption(D6O_IA_NA);
- ASSERT_TRUE(option_ia_na);
-
- // check that the status is no address available
- boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(option_ia_na);
- ASSERT_TRUE(ia);
-
- checkIA_NAStatusCode(ia, expected_status_code);
- }
-
- // Checks that server rejected IA_NA, i.e. that it has no addresses and
- // that expected status code really appears there. In some limited cases
- // (reply to RELEASE) it may be used to verify positive case, where
- // IA_NA response is expected to not include address.
- //
- // Status code indicates type of error encountered (in theory it can also
- // indicate success, but servers typically don't send success status
- // as this is the default result and it saves bandwidth)
- void checkIA_NAStatusCode(const boost::shared_ptr<Option6IA>& ia,
- uint16_t expected_status_code) {
- // Make sure there is no address assigned.
- EXPECT_FALSE(ia->getOption(D6O_IAADDR));
-
- // T1, T2 should be zeroed
- EXPECT_EQ(0, ia->getT1());
- EXPECT_EQ(0, ia->getT2());
-
- OptionCustomPtr status =
- boost::dynamic_pointer_cast<OptionCustom>(ia->getOption(D6O_STATUS_CODE));
-
- // It is ok to not include status success as this is the default behavior
- if (expected_status_code == STATUS_Success && !status) {
- return;
- }
-
- EXPECT_TRUE(status);
-
- if (status) {
- // We don't have dedicated class for status code, so let's just interpret
- // first 2 bytes as status. Remainder of the status code option content is
- // just a text explanation what went wrong.
- EXPECT_EQ(static_cast<uint16_t>(expected_status_code),
- status->readInteger<uint16_t>(0));
- }
- }
-
- void checkMsgStatusCode(const Pkt6Ptr& msg, uint16_t expected_status) {
- OptionCustomPtr status =
- boost::dynamic_pointer_cast<OptionCustom>(msg->getOption(D6O_STATUS_CODE));
-
- // It is ok to not include status success as this is the default behavior
- if (expected_status == STATUS_Success && !status) {
- return;
- }
-
- EXPECT_TRUE(status);
- if (status) {
- // We don't have dedicated class for status code, so let's just interpret
- // first 2 bytes as status. Remainder of the status code option content is
- // just a text explanation what went wrong.
- EXPECT_EQ(static_cast<uint16_t>(expected_status),
- status->readInteger<uint16_t>(0));
- }
- }
-
- // Basic checks for generated response (message type and transaction-id).
- void checkResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type,
- uint32_t expected_transid) {
- ASSERT_TRUE(rsp);
- EXPECT_EQ(expected_message_type, rsp->getType());
- EXPECT_EQ(expected_transid, rsp->getTransid());
- }
-
- virtual ~NakedDhcpv6SrvTest() {
- // Let's clean up if there is such a file.
- unlink(DUID_FILE);
- };
-
- // A DUID used in most tests (typically as client-id)
- DuidPtr duid_;
-
- int rcode_;
- ConstElementPtr comment_;
-};
-
-// Provides suport for tests against a preconfigured subnet6
-// extends upon NakedDhcp6SrvTest
-class Dhcpv6SrvTest : public NakedDhcpv6SrvTest {
-public:
- /// Name of the server-id file (used in server-id tests)
-
- // these are empty for now, but let's keep them around
- Dhcpv6SrvTest() {
- subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 48, 1000,
- 2000, 3000, 4000));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:1::"), 64));
- subnet_->addPool(pool_);
-
- CfgMgr::instance().deleteSubnets6();
- CfgMgr::instance().addSubnet6(subnet_);
- }
-
- // Checks that server response (ADVERTISE or REPLY) contains proper IA_NA option
- // It returns IAADDR option for each chaining with checkIAAddr method.
- boost::shared_ptr<Option6IAAddr> checkIA_NA(const Pkt6Ptr& rsp, uint32_t expected_iaid,
- uint32_t expected_t1, uint32_t expected_t2) {
- OptionPtr tmp = rsp->getOption(D6O_IA_NA);
- // Can't use ASSERT_TRUE() in method that returns something
- if (!tmp) {
- ADD_FAILURE() << "IA_NA option not present in response";
- return (boost::shared_ptr<Option6IAAddr>());
- }
-
- boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- if (!ia) {
- ADD_FAILURE() << "IA_NA cannot convert option ptr to Option6";
- return (boost::shared_ptr<Option6IAAddr>());
- }
-
- EXPECT_EQ(expected_iaid, ia->getIAID());
- EXPECT_EQ(expected_t1, ia->getT1());
- EXPECT_EQ(expected_t2, ia->getT2());
-
- tmp = ia->getOption(D6O_IAADDR);
- boost::shared_ptr<Option6IAAddr> addr = boost::dynamic_pointer_cast<Option6IAAddr>(tmp);
- return (addr);
- }
-
- // Check that generated IAADDR option contains expected address
- // and lifetime values match the configured subnet
- void checkIAAddr(const boost::shared_ptr<Option6IAAddr>& addr,
- const IOAddress& expected_addr,
- uint32_t /* expected_preferred */,
- uint32_t /* expected_valid */) {
-
- // Check that the assigned address is indeed from the configured pool.
- // Note that when comparing addresses, we compare the textual
- // representation. IOAddress does not support being streamed to
- // an ostream, which means it can't be used in EXPECT_EQ.
- EXPECT_TRUE(subnet_->inPool(addr->getAddress()));
- EXPECT_EQ(expected_addr.toText(), addr->getAddress().toText());
- EXPECT_EQ(addr->getPreferred(), subnet_->getPreferred());
- EXPECT_EQ(addr->getValid(), subnet_->getValid());
- }
-
- // Checks if the lease sent to client is present in the database
- // and is valid when checked agasint the configured subnet
- Lease6Ptr checkLease(const DuidPtr& duid, const OptionPtr& ia_na,
- boost::shared_ptr<Option6IAAddr> addr) {
- boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(ia_na);
-
- Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(addr->getAddress());
- if (!lease) {
- cout << "Lease for " << addr->getAddress().toText()
- << " not found in the database backend.";
- return (Lease6Ptr());
- }
-
- EXPECT_EQ(addr->getAddress().toText(), lease->addr_.toText());
- EXPECT_TRUE(*lease->duid_ == *duid);
- EXPECT_EQ(ia->getIAID(), lease->iaid_);
- EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
-
- return (lease);
- }
-
- ~Dhcpv6SrvTest() {
- CfgMgr::instance().deleteSubnets6();
- };
-
- // A subnet used in most tests
- Subnet6Ptr subnet_;
-
- // A pool used in most tests
- Pool6Ptr pool_;
-};
-
// This test verifies that incoming SOLICIT can be handled properly when
-// there are no subnets configured.
+// there are no subnets configured.
//
-// This test sends a SOLICIT and the expected response
-// is an ADVERTISE with STATUS_NoAddrsAvail and no address provided in the
+// This test sends a SOLICIT and the expected response
+// is an ADVERTISE with STATUS_NoAddrsAvail and no address provided in the
// response
TEST_F(NakedDhcpv6SrvTest, SolicitNoSubnet) {
NakedDhcpv6Srv srv(0);
Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
sol->setRemoteAddr(IOAddress("fe80::abcd"));
- sol->addOption(generateIA(234, 1500, 3000));
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
OptionPtr clientid = generateClientId();
sol->addOption(clientid);
@@ -342,10 +79,10 @@ TEST_F(NakedDhcpv6SrvTest, SolicitNoSubnet) {
}
// This test verifies that incoming REQUEST can be handled properly when
-// there are no subnets configured.
+// there are no subnets configured.
//
-// This test sends a REQUEST and the expected response
-// is an REPLY with STATUS_NoAddrsAvail and no address provided in the
+// This test sends a REQUEST and the expected response
+// is an REPLY with STATUS_NoAddrsAvail and no address provided in the
// response
TEST_F(NakedDhcpv6SrvTest, RequestNoSubnet) {
NakedDhcpv6Srv srv(0);
@@ -353,7 +90,7 @@ TEST_F(NakedDhcpv6SrvTest, RequestNoSubnet) {
// Let's create a REQUEST
Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(234, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
// with a hint
IOAddress hint("2001:db8:1:1::dead:beef");
@@ -376,8 +113,8 @@ TEST_F(NakedDhcpv6SrvTest, RequestNoSubnet) {
// This test verifies that incoming RENEW can be handled properly, even when
// no subnets are configured.
//
-// This test sends a RENEW and the expected response
-// is an REPLY with STATUS_NoBinding and no address provided in the
+// This test sends a RENEW and the expected response
+// is an REPLY with STATUS_NoBinding and no address provided in the
// response
TEST_F(NakedDhcpv6SrvTest, RenewNoSubnet) {
NakedDhcpv6Srv srv(0);
@@ -391,7 +128,7 @@ TEST_F(NakedDhcpv6SrvTest, RenewNoSubnet) {
// Let's create a RENEW
Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(iaid, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
ia->addOption(renewed_addr_opt);
@@ -411,8 +148,8 @@ TEST_F(NakedDhcpv6SrvTest, RenewNoSubnet) {
// This test verifies that incoming RELEASE can be handled properly, even when
// no subnets are configured.
//
-// This test sends a RELEASE and the expected response
-// is an REPLY with STATUS_NoBinding and no address provided in the
+// This test sends a RELEASE and the expected response
+// is an REPLY with STATUS_NoBinding and no address provided in the
// response
TEST_F(NakedDhcpv6SrvTest, ReleaseNoSubnet) {
NakedDhcpv6Srv srv(0);
@@ -426,7 +163,7 @@ TEST_F(NakedDhcpv6SrvTest, ReleaseNoSubnet) {
// Let's create a RELEASE
Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RELEASE, 1234));
req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(iaid, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
OptionPtr released_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
ia->addOption(released_addr_opt);
@@ -470,7 +207,7 @@ TEST_F(Dhcpv6SrvTest, DUID) {
boost::scoped_ptr<Dhcpv6Srv> srv;
ASSERT_NO_THROW( {
- srv.reset(new Dhcpv6Srv(0));
+ srv.reset(new NakedDhcpv6Srv(0));
});
OptionPtr srvid = srv->getServerID();
@@ -545,7 +282,7 @@ TEST_F(Dhcpv6SrvTest, DUID) {
// and the requested options are actually assigned.
TEST_F(Dhcpv6SrvTest, advertiseOptions) {
ConstElementPtr x;
- string config = "{ \"interface\": [ \"all\" ],"
+ string config = "{ \"interfaces\": [ \"all\" ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
@@ -581,7 +318,7 @@ TEST_F(Dhcpv6SrvTest, advertiseOptions) {
Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
sol->setRemoteAddr(IOAddress("fe80::abcd"));
- sol->addOption(generateIA(234, 1500, 3000));
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
OptionPtr clientid = generateClientId();
sol->addOption(clientid);
@@ -665,7 +402,7 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
sol->setRemoteAddr(IOAddress("fe80::abcd"));
- sol->addOption(generateIA(234, 1500, 3000));
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
OptionPtr clientid = generateClientId();
sol->addOption(clientid);
@@ -678,9 +415,54 @@ TEST_F(Dhcpv6SrvTest, SolicitBasic) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
+ ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool
- checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
+ checkIAAddr(addr, addr->getAddress(), Lease::TYPE_NA);
+
+ // check DUIDs
+ checkServerId(reply, srv.getServerID());
+ checkClientId(reply, clientid);
+}
+
+// This test verifies that incoming SOLICIT can be handled properly, that an
+// ADVERTISE is generated, that the response has a prefix and that prefix
+// really belongs to the configured pool.
+//
+// This test sends a SOLICIT without any hint in IA_PD.
+//
+// constructed very simple SOLICIT message with:
+// - client-id option (mandatory)
+// - IA option (a request for address, without any addresses)
+//
+// expected returned ADVERTISE message:
+// - copy of client-id
+// - server-id
+// - IA that includes IAPREFIX
+TEST_F(Dhcpv6SrvTest, pdSolicitBasic) {
+
+ NakedDhcpv6Srv srv(0);
+
+ Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ sol->setRemoteAddr(IOAddress("fe80::abcd"));
+ sol->addOption(generateIA(D6O_IA_PD, 234, 1500, 3000));
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt6Ptr reply = srv.processSolicit(sol);
+
+ // check if we get response at all
+ checkResponse(reply, DHCPV6_ADVERTISE, 1234);
+
+ // check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAPrefix> prefix = checkIA_PD(reply, 234, subnet_->getT1(),
+ subnet_->getT2());
+ ASSERT_TRUE(prefix);
+
+ // Check that the assigned prefix is indeed from the configured pool
+ checkIAAddr(prefix, prefix->getAddress(), Lease::TYPE_PD);
+ EXPECT_EQ(pd_pool_->getLength(), prefix->getLength());
// check DUIDs
checkServerId(reply, srv.getServerID());
@@ -708,11 +490,11 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
// Let's create a SOLICIT
Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
sol->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(234, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
// with a valid hint
IOAddress hint("2001:db8:1:1::dead:beef");
- ASSERT_TRUE(subnet_->inPool(hint));
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, hint));
OptionPtr hint_opt(new Option6IAAddr(D6O_IAADDR, hint, 300, 500));
ia->addOption(hint_opt);
sol->addOption(ia);
@@ -731,9 +513,10 @@ TEST_F(Dhcpv6SrvTest, SolicitHint) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
+ ASSERT_TRUE(addr);
// check that we've got the address we requested
- checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
+ checkIAAddr(addr, hint, Lease::TYPE_NA);
// check DUIDs
checkServerId(reply, srv.getServerID());
@@ -761,9 +544,9 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
// Let's create a SOLICIT
Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
sol->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(234, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
IOAddress hint("2001:db8:1::cafe:babe");
- ASSERT_FALSE(subnet_->inPool(hint));
+ ASSERT_FALSE(subnet_->inPool(Lease::TYPE_NA, hint));
OptionPtr hint_opt(new Option6IAAddr(D6O_IAADDR, hint, 300, 500));
ia->addOption(hint_opt);
sol->addOption(ia);
@@ -779,10 +562,11 @@ TEST_F(Dhcpv6SrvTest, SolicitInvalidHint) {
// check that IA_NA was returned and that there's an address included
boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
subnet_->getT2());
+ ASSERT_TRUE(addr);
// Check that the assigned address is indeed from the configured pool
- checkIAAddr(addr, addr->getAddress(), subnet_->getPreferred(), subnet_->getValid());
- EXPECT_TRUE(subnet_->inPool(addr->getAddress()));
+ checkIAAddr(addr, addr->getAddress(), Lease::TYPE_NA);
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr->getAddress()));
// check DUIDs
checkServerId(reply, srv.getServerID());
@@ -810,9 +594,9 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
sol2->setRemoteAddr(IOAddress("fe80::1223"));
sol3->setRemoteAddr(IOAddress("fe80::3467"));
- sol1->addOption(generateIA(1, 1500, 3000));
- sol2->addOption(generateIA(2, 1500, 3000));
- sol3->addOption(generateIA(3, 1500, 3000));
+ sol1->addOption(generateIA(D6O_IA_NA, 1, 1500, 3000));
+ sol2->addOption(generateIA(D6O_IA_NA, 2, 1500, 3000));
+ sol3->addOption(generateIA(D6O_IA_NA, 3, 1500, 3000));
// different client-id sizes
OptionPtr clientid1 = generateClientId(12);
@@ -840,11 +624,14 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
subnet_->getT2());
boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(),
subnet_->getT2());
+ ASSERT_TRUE(addr1);
+ ASSERT_TRUE(addr2);
+ ASSERT_TRUE(addr3);
// Check that the assigned address is indeed from the configured pool
- checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid());
- checkIAAddr(addr2, addr2->getAddress(), subnet_->getPreferred(), subnet_->getValid());
- checkIAAddr(addr3, addr3->getAddress(), subnet_->getPreferred(), subnet_->getValid());
+ checkIAAddr(addr1, addr1->getAddress(), Lease::TYPE_NA);
+ checkIAAddr(addr2, addr2->getAddress(), Lease::TYPE_NA);
+ checkIAAddr(addr3, addr3->getAddress(), Lease::TYPE_NA);
// check DUIDs
checkServerId(reply1, srv.getServerID());
@@ -855,12 +642,12 @@ TEST_F(Dhcpv6SrvTest, ManySolicits) {
checkClientId(reply3, clientid3);
// Finally check that the addresses offered are different
- EXPECT_NE(addr1->getAddress().toText(), addr2->getAddress().toText());
- EXPECT_NE(addr2->getAddress().toText(), addr3->getAddress().toText());
- EXPECT_NE(addr3->getAddress().toText(), addr1->getAddress().toText());
- cout << "Offered address to client1=" << addr1->getAddress().toText() << endl;
- cout << "Offered address to client2=" << addr2->getAddress().toText() << endl;
- cout << "Offered address to client3=" << addr3->getAddress().toText() << endl;
+ EXPECT_NE(addr1->getAddress(), addr2->getAddress());
+ EXPECT_NE(addr2->getAddress(), addr3->getAddress());
+ EXPECT_NE(addr3->getAddress(), addr1->getAddress());
+ cout << "Offered address to client1=" << addr1->getAddress() << endl;
+ cout << "Offered address to client2=" << addr2->getAddress() << endl;
+ cout << "Offered address to client3=" << addr3->getAddress() << endl;
}
// This test verifies that incoming REQUEST can be handled properly, that a
@@ -884,11 +671,11 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
// Let's create a REQUEST
Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(234, 1500, 3000);
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
// with a valid hint
IOAddress hint("2001:db8:1:1::dead:beef");
- ASSERT_TRUE(subnet_->inPool(hint));
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, hint));
OptionPtr hint_opt(new Option6IAAddr(D6O_IAADDR, hint, 300, 500));
ia->addOption(hint_opt);
req->addOption(ia);
@@ -908,11 +695,13 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
ASSERT_TRUE(tmp);
// check that IA_NA was returned and that there's an address included
- boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234, subnet_->getT1(),
- subnet_->getT2());
+ boost::shared_ptr<Option6IAAddr> addr = checkIA_NA(reply, 234,
+ subnet_->getT1(),
+ subnet_->getT2());
+ ASSERT_TRUE(addr);
// check that we've got the address we requested
- checkIAAddr(addr, hint, subnet_->getPreferred(), subnet_->getValid());
+ checkIAAddr(addr, hint, Lease::TYPE_NA);
// check DUIDs
checkServerId(reply, srv.getServerID());
@@ -924,6 +713,71 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
LeaseMgrFactory::instance().deleteLease(addr->getAddress());
}
+// This test verifies that incoming REQUEST can be handled properly, that a
+// REPLY is generated, that the response has a prefix and that prefix
+// really belongs to the configured pool.
+//
+// This test sends a REQUEST with IA_PD that contains a valid hint.
+//
+// constructed very simple REQUEST message with:
+// - client-id option (mandatory)
+// - IA option (a request for address, with an address that belongs to the
+// configured pool, i.e. is valid as hint)
+//
+// expected returned REPLY message:
+// - copy of client-id
+// - server-id
+// - IA that includes IAPREFIX
+TEST_F(Dhcpv6SrvTest, pdRequestBasic) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's create a REQUEST
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_PD, 234, 1500, 3000);
+
+ // with a valid hint
+ IOAddress hint("2001:db8:1:2:f::");
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_PD, hint));
+ OptionPtr hint_opt(new Option6IAPrefix(D6O_IAPREFIX, hint, 64, 300, 500));
+ ia->addOption(hint_opt);
+ req->addOption(ia);
+ OptionPtr clientid = generateClientId();
+ req->addOption(clientid);
+
+ // server-id is mandatory in REQUEST
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRequest(req);
+
+ // check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+
+ OptionPtr tmp = reply->getOption(D6O_IA_PD);
+ ASSERT_TRUE(tmp);
+
+ // check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAPrefix> prf = checkIA_PD(reply, 234,
+ subnet_->getT1(),
+ subnet_->getT2());
+ ASSERT_TRUE(prf);
+
+ // check that we've got the address we requested
+ checkIAAddr(prf, hint, Lease::TYPE_PD);
+ EXPECT_EQ(pd_pool_->getLength(), prf->getLength());
+
+ // check DUIDs
+ checkServerId(reply, srv.getServerID());
+ checkClientId(reply, clientid);
+
+ // check that the lease is really in the database
+ Lease6Ptr l = checkPdLease(duid_, reply->getOption(D6O_IA_PD), prf);
+ EXPECT_TRUE(l);
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(prf->getAddress()));
+}
+
// This test checks that the server is offering different addresses to different
// clients in REQUEST. Please note that ADVERTISE is not a guarantee that such
// and address will be assigned. Had the pool was very small and contained only
@@ -934,6 +788,8 @@ TEST_F(Dhcpv6SrvTest, RequestBasic) {
TEST_F(Dhcpv6SrvTest, ManyRequests) {
NakedDhcpv6Srv srv(0);
+ ASSERT_TRUE(subnet_);
+
Pkt6Ptr req1 = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 1234));
Pkt6Ptr req2 = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 2345));
Pkt6Ptr req3 = Pkt6Ptr(new Pkt6(DHCPV6_REQUEST, 3456));
@@ -942,9 +798,9 @@ TEST_F(Dhcpv6SrvTest, ManyRequests) {
req2->setRemoteAddr(IOAddress("fe80::1223"));
req3->setRemoteAddr(IOAddress("fe80::3467"));
- req1->addOption(generateIA(1, 1500, 3000));
- req2->addOption(generateIA(2, 1500, 3000));
- req3->addOption(generateIA(3, 1500, 3000));
+ req1->addOption(generateIA(D6O_IA_NA, 1, 1500, 3000));
+ req2->addOption(generateIA(D6O_IA_NA, 2, 1500, 3000));
+ req3->addOption(generateIA(D6O_IA_NA, 3, 1500, 3000));
// different client-id sizes
OptionPtr clientid1 = generateClientId(12);
@@ -978,10 +834,14 @@ TEST_F(Dhcpv6SrvTest, ManyRequests) {
boost::shared_ptr<Option6IAAddr> addr3 = checkIA_NA(reply3, 3, subnet_->getT1(),
subnet_->getT2());
+ ASSERT_TRUE(addr1);
+ ASSERT_TRUE(addr2);
+ ASSERT_TRUE(addr3);
+
// Check that the assigned address is indeed from the configured pool
- checkIAAddr(addr1, addr1->getAddress(), subnet_->getPreferred(), subnet_->getValid());
- checkIAAddr(addr2, addr2->getAddress(), subnet_->getPreferred(), subnet_->getValid());
- checkIAAddr(addr3, addr3->getAddress(), subnet_->getPreferred(), subnet_->getValid());
+ checkIAAddr(addr1, addr1->getAddress(), Lease::TYPE_NA);
+ checkIAAddr(addr2, addr2->getAddress(), Lease::TYPE_NA);
+ checkIAAddr(addr3, addr3->getAddress(), Lease::TYPE_NA);
// check DUIDs
checkServerId(reply1, srv.getServerID());
@@ -992,12 +852,12 @@ TEST_F(Dhcpv6SrvTest, ManyRequests) {
checkClientId(reply3, clientid3);
// Finally check that the addresses offered are different
- EXPECT_NE(addr1->getAddress().toText(), addr2->getAddress().toText());
- EXPECT_NE(addr2->getAddress().toText(), addr3->getAddress().toText());
- EXPECT_NE(addr3->getAddress().toText(), addr1->getAddress().toText());
- cout << "Assigned address to client1=" << addr1->getAddress().toText() << endl;
- cout << "Assigned address to client2=" << addr2->getAddress().toText() << endl;
- cout << "Assigned address to client3=" << addr3->getAddress().toText() << endl;
+ EXPECT_NE(addr1->getAddress(), addr2->getAddress());
+ EXPECT_NE(addr2->getAddress(), addr3->getAddress());
+ EXPECT_NE(addr3->getAddress(), addr1->getAddress());
+ cout << "Assigned address to client1=" << addr1->getAddress() << endl;
+ cout << "Assigned address to client2=" << addr2->getAddress() << endl;
+ cout << "Assigned address to client3=" << addr3->getAddress() << endl;
}
// This test verifies that incoming (positive) RENEW can be handled properly, that a
@@ -1007,92 +867,29 @@ TEST_F(Dhcpv6SrvTest, ManyRequests) {
// expected:
// - returned REPLY message has copy of client-id
// - returned REPLY message has server-id
-// - returned REPLY message has IA that includes IAADDR
+// - returned REPLY message has IA_NA that includes IAADDR
// - lease is actually renewed in LeaseMgr
-TEST_F(Dhcpv6SrvTest, RenewBasic) {
- NakedDhcpv6Srv srv(0);
-
- const IOAddress addr("2001:db8:1:1::cafe:babe");
- const uint32_t iaid = 234;
-
- // Generate client-id also duid_
- OptionPtr clientid = generateClientId();
-
- // Check that the address we are about to use is indeed in pool
- ASSERT_TRUE(subnet_->inPool(addr));
-
- // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
- // value on purpose. They should be updated during RENEW.
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid_, iaid,
- 501, 502, 503, 504, subnet_->getID(), 0));
- lease->cltt_ = 1234;
- ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
-
- // Check that the lease is really in the database
- Lease6Ptr l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_TRUE(l);
-
- // Check that T1, T2, preferred, valid and cltt really set and not using
- // previous (500, 501, etc.) values
- EXPECT_NE(l->t1_, subnet_->getT1());
- EXPECT_NE(l->t2_, subnet_->getT2());
- EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
- EXPECT_NE(l->valid_lft_, subnet_->getValid());
- EXPECT_NE(l->cltt_, time(NULL));
-
- // Let's create a RENEW
- Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
- req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(iaid, 1500, 3000);
-
- OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
- ia->addOption(renewed_addr_opt);
- req->addOption(ia);
- req->addOption(clientid);
-
- // Server-id is mandatory in RENEW
- req->addOption(srv.getServerID());
-
- // Pass it to the server and hope for a REPLY
- Pkt6Ptr reply = srv.processRenew(req);
-
- // Check if we get response at all
- checkResponse(reply, DHCPV6_REPLY, 1234);
-
- OptionPtr tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
-
- // Check that IA_NA was returned and that there's an address included
- boost::shared_ptr<Option6IAAddr> addr_opt = checkIA_NA(reply, 234, subnet_->getT1(),
- subnet_->getT2());
-
- // Check that we've got the address we requested
- checkIAAddr(addr_opt, addr, subnet_->getPreferred(), subnet_->getValid());
-
- // Check DUIDs
- checkServerId(reply, srv.getServerID());
- checkClientId(reply, clientid);
-
- // Check that the lease is really in the database
- l = checkLease(duid_, reply->getOption(D6O_IA_NA), addr_opt);
- ASSERT_TRUE(l);
-
- // Check that T1, T2, preferred, valid and cltt were really updated
- EXPECT_EQ(l->t1_, subnet_->getT1());
- EXPECT_EQ(l->t2_, subnet_->getT2());
- EXPECT_EQ(l->preferred_lft_, subnet_->getPreferred());
- EXPECT_EQ(l->valid_lft_, subnet_->getValid());
-
- // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
- int32_t cltt = static_cast<int32_t>(l->cltt_);
- int32_t expected = static_cast<int32_t>(time(NULL));
- // equality or difference by 1 between cltt and expected is ok.
- EXPECT_GE(1, abs(cltt - expected));
+TEST_F(Dhcpv6SrvTest, renewBasic) {
+ testRenewBasic(Lease::TYPE_NA, "2001:db8:1:1::cafe:babe",
+ "2001:db8:1:1::cafe:babe", 128);
+}
- EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr_opt->getAddress()));
+// This test verifies that incoming (positive) PD RENEW can be handled properly,
+// that a REPLY is generated, that the response has a prefix and that prefix
+// really belongs to the configured pool and that lease is actually renewed.
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA_PD that includes IAPREFIX
+// - lease is actually renewed in LeaseMgr
+TEST_F(Dhcpv6SrvTest, pdRenewBasic) {
+ testRenewBasic(Lease::TYPE_PD, "2001:db8:1:2::",
+ "2001:db8:1:2::", pd_pool_->getLength());
}
-// This test verifies that incoming (invalid) RENEW can be handled properly.
+// This test verifies that incoming (invalid) RENEW with an address
+// can be handled properly.
//
// This test checks 3 scenarios:
// 1. there is no such lease at all
@@ -1102,181 +899,59 @@ TEST_F(Dhcpv6SrvTest, RenewBasic) {
// expected:
// - returned REPLY message has copy of client-id
// - returned REPLY message has server-id
-// - returned REPLY message has IA that includes STATUS-CODE
+// - returned REPLY message has IA_NA that includes STATUS-CODE
// - No lease in LeaseMgr
TEST_F(Dhcpv6SrvTest, RenewReject) {
- NakedDhcpv6Srv srv(0);
-
- const IOAddress addr("2001:db8:1:1::dead");
- const uint32_t transid = 1234;
- const uint32_t valid_iaid = 234;
- const uint32_t bogus_iaid = 456;
-
- // Quick sanity check that the address we're about to use is ok
- ASSERT_TRUE(subnet_->inPool(addr));
-
- // GenerateClientId() also sets duid_
- OptionPtr clientid = generateClientId();
-
- // Check that the lease is NOT in the database
- Lease6Ptr l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_FALSE(l);
-
- // Let's create a RENEW
- Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, transid));
- req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(bogus_iaid, 1500, 3000);
-
- OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
- ia->addOption(renewed_addr_opt);
- req->addOption(ia);
- req->addOption(clientid);
-
- // Server-id is mandatory in RENEW
- req->addOption(srv.getServerID());
-
- // Case 1: No lease known to server
-
- // Pass it to the server and hope for a REPLY
- Pkt6Ptr reply = srv.processRenew(req);
-
- // Check if we get response at all
- checkResponse(reply, DHCPV6_REPLY, transid);
- OptionPtr tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
-
- // Check that there is no lease added
- l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_FALSE(l);
-
- // CASE 2: Lease is known and belongs to this client, but to a different IAID
-
- // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
- // value on purpose. They should be updated during RENEW.
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid_, valid_iaid,
- 501, 502, 503, 504, subnet_->getID(), 0));
- lease->cltt_ = 123; // Let's use it as an indicator that the lease
- // was NOT updated.
- ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
-
- // Pass it to the server and hope for a REPLY
- reply = srv.processRenew(req);
- checkResponse(reply, DHCPV6_REPLY, transid);
- tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
-
- // There is a iaid mis-match, so server should respond that there is
- // no such address to renew.
-
- // CASE 3: Lease belongs to a client with different client-id
- req->delOption(D6O_CLIENTID);
- ia = boost::dynamic_pointer_cast<Option6IA>(req->getOption(D6O_IA_NA));
- ia->setIAID(valid_iaid); // Now iaid in renew matches that in leasemgr
- req->addOption(generateClientId(13)); // generate different DUID
- // (with length 13)
-
- reply = srv.processRenew(req);
- checkResponse(reply, DHCPV6_REPLY, transid);
- tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
-
- lease = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_TRUE(lease);
- // Verify that the lease was not updated.
- EXPECT_EQ(123, lease->cltt_);
+ testRenewReject(Lease::TYPE_NA, IOAddress("2001:db8:1:1::dead"));
+}
- EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+// This test verifies that incoming (invalid) RENEW with a prefix
+// can be handled properly.
+//
+// This test checks 3 scenarios:
+// 1. there is no such lease at all
+// 2. there is such a lease, but it is assigned to a different IAID
+// 3. there is such a lease, but it belongs to a different client
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA_PD that includes STATUS-CODE
+// - No lease in LeaseMgr
+TEST_F(Dhcpv6SrvTest, pdRenewReject) {
+ testRenewReject(Lease::TYPE_PD, IOAddress("2001:db8:1:2::"));
}
-// This test verifies that incoming (positive) RELEASE can be handled properly,
-// that a REPLY is generated, that the response has status code and that the
-// lease is indeed removed from the database.
+// This test verifies that incoming (positive) RELEASE with address can be
+// handled properly, that a REPLY is generated, that the response has status
+// code and that the lease is indeed removed from the database.
//
// expected:
// - returned REPLY message has copy of client-id
// - returned REPLY message has server-id
-// - returned REPLY message has IA that does not include an IAADDR
+// - returned REPLY message has IA_NA that does not include an IAADDR
// - lease is actually removed from LeaseMgr
TEST_F(Dhcpv6SrvTest, ReleaseBasic) {
- NakedDhcpv6Srv srv(0);
-
- const IOAddress addr("2001:db8:1:1::cafe:babe");
- const uint32_t iaid = 234;
-
- // Generate client-id also duid_
- OptionPtr clientid = generateClientId();
-
- // Check that the address we are about to use is indeed in pool
- ASSERT_TRUE(subnet_->inPool(addr));
-
- // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
- // value on purpose. They should be updated during RENEW.
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid_, iaid,
- 501, 502, 503, 504, subnet_->getID(), 0));
- lease->cltt_ = 1234;
- ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
-
- // Check that the lease is really in the database
- Lease6Ptr l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_TRUE(l);
-
- // Let's create a RELEASE
- Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RELEASE, 1234));
- req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(iaid, 1500, 3000);
-
- OptionPtr released_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
- ia->addOption(released_addr_opt);
- req->addOption(ia);
- req->addOption(clientid);
-
- // Server-id is mandatory in RELEASE
- req->addOption(srv.getServerID());
-
- // Pass it to the server and hope for a REPLY
- Pkt6Ptr reply = srv.processRelease(req);
-
- // Check if we get response at all
- checkResponse(reply, DHCPV6_REPLY, 1234);
-
- OptionPtr tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
-
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- checkIA_NAStatusCode(ia, STATUS_Success);
- checkMsgStatusCode(reply, STATUS_Success);
-
- // There should be no address returned in RELEASE (see RFC3315, 18.2.6)
- EXPECT_FALSE(tmp->getOption(D6O_IAADDR));
-
- // Check DUIDs
- checkServerId(reply, srv.getServerID());
- checkClientId(reply, clientid);
-
- // Check that the lease is really gone in the database
- // get lease by address
- l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_FALSE(l);
+ testReleaseBasic(Lease::TYPE_NA, IOAddress("2001:db8:1:1::cafe:babe"),
+ IOAddress("2001:db8:1:1::cafe:babe"));
+}
- // get lease by subnetid/duid/iaid combination
- l = LeaseMgrFactory::instance().getLease6(*duid_, iaid, subnet_->getID());
- ASSERT_FALSE(l);
+// This test verifies that incoming (positive) RELEASE with prefix can be
+// handled properly, that a REPLY is generated, that the response has
+// status code and that the lease is indeed removed from the database.
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA_PD that does not include an IAPREFIX
+// - lease is actually removed from LeaseMgr
+TEST_F(Dhcpv6SrvTest, pdReleaseBasic) {
+ testReleaseBasic(Lease::TYPE_PD, IOAddress("2001:db8:1:2::"),
+ IOAddress("2001:db8:1:2::"));
}
-// This test verifies that incoming (invalid) RELEASE can be handled properly.
+// This test verifies that incoming (invalid) RELEASE with an address
+// can be handled properly.
//
// This test checks 3 scenarios:
// 1. there is no such lease at all
@@ -1286,107 +961,27 @@ TEST_F(Dhcpv6SrvTest, ReleaseBasic) {
// expected:
// - returned REPLY message has copy of client-id
// - returned REPLY message has server-id
-// - returned REPLY message has IA that includes STATUS-CODE
+// - returned REPLY message has IA_NA that includes STATUS-CODE
// - No lease in LeaseMgr
TEST_F(Dhcpv6SrvTest, ReleaseReject) {
+ testReleaseReject(Lease::TYPE_NA, IOAddress("2001:db8:1:1::dead"));
+}
- NakedDhcpv6Srv srv(0);
-
- const IOAddress addr("2001:db8:1:1::dead");
- const uint32_t transid = 1234;
- const uint32_t valid_iaid = 234;
- const uint32_t bogus_iaid = 456;
-
- // Quick sanity check that the address we're about to use is ok
- ASSERT_TRUE(subnet_->inPool(addr));
-
- // GenerateClientId() also sets duid_
- OptionPtr clientid = generateClientId();
-
- // Check that the lease is NOT in the database
- Lease6Ptr l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_FALSE(l);
-
- // Let's create a RELEASE
- Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RELEASE, transid));
- req->setRemoteAddr(IOAddress("fe80::abcd"));
- boost::shared_ptr<Option6IA> ia = generateIA(bogus_iaid, 1500, 3000);
-
- OptionPtr released_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
- ia->addOption(released_addr_opt);
- req->addOption(ia);
- req->addOption(clientid);
-
- // Server-id is mandatory in RENEW
- req->addOption(srv.getServerID());
-
- // Case 1: No lease known to server
- SCOPED_TRACE("CASE 1: No lease known to server");
-
- // Pass it to the server and hope for a REPLY
- Pkt6Ptr reply = srv.processRelease(req);
-
- // Check if we get response at all
- checkResponse(reply, DHCPV6_REPLY, transid);
- OptionPtr tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
- checkMsgStatusCode(reply, STATUS_NoBinding);
-
- // Check that the lease is not there
- l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_FALSE(l);
-
- // CASE 2: Lease is known and belongs to this client, but to a different IAID
- SCOPED_TRACE("CASE 2: Lease is known and belongs to this client, but to a different IAID");
-
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid_, valid_iaid,
- 501, 502, 503, 504, subnet_->getID(), 0));
- ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
-
- // Pass it to the server and hope for a REPLY
- reply = srv.processRelease(req);
- checkResponse(reply, DHCPV6_REPLY, transid);
- tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
- checkMsgStatusCode(reply, STATUS_NoBinding);
-
- // Check that the lease is still there
- l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_TRUE(l);
-
- // CASE 3: Lease belongs to a client with different client-id
- SCOPED_TRACE("CASE 3: Lease belongs to a client with different client-id");
-
- req->delOption(D6O_CLIENTID);
- ia = boost::dynamic_pointer_cast<Option6IA>(req->getOption(D6O_IA_NA));
- ia->setIAID(valid_iaid); // Now iaid in renew matches that in leasemgr
- req->addOption(generateClientId(13)); // generate different DUID
- // (with length 13)
-
- reply = srv.processRelease(req);
- checkResponse(reply, DHCPV6_REPLY, transid);
- tmp = reply->getOption(D6O_IA_NA);
- ASSERT_TRUE(tmp);
- // Check that IA_NA was returned and that there's an address included
- ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
- ASSERT_TRUE(ia);
- checkIA_NAStatusCode(ia, STATUS_NoBinding);
- checkMsgStatusCode(reply, STATUS_NoBinding);
-
- // Check that the lease is still there
- l = LeaseMgrFactory::instance().getLease6(addr);
- ASSERT_TRUE(l);
-
- // Finally, let's cleanup the database
- EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+// This test verifies that incoming (invalid) RELEASE with a prefix
+// can be handled properly.
+//
+// This test checks 3 scenarios:
+// 1. there is no such lease at all
+// 2. there is such a lease, but it is assigned to a different IAID
+// 3. there is such a lease, but it belongs to a different client
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA_PD that includes STATUS-CODE
+// - No lease in LeaseMgr
+TEST_F(Dhcpv6SrvTest, pdReleaseReject) {
+ testReleaseReject(Lease::TYPE_PD, IOAddress("2001:db8:1:2::"));
}
// This test verifies if the status code option is generated properly.
@@ -1592,6 +1187,113 @@ TEST_F(Dhcpv6SrvTest, selectSubnetIface) {
EXPECT_EQ(subnet3, srv.selectSubnet(pkt));
}
+// This test verifies if selectSubnet() selects proper subnet for a given
+// linkaddr in RELAY-FORW message
+TEST_F(Dhcpv6SrvTest, selectSubnetRelayLinkaddr) {
+ NakedDhcpv6Srv srv(0);
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+ Pkt6::RelayInfo relay;
+ relay.linkaddr_ = IOAddress("2001:db8:2::1234");
+ relay.peeraddr_ = IOAddress("fe80::1");
+
+ // CASE 1: We have only one subnet defined and we received relayed traffic.
+ // The only available subnet should NOT be selected.
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+ Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ pkt->relay_info_.push_back(relay);
+
+ Subnet6Ptr selected = srv.selectSubnet(pkt);
+ EXPECT_FALSE(selected);
+
+ // CASE 2: We have three subnets defined and we received relayed traffic.
+ // Nothing should be selected.
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1);
+ CfgMgr::instance().addSubnet6(subnet2);
+ CfgMgr::instance().addSubnet6(subnet3);
+ selected = srv.selectSubnet(pkt);
+ EXPECT_EQ(selected, subnet2);
+
+ // CASE 3: We have three subnets defined and we received relayed traffic
+ // that came out of subnet 2. We should select subnet2 then
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1);
+ CfgMgr::instance().addSubnet6(subnet2);
+ CfgMgr::instance().addSubnet6(subnet3);
+
+ // Source of the packet should have no meaning. Selection is based
+ // on linkaddr field in the relay
+ pkt->setRemoteAddr(IOAddress("2001:db8:1::baca"));
+ selected = srv.selectSubnet(pkt);
+ EXPECT_EQ(selected, subnet2);
+
+ // CASE 4: We have three subnets defined and we received relayed traffic
+ // that came out of undefined subnet. We should select nothing
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1);
+ CfgMgr::instance().addSubnet6(subnet2);
+ CfgMgr::instance().addSubnet6(subnet3);
+ pkt->relay_info_.clear();
+ relay.linkaddr_ = IOAddress("2001:db8:4::1234");
+ pkt->relay_info_.push_back(relay);
+ selected = srv.selectSubnet(pkt);
+ EXPECT_FALSE(selected);
+
+}
+
+// This test verifies if selectSubnet() selects proper subnet for a given
+// interface-id option
+TEST_F(Dhcpv6SrvTest, selectSubnetRelayInterfaceId) {
+ NakedDhcpv6Srv srv(0);
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2001:db8:1::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("2001:db8:2::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("2001:db8:3::"), 48, 1, 2, 3, 4));
+
+ subnet1->setInterfaceId(generateInterfaceId("relay1"));
+ subnet2->setInterfaceId(generateInterfaceId("relay2"));
+
+ // CASE 1: We have only one subnet defined and it is for interface-id "relay1"
+ // Packet came with interface-id "relay2". We should not select subnet1
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1); // just a single subnet
+
+ Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ Pkt6::RelayInfo relay;
+ relay.linkaddr_ = IOAddress("2001:db8:2::1234");
+ relay.peeraddr_ = IOAddress("fe80::1");
+ OptionPtr opt = generateInterfaceId("relay2");
+ relay.options_.insert(make_pair(opt->getType(), opt));
+ pkt->relay_info_.push_back(relay);
+
+ // There is only one subnet configured and we are outside of that subnet
+ Subnet6Ptr selected = srv.selectSubnet(pkt);
+ EXPECT_FALSE(selected);
+
+ // CASE 2: We have only one subnet defined and it is for interface-id "relay2"
+ // Packet came with interface-id "relay2". We should select it
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet2); // just a single subnet
+ selected = srv.selectSubnet(pkt);
+ EXPECT_EQ(selected, subnet2);
+
+ // CASE 3: We have only 3 subnets defined: one remote for interface-id "relay1",
+ // one remote for interface-id "relay2" and third local
+ // packet comes with interface-id "relay2". We should select subnet2
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet1);
+ CfgMgr::instance().addSubnet6(subnet2);
+ CfgMgr::instance().addSubnet6(subnet3);
+
+ EXPECT_EQ(subnet2, srv.selectSubnet(pkt));
+}
+
// This test verifies if the server-id disk operations (read, write) are
// working properly.
TEST_F(Dhcpv6SrvTest, ServerID) {
@@ -1624,6 +1326,383 @@ TEST_F(Dhcpv6SrvTest, ServerID) {
EXPECT_EQ(duid1_text, text);
}
+// Checks if server responses are sent to the proper port.
+TEST_F(Dhcpv6SrvTest, portsDirectTraffic) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = captureSimpleSolicit();
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ srv.run();
+
+ // Get Advertise...
+ ASSERT_FALSE(srv.fake_sent_.empty());
+ Pkt6Ptr adv = srv.fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // This is sent back to client directly, should be port 546
+ EXPECT_EQ(DHCP6_CLIENT_PORT, adv->getRemotePort());
+}
+
+// Checks if server responses are sent to the proper port.
+TEST_F(Dhcpv6SrvTest, portsRelayedTraffic) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = captureRelayedSolicit();
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ srv.run();
+
+ // Get Advertise...
+ ASSERT_FALSE(srv.fake_sent_.empty());
+ Pkt6Ptr adv = srv.fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // This is sent back to relay, so port is 547
+ EXPECT_EQ(DHCP6_SERVER_PORT, adv->getRemotePort());
+}
+
+// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems
+// @todo Uncomment this test as part of #3180 work.
+// Kea code currently fails to handle docsis traffic.
+TEST_F(Dhcpv6SrvTest, docsisTraffic) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt6Ptr sol = captureDocsisRelayedSolicit();
+
+ // Simulate that we have received that traffic
+ srv.fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ srv.run();
+
+ // We should have an Advertise in response
+ ASSERT_FALSE(srv.fake_sent_.empty());
+ Pkt6Ptr adv = srv.fake_sent_.front();
+ ASSERT_TRUE(adv);
+}
+
+// Checks if server is able to handle a relayed traffic from DOCSIS3.0 modems
+TEST_F(Dhcpv6SrvTest, docsisVendorOptionsParse) {
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt6Ptr sol = captureDocsisRelayedSolicit();
+ EXPECT_NO_THROW(sol->unpack());
+
+ // Check if the packet contain
+ OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS);
+ ASSERT_TRUE(opt);
+
+ boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+
+ EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_ORO));
+ EXPECT_TRUE(vendor->getOption(36));
+ EXPECT_TRUE(vendor->getOption(35));
+ EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_DEVICE_TYPE));
+ EXPECT_TRUE(vendor->getOption(3));
+ EXPECT_TRUE(vendor->getOption(4));
+ EXPECT_TRUE(vendor->getOption(5));
+ EXPECT_TRUE(vendor->getOption(6));
+ EXPECT_TRUE(vendor->getOption(7));
+ EXPECT_TRUE(vendor->getOption(8));
+ EXPECT_TRUE(vendor->getOption(9));
+ EXPECT_TRUE(vendor->getOption(DOCSIS3_V6_VENDOR_NAME));
+ EXPECT_TRUE(vendor->getOption(15));
+
+ EXPECT_FALSE(vendor->getOption(20));
+ EXPECT_FALSE(vendor->getOption(11));
+ EXPECT_FALSE(vendor->getOption(17));
+}
+
+// Checks if server is able to parse incoming docsis option and extract suboption 1 (docsis ORO)
+TEST_F(Dhcpv6SrvTest, docsisVendorORO) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's get a traffic capture from DOCSIS3.0 modem
+ Pkt6Ptr sol = captureDocsisRelayedSolicit();
+ ASSERT_NO_THROW(sol->unpack());
+
+ // Check if the packet contains vendor options option
+ OptionPtr opt = sol->getOption(D6O_VENDOR_OPTS);
+ ASSERT_TRUE(opt);
+
+ boost::shared_ptr<OptionVendor> vendor = boost::dynamic_pointer_cast<OptionVendor>(opt);
+ ASSERT_TRUE(vendor);
+
+ opt = vendor->getOption(DOCSIS3_V6_ORO);
+ ASSERT_TRUE(opt);
+
+ OptionUint16ArrayPtr oro = boost::dynamic_pointer_cast<OptionUint16Array>(opt);
+ EXPECT_TRUE(oro);
+}
+
+// This test checks if Option Request Option (ORO) in docsis (vendor-id=4491)
+// vendor options is parsed correctly and the requested options are actually assigned.
+TEST_F(Dhcpv6SrvTest, vendorOptionsORO) {
+ ConstElementPtr x;
+ string config = "{ \"interfaces\": [ \"all\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ " \"option-def\": [ {"
+ " \"name\": \"config-file\","
+ " \"code\": 33,"
+ " \"type\": \"string\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"vendor-4491\","
+ " \"encapsulate\": \"\""
+ " } ],"
+ " \"option-data\": [ {"
+ " \"name\": \"config-file\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": 33,"
+ " \"data\": \"normal_erouter_v6.cm\","
+ " \"csv-format\": True"
+ " }],"
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/64\" ],"
+ " \"subnet\": \"2001:db8:1::/48\", "
+ " \"renew-timer\": 1000, "
+ " \"rebind-timer\": 1000, "
+ " \"preferred-lifetime\": 3000,"
+ " \"valid-lifetime\": 4000,"
+ " \"interface-id\": \"\","
+ " \"interface\": \"\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+
+ NakedDhcpv6Srv srv(0);
+
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv, json));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+
+ ASSERT_EQ(0, rcode_);
+
+ Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ sol->setRemoteAddr(IOAddress("fe80::abcd"));
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt6Ptr adv = srv.processSolicit(sol);
+
+ // check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // We did not include any vendor opts in SOLCIT, so there should be none
+ // in ADVERTISE.
+ ASSERT_FALSE(adv->getOption(D6O_VENDOR_OPTS));
+
+ // Let's add a vendor-option (vendor-id=4491) with a single sub-option.
+ // That suboption has code 1 and is a docsis ORO option.
+ boost::shared_ptr<OptionUint16Array> vendor_oro(new OptionUint16Array(Option::V6,
+ DOCSIS3_V6_ORO));
+ vendor_oro->addValue(DOCSIS3_V6_CONFIG_FILE); // Request option 33
+ OptionPtr vendor(new OptionVendor(Option::V6, 4491));
+ vendor->addOption(vendor_oro);
+ sol->addOption(vendor);
+
+ // Need to process SOLICIT again after requesting new option.
+ adv = srv.processSolicit(sol);
+ ASSERT_TRUE(adv);
+
+ // Check if thre is vendor option response
+ OptionPtr tmp = adv->getOption(D6O_VENDOR_OPTS);
+ ASSERT_TRUE(tmp);
+
+ // The response should be OptionVendor object
+ boost::shared_ptr<OptionVendor> vendor_resp =
+ boost::dynamic_pointer_cast<OptionVendor>(tmp);
+ ASSERT_TRUE(vendor_resp);
+
+ OptionPtr docsis33 = vendor_resp->getOption(33);
+ ASSERT_TRUE(docsis33);
+
+ OptionStringPtr config_file = boost::dynamic_pointer_cast<OptionString>(docsis33);
+ ASSERT_TRUE(config_file);
+ EXPECT_EQ("normal_erouter_v6.cm", config_file->getValue());
+}
+
+// Test checks whether it is possible to use option definitions defined in
+// src/lib/dhcp/docsis3_option_defs.h.
+TEST_F(Dhcpv6SrvTest, vendorOptionsDocsisDefinitions) {
+ ConstElementPtr x;
+ string config_prefix = "{ \"interfaces\": [ \"all\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ " \"option-data\": [ {"
+ " \"name\": \"config-file\","
+ " \"space\": \"vendor-4491\","
+ " \"code\": ";
+ string config_postfix = ","
+ " \"data\": \"normal_erouter_v6.cm\","
+ " \"csv-format\": True"
+ " }],"
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/64\" ],"
+ " \"subnet\": \"2001:db8:1::/48\", "
+ " \"renew-timer\": 1000, "
+ " \"rebind-timer\": 1000, "
+ " \"preferred-lifetime\": 3000,"
+ " \"valid-lifetime\": 4000,"
+ " \"interface-id\": \"\","
+ " \"interface\": \"\""
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ // There is docsis3 (vendor-id=4491) vendor option 33, which is a
+ // config-file. Its format is a single string.
+ string config_valid = config_prefix + "33" + config_postfix;
+
+ // There is no option 99 defined in vendor-id=4491. As there is no
+ // definition, the config should fail.
+ string config_bogus = config_prefix + "99" + config_postfix;
+
+ ElementPtr json_bogus = Element::fromJSON(config_bogus);
+ ElementPtr json_valid = Element::fromJSON(config_valid);
+
+ NakedDhcpv6Srv srv(0);
+
+ // This should fail (missing option definition)
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv, json_bogus));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(1, rcode_);
+
+ // This should work (option definition present)
+ EXPECT_NO_THROW(x = configureDhcp6Server(srv, json_valid));
+ ASSERT_TRUE(x);
+ comment_ = parseAnswer(rcode_, x);
+ ASSERT_EQ(0, rcode_);
+}
+
+// This test verifies that the following option structure can be parsed:
+// - option (option space 'foobar')
+// - sub option (option space 'foo')
+// - sub option (option space 'bar')
+TEST_F(Dhcpv6SrvTest, unpackOptions) {
+ // Create option definition for each level of encapsulation. Each option
+ // definition is for the option code 1. Options may have the same
+ // option code because they belong to different option spaces.
+
+ // Top level option encapsulates options which belong to 'space-foo'.
+ OptionDefinitionPtr opt_def(new OptionDefinition("option-foobar", 1, "uint32",
+ "space-foo"));\
+ // Middle option encapsulates options which belong to 'space-bar'
+ OptionDefinitionPtr opt_def2(new OptionDefinition("option-foo", 1, "uint16",
+ "space-bar"));
+ // Low level option doesn't encapsulate any option space.
+ OptionDefinitionPtr opt_def3(new OptionDefinition("option-bar", 1,
+ "uint8"));
+
+ // Add option definitions to the Configuration Manager. Each goes under
+ // different option space.
+ CfgMgr& cfgmgr = CfgMgr::instance();
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def, "space-foobar"));
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def2, "space-foo"));
+ ASSERT_NO_THROW(cfgmgr.addOptionDef(opt_def3, "space-bar"));
+
+ // Create the buffer holding the structure of options.
+ const char raw_data[] = {
+ // First option starts here.
+ 0x00, 0x01, // option code = 1
+ 0x00, 0x0F, // option length = 15
+ 0x00, 0x01, 0x02, 0x03, // This option carries uint32 value
+ // Sub option starts here.
+ 0x00, 0x01, // option code = 1
+ 0x00, 0x07, // option length = 7
+ 0x01, 0x02, // this option carries uint16 value
+ // Last option starts here.
+ 0x00, 0x01, // option code = 1
+ 0x00, 0x01, // option length = 1
+ 0x00 // This option carries a single uint8 value and has no sub options.
+ };
+ OptionBuffer buf(raw_data, raw_data + sizeof(raw_data));
+
+ // Parse options.
+ NakedDhcpv6Srv srv(0);
+ OptionCollection options;
+ ASSERT_NO_THROW(srv.unpackOptions(buf, "space-foobar", options, 0, 0));
+
+ // There should be one top level option.
+ ASSERT_EQ(1, options.size());
+ boost::shared_ptr<OptionInt<uint32_t> > option_foobar =
+ boost::dynamic_pointer_cast<OptionInt<uint32_t> >(options.begin()->
+ second);
+ ASSERT_TRUE(option_foobar);
+ EXPECT_EQ(1, option_foobar->getType());
+ EXPECT_EQ(0x00010203, option_foobar->getValue());
+ // There should be a middle level option held in option_foobar.
+ boost::shared_ptr<OptionInt<uint16_t> > option_foo =
+ boost::dynamic_pointer_cast<OptionInt<uint16_t> >(option_foobar->
+ getOption(1));
+ ASSERT_TRUE(option_foo);
+ EXPECT_EQ(1, option_foo->getType());
+ EXPECT_EQ(0x0102, option_foo->getValue());
+ // Finally, there should be a low level option under option_foo.
+ boost::shared_ptr<OptionInt<uint8_t> > option_bar =
+ boost::dynamic_pointer_cast<OptionInt<uint8_t> >(option_foo->getOption(1));
+ ASSERT_TRUE(option_bar);
+ EXPECT_EQ(1, option_bar->getType());
+ EXPECT_EQ(0x0, option_bar->getValue());
+}
+
+// Checks if client packets are classified properly
+TEST_F(Dhcpv6SrvTest, clientClassification) {
+
+ NakedDhcpv6Srv srv(0);
+
+ // Let's create a relayed SOLICIT. This particular relayed SOLICIT has
+ // vendor-class set to docsis3.0
+ Pkt6Ptr sol1;
+ ASSERT_NO_THROW(sol1 = captureDocsisRelayedSolicit());
+ ASSERT_NO_THROW(sol1->unpack());
+
+ srv.classifyPacket(sol1);
+
+ // It should belong to docsis3.0 class. It should not belong to eRouter1.0
+ EXPECT_TRUE(sol1->inClass("docsis3.0"));
+ EXPECT_FALSE(sol1->inClass("eRouter1.0"));
+
+ // Let's get a relayed SOLICIT. This particular relayed SOLICIT has
+ // vendor-class set to eRouter1.0
+ Pkt6Ptr sol2;
+ ASSERT_NO_THROW(sol2 = captureeRouterRelayedSolicit());
+ ASSERT_NO_THROW(sol2->unpack());
+
+ srv.classifyPacket(sol2);
+
+ EXPECT_TRUE(sol2->inClass("eRouter1.0"));
+ EXPECT_FALSE(sol2->inClass("docsis3.0"));
+}
+
+
/// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test
/// to call processX() methods.
diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.cc b/src/bin/dhcp6/tests/dhcp6_test_utils.cc
new file mode 100644
index 0000000..88dee36
--- /dev/null
+++ b/src/bin/dhcp6/tests/dhcp6_test_utils.cc
@@ -0,0 +1,557 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <gtest/gtest.h>
+#include <dhcp6/tests/dhcp6_test_utils.h>
+
+namespace isc {
+namespace test {
+
+// Checks that server response (ADVERTISE or REPLY) contains proper IA_NA option
+// It returns IAADDR option for each chaining with checkIAAddr method.
+boost::shared_ptr<Option6IAAddr>
+Dhcpv6SrvTest::checkIA_NA(const Pkt6Ptr& rsp, uint32_t expected_iaid,
+ uint32_t expected_t1, uint32_t expected_t2) {
+ OptionPtr tmp = rsp->getOption(D6O_IA_NA);
+ // Can't use ASSERT_TRUE() in method that returns something
+ if (!tmp) {
+ ADD_FAILURE() << "IA_NA option not present in response";
+ return (boost::shared_ptr<Option6IAAddr>());
+ }
+
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ if (!ia) {
+ ADD_FAILURE() << "IA_NA cannot convert option ptr to Option6";
+ return (boost::shared_ptr<Option6IAAddr>());
+ }
+
+ EXPECT_EQ(expected_iaid, ia->getIAID());
+ EXPECT_EQ(expected_t1, ia->getT1());
+ EXPECT_EQ(expected_t2, ia->getT2());
+
+ tmp = ia->getOption(D6O_IAADDR);
+ boost::shared_ptr<Option6IAAddr> addr = boost::dynamic_pointer_cast<Option6IAAddr>(tmp);
+ return (addr);
+}
+
+boost::shared_ptr<Option6IAPrefix>
+Dhcpv6SrvTest::checkIA_PD(const Pkt6Ptr& rsp, uint32_t expected_iaid,
+ uint32_t expected_t1, uint32_t expected_t2) {
+ OptionPtr tmp = rsp->getOption(D6O_IA_PD);
+ // Can't use ASSERT_TRUE() in method that returns something
+ if (!tmp) {
+ ADD_FAILURE() << "IA_PD option not present in response";
+ return (boost::shared_ptr<Option6IAPrefix>());
+ }
+
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ if (!ia) {
+ ADD_FAILURE() << "IA_PD cannot convert option ptr to Option6";
+ return (boost::shared_ptr<Option6IAPrefix>());
+ }
+
+ EXPECT_EQ(expected_iaid, ia->getIAID());
+ EXPECT_EQ(expected_t1, ia->getT1());
+ EXPECT_EQ(expected_t2, ia->getT2());
+
+ tmp = ia->getOption(D6O_IAPREFIX);
+ boost::shared_ptr<Option6IAPrefix> addr = boost::dynamic_pointer_cast<Option6IAPrefix>(tmp);
+ return (addr);
+}
+
+// Checks if the lease sent to client is present in the database
+// and is valid when checked agasint the configured subnet
+Lease6Ptr
+Dhcpv6SrvTest::checkLease(const DuidPtr& duid, const OptionPtr& ia_na,
+ boost::shared_ptr<Option6IAAddr> addr) {
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(ia_na);
+
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr->getAddress());
+ if (!lease) {
+ std::cout << "Lease for " << addr->getAddress()
+ << " not found in the database backend.";
+ return (Lease6Ptr());
+ }
+
+ EXPECT_EQ(addr->getAddress(), lease->addr_);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_EQ(ia->getIAID(), lease->iaid_);
+ EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
+
+ return (lease);
+}
+
+Lease6Ptr
+Dhcpv6SrvTest::checkPdLease(const DuidPtr& duid, const OptionPtr& ia_pd,
+ boost::shared_ptr<Option6IAPrefix> prefix){
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(ia_pd);
+
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(Lease::TYPE_PD,
+ prefix->getAddress());
+ if (!lease) {
+ std::cout << "PD lease for " << prefix->getAddress()
+ << " not found in the database backend.";
+ return (Lease6Ptr());
+ }
+
+ EXPECT_EQ(prefix->getAddress(), lease->addr_);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_EQ(ia->getIAID(), lease->iaid_);
+ EXPECT_EQ(subnet_->getID(), lease->subnet_id_);
+
+ return (lease);
+}
+
+
+Pkt6Ptr
+Dhcpv6SrvTest::createMessage(uint8_t message_type, Lease::Type lease_type,
+ const IOAddress& addr, const uint8_t prefix_len,
+ uint32_t iaid) {
+ // Let's create a RENEW
+ Pkt6Ptr msg = Pkt6Ptr(new Pkt6(message_type, 1234));
+ msg->setRemoteAddr(IOAddress("fe80::abcd"));
+
+ uint16_t code;
+ OptionPtr subopt;
+ switch (lease_type) {
+ case Lease::TYPE_NA:
+ code = D6O_IA_NA;
+ subopt.reset(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ break;
+ case Lease::TYPE_PD:
+ code = D6O_IA_PD;
+ subopt.reset(new Option6IAPrefix(D6O_IAPREFIX, addr, prefix_len,
+ 300, 500));
+ break;
+ default:
+ isc_throw(BadValue, "Invalid lease type specified");
+ }
+
+ boost::shared_ptr<Option6IA> ia = generateIA(code, iaid, 1500, 3000);
+
+ ia->addOption(subopt);
+ msg->addOption(ia);
+
+ return (msg);
+}
+
+void
+Dhcpv6SrvTest::testRenewBasic(Lease::Type type, const std::string& existing_addr,
+ const std::string& renew_addr,
+ const uint8_t prefix_len) {
+ NakedDhcpv6Srv srv(0);
+
+ const IOAddress existing(existing_addr);
+ const IOAddress renew(renew_addr);
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(type, existing));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(type, existing, duid_, iaid, 501, 502, 503, 504,
+ subnet_->getID(), prefix_len));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, existing);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt really set and not using
+ // previous (500, 501, etc.) values
+ EXPECT_NE(l->t1_, subnet_->getT1());
+ EXPECT_NE(l->t2_, subnet_->getT2());
+ EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_NE(l->valid_lft_, subnet_->getValid());
+ EXPECT_NE(l->cltt_, time(NULL));
+
+ Pkt6Ptr req = createMessage(DHCPV6_RENEW, type, IOAddress(renew_addr),
+ prefix_len, iaid);
+ req->addOption(clientid);
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRenew(req);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+
+ // Check DUIDs
+ checkServerId(reply, srv.getServerID());
+ checkClientId(reply, clientid);
+
+ switch (type) {
+ case Lease::TYPE_NA: {
+ // Check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAAddr>
+ addr_opt = checkIA_NA(reply, 234, subnet_->getT1(), subnet_->getT2());
+
+ ASSERT_TRUE(addr_opt);
+
+ // Check that we've got the address we requested
+ checkIAAddr(addr_opt, renew, Lease::TYPE_NA);
+
+ // Check that the lease is really in the database
+ l = checkLease(duid_, reply->getOption(D6O_IA_NA), addr_opt);
+ ASSERT_TRUE(l);
+ break;
+ }
+
+ case Lease::TYPE_PD: {
+ // Check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAPrefix> prefix_opt
+ = checkIA_PD(reply, 234, subnet_->getT1(), subnet_->getT2());
+
+ ASSERT_TRUE(prefix_opt);
+
+ // Check that we've got the address we requested
+ checkIAAddr(prefix_opt, renew, Lease::TYPE_PD);
+ EXPECT_EQ(pd_pool_->getLength(), prefix_opt->getLength());
+
+ // Check that the lease is really in the database
+ l = checkLease(duid_, reply->getOption(D6O_IA_PD), prefix_opt);
+ ASSERT_TRUE(l);
+ break;
+ }
+ default:
+ isc_throw(BadValue, "Invalid lease type");
+ }
+
+ // Check that T1, T2, preferred, valid and cltt were really updated
+ EXPECT_EQ(l->t1_, subnet_->getT1());
+ EXPECT_EQ(l->t2_, subnet_->getT2());
+ EXPECT_EQ(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_EQ(l->valid_lft_, subnet_->getValid());
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(renew_addr));
+}
+
+void
+Dhcpv6SrvTest::testRenewReject(Lease::Type type, const IOAddress& addr) {
+
+ NakedDhcpv6Srv srv(0);
+
+ const uint32_t transid = 1234;
+ const uint32_t valid_iaid = 234;
+ const uint32_t bogus_iaid = 456;
+
+ uint32_t code;
+ uint8_t prefix_len;
+ if (type == Lease::TYPE_NA) {
+ code = D6O_IA_NA;
+ prefix_len = 128;
+ } else if (type == Lease::TYPE_PD) {
+ code = D6O_IA_PD;
+ prefix_len = pd_pool_->getLength();
+ } else {
+ isc_throw(BadValue, "Invalid lease type");
+ }
+
+ // Quick sanity check that the address we're about to use is ok
+ ASSERT_TRUE(subnet_->inPool(type, addr));
+
+ // GenerateClientId() also sets duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the lease is NOT in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_FALSE(l);
+
+ // Let's create a RENEW
+ Pkt6Ptr req = createMessage(DHCPV6_RENEW, type, IOAddress(addr), prefix_len,
+ bogus_iaid);
+ req->addOption(clientid);
+ req->addOption(srv.getServerID());
+
+ // Case 1: No lease known to server
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRenew(req);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ OptionPtr tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_?? was returned and that there's proper status code
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+
+ // Check that there is no lease added
+ l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_FALSE(l);
+
+ // CASE 2: Lease is known and belongs to this client, but to a different IAID
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(type, addr, duid_, valid_iaid,
+ 501, 502, 503, 504, subnet_->getID(),
+ prefix_len));
+ lease->cltt_ = 123; // Let's use it as an indicator that the lease
+ // was NOT updated.
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Pass it to the server and hope for a REPLY
+ reply = srv.processRenew(req);
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_?? was returned and that there's proper status code
+ ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+
+ // There is a iaid mis-match, so server should respond that there is
+ // no such address to renew.
+
+ // CASE 3: Lease belongs to a client with different client-id
+ req->delOption(D6O_CLIENTID);
+ ia = boost::dynamic_pointer_cast<Option6IA>(req->getOption(code));
+ ia->setIAID(valid_iaid); // Now iaid in renew matches that in leasemgr
+ req->addOption(generateClientId(13)); // generate different DUID
+ // (with length 13)
+
+ reply = srv.processRenew(req);
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_?? was returned and that there's proper status code
+ ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+
+ lease = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_TRUE(lease);
+ // Verify that the lease was not updated.
+ EXPECT_EQ(123, lease->cltt_);
+
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+}
+
+void
+Dhcpv6SrvTest::testReleaseBasic(Lease::Type type, const IOAddress& existing,
+ const IOAddress& release_addr) {
+ NakedDhcpv6Srv srv(0);
+
+ const uint32_t iaid = 234;
+
+ uint32_t code; // option code of the container (IA_NA or IA_PD)
+ uint8_t prefix_len;
+ if (type == Lease::TYPE_NA) {
+ code = D6O_IA_NA;
+ prefix_len = 128;
+ } else if (type == Lease::TYPE_PD) {
+ code = D6O_IA_PD;
+ prefix_len = pd_pool_->getLength();
+ } else {
+ isc_throw(BadValue, "Invalid lease type");
+ }
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(type, existing));
+
+ // Let's prepopulate the database
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, existing, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(),
+ prefix_len));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, existing);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ Pkt6Ptr rel = createMessage(DHCPV6_RELEASE, type, release_addr, prefix_len,
+ iaid);
+ rel->addOption(clientid);
+ rel->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRelease(rel);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+
+ OptionPtr tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ checkIA_NAStatusCode(ia, STATUS_Success);
+ checkMsgStatusCode(reply, STATUS_Success);
+
+ // There should be no address returned in RELEASE (see RFC3315, 18.2.6)
+ // There should be no prefix
+ EXPECT_FALSE(tmp->getOption(D6O_IAADDR));
+ EXPECT_FALSE(tmp->getOption(D6O_IAPREFIX));
+
+ // Check DUIDs
+ checkServerId(reply, srv.getServerID());
+ checkClientId(reply, clientid);
+
+ // Check that the lease is really gone in the database
+ // get lease by address
+ l = LeaseMgrFactory::instance().getLease6(type, release_addr);
+ ASSERT_FALSE(l);
+
+ // get lease by subnetid/duid/iaid combination
+ l = LeaseMgrFactory::instance().getLease6(type, *duid_, iaid,
+ subnet_->getID());
+ ASSERT_FALSE(l);
+}
+
+void
+Dhcpv6SrvTest::testReleaseReject(Lease::Type type, const IOAddress& addr) {
+ NakedDhcpv6Srv srv(0);
+
+ const uint32_t transid = 1234;
+ const uint32_t valid_iaid = 234;
+ const uint32_t bogus_iaid = 456;
+
+ uint32_t code; // option code of the container (IA_NA or IA_PD)
+ uint8_t prefix_len;
+ if (type == Lease::TYPE_NA) {
+ code = D6O_IA_NA;
+ prefix_len = 128;
+ } else if (type == Lease::TYPE_PD) {
+ code = D6O_IA_PD;
+ prefix_len = pd_pool_->getLength();
+ } else {
+ isc_throw(BadValue, "Invalid lease type");
+ }
+
+ // Quick sanity check that the address we're about to use is ok
+ ASSERT_TRUE(subnet_->inPool(type, addr));
+
+ // GenerateClientId() also sets duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the lease is NOT in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_FALSE(l);
+
+ // Let's create a RELEASE
+ Pkt6Ptr rel = createMessage(DHCPV6_RELEASE, type, addr, prefix_len, valid_iaid);
+ rel->addOption(clientid);
+ rel->addOption(srv.getServerID());
+
+ // Case 1: No lease known to server
+ SCOPED_TRACE("CASE 1: No lease known to server");
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRelease(rel);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ OptionPtr tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+ // Check that IA_NA/IA_PD was returned and that there's status code in it
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+ checkMsgStatusCode(reply, STATUS_NoBinding);
+
+ // Check that the lease is not there
+ l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_FALSE(l);
+
+ // CASE 2: Lease is known and belongs to this client, but to a different IAID
+ SCOPED_TRACE("CASE 2: Lease is known and belongs to this client, but to a different IAID");
+
+ Lease6Ptr lease(new Lease6(type, addr, duid_, valid_iaid, 501, 502, 503,
+ 504, subnet_->getID(), prefix_len));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Let's create a different RELEASE, with a bogus iaid
+ rel = createMessage(DHCPV6_RELEASE, type, addr, prefix_len, bogus_iaid);
+ rel->addOption(clientid);
+ rel->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ reply = srv.processRelease(rel);
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_?? was returned and that there's proper status code
+ ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+ checkMsgStatusCode(reply, STATUS_NoBinding);
+
+ // Check that the lease is still there
+ l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_TRUE(l);
+
+ // CASE 3: Lease belongs to a client with different client-id
+ SCOPED_TRACE("CASE 3: Lease belongs to a client with different client-id");
+
+ rel->delOption(D6O_CLIENTID);
+ ia = boost::dynamic_pointer_cast<Option6IA>(rel->getOption(code));
+ ia->setIAID(valid_iaid); // Now iaid in renew matches that in leasemgr
+ rel->addOption(generateClientId(13)); // generate different DUID
+ // (with length 13)
+
+ reply = srv.processRelease(rel);
+ checkResponse(reply, DHCPV6_REPLY, transid);
+ tmp = reply->getOption(code);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_?? was returned and that there's proper status code
+ ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ checkIA_NAStatusCode(ia, STATUS_NoBinding);
+ checkMsgStatusCode(reply, STATUS_NoBinding);
+
+ // Check that the lease is still there
+ l = LeaseMgrFactory::instance().getLease6(type, addr);
+ ASSERT_TRUE(l);
+
+ // Finally, let's cleanup the database
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr));
+}
+
+
+// Generate IA_NA option with specified parameters
+boost::shared_ptr<Option6IA>
+NakedDhcpv6SrvTest::generateIA(uint16_t type, uint32_t iaid, uint32_t t1,
+ uint32_t t2) {
+ boost::shared_ptr<Option6IA> ia =
+ boost::shared_ptr<Option6IA>(new Option6IA(type, iaid));
+ ia->setT1(t1);
+ ia->setT2(t2);
+ return (ia);
+}
+
+}; // end of isc::test namespace
+}; // end of isc namespace
diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.h b/src/bin/dhcp6/tests/dhcp6_test_utils.h
new file mode 100644
index 0000000..82ff984
--- /dev/null
+++ b/src/bin/dhcp6/tests/dhcp6_test_utils.h
@@ -0,0 +1,503 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file dhcp6_test_utils.h
+///
+/// @brief This file contains utility classes used for DHCPv6 server testing
+
+#ifndef DHCP6_TEST_UTILS_H
+#define DHCP6_TEST_UTILS_H
+
+#include <gtest/gtest.h>
+
+#include <dhcp/pkt6.h>
+#include <dhcp/option6_ia.h>
+#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
+#include <dhcp/option_int_array.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcp6/dhcp6_srv.h>
+#include <hooks/hooks_manager.h>
+#include <config/ccsession.h>
+
+#include <list>
+
+using namespace isc::dhcp;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::hooks;
+using namespace isc::asiolink;
+using namespace isc::util;
+using namespace isc::hooks;
+
+namespace isc {
+namespace test {
+
+/// @brief "naked" Dhcpv6Srv class that exposes internal members
+class NakedDhcpv6Srv: public isc::dhcp::Dhcpv6Srv {
+public:
+ NakedDhcpv6Srv(uint16_t port) : Dhcpv6Srv(port) {
+ // Open the "memfile" database for leases
+ std::string memfile = "type=memfile";
+ LeaseMgrFactory::create(memfile);
+ }
+
+ /// @brief fakes packet reception
+ /// @param timeout ignored
+ ///
+ /// The method receives all packets queued in receive
+ /// queue, one after another. Once the queue is empty,
+ /// it initiates the shutdown procedure.
+ ///
+ /// See fake_received_ field for description
+ virtual isc::dhcp::Pkt6Ptr receivePacket(int /*timeout*/) {
+
+ // If there is anything prepared as fake incoming
+ // traffic, use it
+ if (!fake_received_.empty()) {
+ Pkt6Ptr pkt = fake_received_.front();
+ fake_received_.pop_front();
+ return (pkt);
+ }
+
+ // If not, just trigger shutdown and
+ // return immediately
+ shutdown();
+ return (Pkt6Ptr());
+ }
+
+ /// @brief fake packet sending
+ ///
+ /// Pretend to send a packet, but instead just store
+ /// it in fake_send_ list where test can later inspect
+ /// server's response.
+ virtual void sendPacket(const Pkt6Ptr& pkt) {
+ fake_sent_.push_back(pkt);
+ }
+
+ /// @brief adds a packet to fake receive queue
+ ///
+ /// See fake_received_ field for description
+ void fakeReceive(const Pkt6Ptr& pkt) {
+ fake_received_.push_back(pkt);
+ }
+
+ virtual ~NakedDhcpv6Srv() {
+ // Close the lease database
+ LeaseMgrFactory::destroy();
+ }
+
+ using Dhcpv6Srv::processSolicit;
+ using Dhcpv6Srv::processRequest;
+ using Dhcpv6Srv::processRenew;
+ using Dhcpv6Srv::processRelease;
+ using Dhcpv6Srv::processClientFqdn;
+ using Dhcpv6Srv::createNameChangeRequests;
+ using Dhcpv6Srv::createRemovalNameChangeRequest;
+ using Dhcpv6Srv::createStatusCode;
+ using Dhcpv6Srv::selectSubnet;
+ using Dhcpv6Srv::sanityCheck;
+ using Dhcpv6Srv::classifyPacket;
+ using Dhcpv6Srv::loadServerID;
+ using Dhcpv6Srv::writeServerID;
+ using Dhcpv6Srv::unpackOptions;
+ using Dhcpv6Srv::name_change_reqs_;
+
+ /// @brief packets we pretend to receive
+ ///
+ /// Instead of setting up sockets on interfaces that change between OSes, it
+ /// is much easier to fake packet reception. This is a list of packets that
+ /// we pretend to have received. You can schedule new packets to be received
+ /// using fakeReceive() and NakedDhcpv6Srv::receivePacket() methods.
+ std::list<Pkt6Ptr> fake_received_;
+
+ std::list<Pkt6Ptr> fake_sent_;
+};
+
+static const char* DUID_FILE = "server-id-test.txt";
+
+// test fixture for any tests requiring blank/empty configuration
+// serves as base class for additional tests
+class NakedDhcpv6SrvTest : public ::testing::Test {
+public:
+
+ NakedDhcpv6SrvTest() : rcode_(-1) {
+ // it's ok if that fails. There should not be such a file anyway
+ unlink(DUID_FILE);
+
+ const IfaceMgr::IfaceCollection& ifaces = IfaceMgr::instance().getIfaces();
+
+ // There must be some interface detected
+ if (ifaces.empty()) {
+ // We can't use ASSERT in constructor
+ ADD_FAILURE() << "No interfaces detected.";
+ }
+
+ valid_iface_ = ifaces.begin()->getName();
+ }
+
+ // Generate IA_NA or IA_PD option with specified parameters
+ boost::shared_ptr<Option6IA> generateIA(uint16_t type, uint32_t iaid,
+ uint32_t t1, uint32_t t2);
+
+ /// @brief generates interface-id option, based on text
+ ///
+ /// @param iface_id textual representation of the interface-id content
+ ///
+ /// @return pointer to the option object
+ OptionPtr generateInterfaceId(const std::string& iface_id) {
+ OptionBuffer tmp(iface_id.begin(), iface_id.end());
+ return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, tmp));
+ }
+
+ // Generate client-id option
+ OptionPtr generateClientId(size_t duid_size = 32) {
+
+ OptionBuffer clnt_duid(duid_size);
+ for (int i = 0; i < duid_size; i++) {
+ clnt_duid[i] = 100 + i;
+ }
+
+ duid_ = DuidPtr(new DUID(clnt_duid));
+
+ return (OptionPtr(new Option(Option::V6, D6O_CLIENTID,
+ clnt_duid.begin(),
+ clnt_duid.begin() + duid_size)));
+ }
+
+ // Checks if server response (ADVERTISE or REPLY) includes proper server-id.
+ void checkServerId(const Pkt6Ptr& rsp, const OptionPtr& expected_srvid) {
+ // check that server included its server-id
+ OptionPtr tmp = rsp->getOption(D6O_SERVERID);
+ EXPECT_EQ(tmp->getType(), expected_srvid->getType() );
+ ASSERT_EQ(tmp->len(), expected_srvid->len() );
+ EXPECT_TRUE(tmp->getData() == expected_srvid->getData());
+ }
+
+ // Checks if server response (ADVERTISE or REPLY) includes proper client-id.
+ void checkClientId(const Pkt6Ptr& rsp, const OptionPtr& expected_clientid) {
+ // check that server included our own client-id
+ OptionPtr tmp = rsp->getOption(D6O_CLIENTID);
+ ASSERT_TRUE(tmp);
+ EXPECT_EQ(expected_clientid->getType(), tmp->getType());
+ ASSERT_EQ(expected_clientid->len(), tmp->len());
+
+ // check that returned client-id is valid
+ EXPECT_TRUE(expected_clientid->getData() == tmp->getData());
+ }
+
+ // Checks if server response is a NAK
+ void checkNakResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type,
+ uint32_t expected_transid,
+ uint16_t expected_status_code) {
+ // Check if we get response at all
+ checkResponse(rsp, expected_message_type, expected_transid);
+
+ // Check that IA_NA was returned
+ OptionPtr option_ia_na = rsp->getOption(D6O_IA_NA);
+ ASSERT_TRUE(option_ia_na);
+
+ // check that the status is no address available
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(option_ia_na);
+ ASSERT_TRUE(ia);
+
+ checkIA_NAStatusCode(ia, expected_status_code);
+ }
+
+ // Checks that server rejected IA_NA, i.e. that it has no addresses and
+ // that expected status code really appears there. In some limited cases
+ // (reply to RELEASE) it may be used to verify positive case, where
+ // IA_NA response is expected to not include address.
+ //
+ // Status code indicates type of error encountered (in theory it can also
+ // indicate success, but servers typically don't send success status
+ // as this is the default result and it saves bandwidth)
+ void checkIA_NAStatusCode(const boost::shared_ptr<Option6IA>& ia,
+ uint16_t expected_status_code) {
+ // Make sure there is no address assigned.
+ EXPECT_FALSE(ia->getOption(D6O_IAADDR));
+
+ // T1, T2 should be zeroed
+ EXPECT_EQ(0, ia->getT1());
+ EXPECT_EQ(0, ia->getT2());
+
+ OptionCustomPtr status =
+ boost::dynamic_pointer_cast<OptionCustom>(ia->getOption(D6O_STATUS_CODE));
+
+ // It is ok to not include status success as this is the default behavior
+ if (expected_status_code == STATUS_Success && !status) {
+ return;
+ }
+
+ EXPECT_TRUE(status);
+
+ if (status) {
+ // We don't have dedicated class for status code, so let's just interpret
+ // first 2 bytes as status. Remainder of the status code option content is
+ // just a text explanation what went wrong.
+ EXPECT_EQ(static_cast<uint16_t>(expected_status_code),
+ status->readInteger<uint16_t>(0));
+ }
+ }
+
+ void checkMsgStatusCode(const Pkt6Ptr& msg, uint16_t expected_status) {
+ OptionCustomPtr status =
+ boost::dynamic_pointer_cast<OptionCustom>(msg->getOption(D6O_STATUS_CODE));
+
+ // It is ok to not include status success as this is the default behavior
+ if (expected_status == STATUS_Success && !status) {
+ return;
+ }
+
+ EXPECT_TRUE(status);
+ if (status) {
+ // We don't have dedicated class for status code, so let's just interpret
+ // first 2 bytes as status. Remainder of the status code option content is
+ // just a text explanation what went wrong.
+ EXPECT_EQ(static_cast<uint16_t>(expected_status),
+ status->readInteger<uint16_t>(0));
+ }
+ }
+
+ // Basic checks for generated response (message type and transaction-id).
+ void checkResponse(const Pkt6Ptr& rsp, uint8_t expected_message_type,
+ uint32_t expected_transid) {
+ ASSERT_TRUE(rsp);
+ EXPECT_EQ(expected_message_type, rsp->getType());
+ EXPECT_EQ(expected_transid, rsp->getTransid());
+ }
+
+ virtual ~NakedDhcpv6SrvTest() {
+ // Let's clean up if there is such a file.
+ unlink(DUID_FILE);
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "buffer6_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "buffer6_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease6_renew");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease6_release");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "pkt6_receive");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "pkt6_send");
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "subnet6_select");
+
+ };
+
+ // A DUID used in most tests (typically as client-id)
+ DuidPtr duid_;
+
+ int rcode_;
+ ConstElementPtr comment_;
+
+ // Name of a valid network interface
+ std::string valid_iface_;
+};
+
+// Provides suport for tests against a preconfigured subnet6
+// extends upon NakedDhcp6SrvTest
+class Dhcpv6SrvTest : public NakedDhcpv6SrvTest {
+public:
+ /// Name of the server-id file (used in server-id tests)
+
+ /// @brief Constructor that initalizes a simple default configuration
+ ///
+ /// Sets up a single subnet6 with one pool for addresses and second
+ /// pool for prefixes.
+ Dhcpv6SrvTest() {
+ subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 48, 1000,
+ 2000, 3000, 4000));
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"),
+ 64));
+ subnet_->addPool(pool_);
+
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().addSubnet6(subnet_);
+
+ // configure PD pool
+ pd_pool_ = Pool6Ptr(new Pool6(Lease::TYPE_PD,
+ IOAddress("2001:db8:1:2::"), 64, 80));
+ subnet_->addPool(pd_pool_);
+ }
+
+ /// @brief destructor
+ ///
+ /// Removes existing configuration.
+ ~Dhcpv6SrvTest() {
+ CfgMgr::instance().deleteSubnets6();
+ };
+
+ /// @brief Checks that server response (ADVERTISE or REPLY) contains proper
+ /// IA_NA option
+ ///
+ /// @param rsp server's response
+ /// @param expected_iaid expected IAID value
+ /// @param expected_t1 expected T1 value
+ /// @param expected_t2 expected T2 value
+ /// @return IAADDR option for easy chaining with checkIAAddr method
+ boost::shared_ptr<Option6IAAddr>
+ checkIA_NA(const Pkt6Ptr& rsp, uint32_t expected_iaid,
+ uint32_t expected_t1, uint32_t expected_t2);
+
+ /// @brief Checks that server response (ADVERTISE or REPLY) contains proper
+ /// IA_PD option
+ ///
+ /// @param rsp server's response
+ /// @param expected_iaid expected IAID value
+ /// @param expected_t1 expected T1 value
+ /// @param expected_t2 expected T2 value
+ /// @return IAPREFIX option for easy chaining with checkIAAddr method
+ boost::shared_ptr<Option6IAPrefix>
+ checkIA_PD(const Pkt6Ptr& rsp, uint32_t expected_iaid,
+ uint32_t expected_t1, uint32_t expected_t2);
+
+ // Check that generated IAADDR option contains expected address
+ // and lifetime values match the configured subnet
+ void checkIAAddr(const boost::shared_ptr<Option6IAAddr>& addr,
+ const IOAddress& expected_addr,
+ Lease::Type type) {
+
+ // Check that the assigned address is indeed from the configured pool.
+ // Note that when comparing addresses, we compare the textual
+ // representation. IOAddress does not support being streamed to
+ // an ostream, which means it can't be used in EXPECT_EQ.
+ EXPECT_TRUE(subnet_->inPool(type, addr->getAddress()));
+ EXPECT_EQ(expected_addr.toText(), addr->getAddress().toText());
+ EXPECT_EQ(addr->getPreferred(), subnet_->getPreferred());
+ EXPECT_EQ(addr->getValid(), subnet_->getValid());
+ }
+
+ // Checks if the lease sent to client is present in the database
+ // and is valid when checked agasint the configured subnet
+ Lease6Ptr checkLease(const DuidPtr& duid, const OptionPtr& ia_na,
+ boost::shared_ptr<Option6IAAddr> addr);
+
+ /// @brief Verifies received IAPrefix option
+ ///
+ /// Verifies if the received IAPrefix option matches the lease in the
+ /// database.
+ ///
+ /// @param duid client's DUID
+ /// @param ia_pd IA_PD option that contains the IAPRefix option
+ /// @param prefix pointer to the IAPREFIX option
+ /// @return corresponding IPv6 lease (if found)
+ Lease6Ptr checkPdLease(const DuidPtr& duid, const OptionPtr& ia_pd,
+ boost::shared_ptr<Option6IAPrefix> prefix);
+
+ /// @brief Creates a message with specified IA
+ ///
+ /// A utility function that creates a message of the specified type with
+ /// a specified container (IA_NA or IA_PD) and an address or prefix
+ /// inside it.
+ ///
+ /// @param message_type type of the message (e.g. DHCPV6_SOLICIT)
+ /// @param lease_type type of a lease (TYPE_NA or TYPE_PD)
+ /// @param addr address or prefix to use in IADDRESS or IAPREFIX options
+ /// @param prefix_len length of the prefix (used for prefixes only)
+ /// @param iaid IA identifier (used in IA_XX option)
+ /// @return created message
+ Pkt6Ptr
+ createMessage(uint8_t message_type, Lease::Type lease_type,
+ const IOAddress& addr, const uint8_t prefix_len,
+ uint32_t iaid);
+
+ /// @brief Performs basic (positive) RENEW test
+ ///
+ /// See renewBasic and pdRenewBasic tests for detailed explanation.
+ /// In essence the test attempts to perform a successful RENEW scenario.
+ ///
+ /// This method does not throw, but uses gtest macros to signify failures.
+ ///
+ /// @param type type (TYPE_NA or TYPE_PD)
+ /// @param existing_addr address to be preinserted into the database
+ /// @param renew_addr address being sent in RENEW
+ /// @param prefix_len length of the prefix (128 for addresses)
+ void
+ testRenewBasic(Lease::Type type, const std::string& existing_addr,
+ const std::string& renew_addr, const uint8_t prefix_len);
+
+ /// @brief Performs negative RENEW test
+ ///
+ /// See renewReject and pdRenewReject tests for detailed explanation.
+ /// In essence the test attempts to perform couple failed RENEW scenarios.
+ ///
+ /// This method does not throw, but uses gtest macros to signify failures.
+ ///
+ /// @param type type (TYPE_NA or TYPE_PD)
+ /// @param addr address being sent in RENEW
+ void
+ testRenewReject(Lease::Type type, const IOAddress& addr);
+
+ /// @brief Performs basic (positive) RELEASE test
+ ///
+ /// See releaseBasic and pdReleaseBasic tests for detailed explanation.
+ /// In essence the test attempts to perform a successful RELEASE scenario.
+ ///
+ /// This method does not throw, but uses gtest macros to signify failures.
+ ///
+ /// @param type type (TYPE_NA or TYPE_PD)
+ /// @param existing address to be preinserted into the database
+ /// @param release_addr address being sent in RELEASE
+ void
+ testReleaseBasic(Lease::Type type, const IOAddress& existing,
+ const IOAddress& release_addr);
+
+ /// @brief Performs negative RELEASE test
+ ///
+ /// See releaseReject and pdReleaseReject tests for detailed explanation.
+ /// In essence the test attempts to perform couple failed RELEASE scenarios.
+ ///
+ /// This method does not throw, but uses gtest macros to signify failures.
+ ///
+ /// @param type type (TYPE_NA or TYPE_PD)
+ /// @param addr address being sent in RELEASE
+ void
+ testReleaseReject(Lease::Type type, const IOAddress& addr);
+
+ // see wireshark.cc for descriptions
+ // The descriptions are too large and too closely related to the
+ // code, so it is kept in .cc rather than traditionally in .h
+ Pkt6Ptr captureSimpleSolicit();
+ Pkt6Ptr captureRelayedSolicit();
+ Pkt6Ptr captureDocsisRelayedSolicit();
+ Pkt6Ptr captureeRouterRelayedSolicit();
+
+ /// @brief Auxiliary method that sets Pkt6 fields
+ ///
+ /// Used to reconstruct captured packets. Sets UDP ports, interface names,
+ /// and other fields to some believable values.
+ /// @param pkt packet that will have its fields set
+ void captureSetDefaultFields(const Pkt6Ptr& pkt);
+
+ /// A subnet used in most tests
+ Subnet6Ptr subnet_;
+
+ /// A normal, non-temporary pool used in most tests
+ Pool6Ptr pool_;
+
+ /// A prefix pool used in most tests
+ Pool6Ptr pd_pool_;
+};
+
+}; // end of isc::test namespace
+}; // end of isc namespace
+
+#endif // DHCP6_TEST_UTILS_H
diff --git a/src/bin/dhcp6/tests/fqdn_unittest.cc b/src/bin/dhcp6/tests/fqdn_unittest.cc
new file mode 100644
index 0000000..8a2da69
--- /dev/null
+++ b/src/bin/dhcp6/tests/fqdn_unittest.cc
@@ -0,0 +1,778 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option6_client_fqdn.h>
+#include <dhcp/option6_ia.h>
+#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option_int_array.h>
+#include <dhcpsrv/lease.h>
+
+#include <dhcp6/tests/dhcp6_test_utils.h>
+#include <boost/pointer_cast.hpp>
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::test;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp_ddns;
+using namespace isc::util;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+/// @brief A test fixture class for testing DHCPv6 Client FQDN Option handling.
+class FqdnDhcpv6SrvTest : public Dhcpv6SrvTest {
+public:
+
+ /// @brief Constructor
+ FqdnDhcpv6SrvTest()
+ : Dhcpv6SrvTest() {
+ // generateClientId assigns DUID to duid_.
+ generateClientId();
+ lease_.reset(new Lease6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ duid_, 1234, 501, 502, 503,
+ 504, 1, 0));
+
+ }
+
+ /// @brief Destructor
+ virtual ~FqdnDhcpv6SrvTest() {
+ }
+
+ /// @brief Construct the DHCPv6 Client FQDN option using flags and
+ /// domain-name.
+ ///
+ /// @param flags Flags to be set for the created option.
+ /// @param fqdn_name A name which should be stored in the option.
+ /// @param fqdn_type A type of the name carried by the option: partial
+ /// or fully qualified.
+ ///
+ /// @return A pointer to the created option.
+ Option6ClientFqdnPtr
+ createClientFqdn(const uint8_t flags,
+ const std::string& fqdn_name,
+ const Option6ClientFqdn::DomainNameType fqdn_type) {
+ return (Option6ClientFqdnPtr(new Option6ClientFqdn(flags,
+ fqdn_name,
+ fqdn_type)));
+ }
+
+ /// @brief Create a message with or without DHCPv6 Client FQDN Option.
+ ///
+ /// @param msg_type A type of the DHCPv6 message to be created.
+ /// @param fqdn_flags Flags to be carried in the FQDN option.
+ /// @param fqdn_domain_name A name to be carried in the FQDN option.
+ /// @param fqdn_type A type of the name carried by the option: partial
+ /// or fully qualified.
+ /// @param include_oro A boolean value which indicates whether the ORO
+ /// option should be added to the message (if true).
+ /// @param srvid server id to be stored in the message.
+ ///
+ /// @return An object representing the created message.
+ Pkt6Ptr generateMessage(uint8_t msg_type,
+ const uint8_t fqdn_flags,
+ const std::string& fqdn_domain_name,
+ const Option6ClientFqdn::DomainNameType
+ fqdn_type,
+ const bool include_oro,
+ OptionPtr srvid = OptionPtr()) {
+ Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(msg_type, 1234));
+ pkt->setRemoteAddr(IOAddress("fe80::abcd"));
+ Option6IAPtr ia = generateIA(D6O_IA_NA, 234, 1500, 3000);
+
+ if (msg_type != DHCPV6_REPLY) {
+ IOAddress hint("2001:db8:1:1::dead:beef");
+ OptionPtr hint_opt(new Option6IAAddr(D6O_IAADDR, hint, 300, 500));
+ ia->addOption(hint_opt);
+ pkt->addOption(ia);
+ }
+
+ OptionPtr clientid = generateClientId();
+ pkt->addOption(clientid);
+ if (srvid && (msg_type != DHCPV6_SOLICIT)) {
+ pkt->addOption(srvid);
+ }
+
+ pkt->addOption(createClientFqdn(fqdn_flags, fqdn_domain_name,
+ fqdn_type));
+
+ if (include_oro) {
+ OptionUint16ArrayPtr oro(new OptionUint16Array(Option::V6,
+ D6O_ORO));
+ oro->addValue(D6O_CLIENT_FQDN);
+ pkt->addOption(oro);
+ }
+
+ return (pkt);
+ }
+
+ /// @brief Creates instance of the DHCPv6 message with client id and
+ /// server id.
+ ///
+ /// @param msg_type A type of the message to be created.
+ /// @param srv An object representing the DHCPv6 server, which
+ /// is used to generate the client identifier.
+ ///
+ /// @return An object representing the created message.
+ Pkt6Ptr generateMessageWithIds(const uint8_t msg_type,
+ NakedDhcpv6Srv& srv) {
+ Pkt6Ptr pkt = Pkt6Ptr(new Pkt6(msg_type, 1234));
+ // Generate client-id.
+ OptionPtr opt_clientid = generateClientId();
+ pkt->addOption(opt_clientid);
+
+ if (msg_type != DHCPV6_SOLICIT) {
+ // Generate server-id.
+ pkt->addOption(srv.getServerID());
+ }
+
+ return (pkt);
+ }
+
+ /// @brief Returns an instance of the option carrying FQDN.
+ ///
+ /// @param pkt A message holding FQDN option to be returned.
+ ///
+ /// @return An object representing DHCPv6 Client FQDN option.
+ Option6ClientFqdnPtr getClientFqdnOption(const Pkt6Ptr& pkt) {
+ return (boost::dynamic_pointer_cast<Option6ClientFqdn>
+ (pkt->getOption(D6O_CLIENT_FQDN)));
+ }
+
+ /// @brief Adds IA option to the message.
+ ///
+ /// Addded option holds an address.
+ ///
+ /// @param iaid IAID
+ /// @param pkt A DHCPv6 message to which the IA option should be added.
+ void addIA(const uint32_t iaid, const IOAddress& addr, Pkt6Ptr& pkt) {
+ Option6IAPtr opt_ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+ Option6IAAddrPtr opt_iaaddr(new Option6IAAddr(D6O_IAADDR, addr,
+ 300, 500));
+ opt_ia->addOption(opt_iaaddr);
+ pkt->addOption(opt_ia);
+ }
+
+
+ /// @brief Adds IA option to the message.
+ ///
+ /// Added option holds status code.
+ ///
+ /// @param iaid IAID
+ /// @param status_code Status code
+ /// @param pkt A DHCPv6 message to which the option should be added.
+ void addIA(const uint32_t iaid, const uint16_t status_code, Pkt6Ptr& pkt) {
+ Option6IAPtr opt_ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+ addStatusCode(status_code, "", opt_ia);
+ pkt->addOption(opt_ia);
+ }
+
+ /// @brief Creates status code with the specified code and message.
+ ///
+ /// @param code A status code.
+ /// @param msg A string representation of the message to be added to the
+ /// Status Code option.
+ ///
+ /// @return An object representing the Status Code option.
+ OptionCustomPtr createStatusCode(const uint16_t code,
+ const std::string& msg) {
+ OptionDefinition def("status-code", D6O_STATUS_CODE, "record");
+ def.addRecordField("uint16");
+ def.addRecordField("string");
+ OptionCustomPtr opt_status(new OptionCustom(def, Option::V6));
+ opt_status->writeInteger(code);
+ if (!msg.empty()) {
+ opt_status->writeString(msg, 1);
+ }
+ return (opt_status);
+ }
+
+ /// @brief Adds Status Code option to the IA.
+ ///
+ /// @param code A status code value.
+ /// @param msg A string representation of the message to be added to the
+ /// Status Code option.
+ void addStatusCode(const uint16_t code, const std::string& msg,
+ Option6IAPtr& opt_ia) {
+ opt_ia->addOption(createStatusCode(code, msg));
+ }
+
+ /// @brief Verifies if the DHCPv6 server processes DHCPv6 Client FQDN option
+ /// as expected.
+ ///
+ /// This function simulates generation of the client's message holding FQDN.
+ /// It then calls the server's @c Dhcpv6Srv::processClientFqdn option to
+ /// generate server's response to the FQDN. This function returns the FQDN
+ /// which should be appended to the server's response to the client.
+ /// This function verifies that the FQDN option returned is correct.
+ ///
+ /// @param msg_type A type of the client's message.
+ /// @param use_oro A boolean value which indicates whether the DHCPv6 ORO
+ /// option (requesting return of the FQDN option by the server) should be
+ /// included in the client's message (if true), or not included (if false).
+ /// @param in_flags A value of flags field to be set for the FQDN carried
+ /// in the client's message.
+ /// @param in_domain_name A domain name to be carried in the client's FQDN
+ /// option.
+ /// @param in_domain_type A type of the domain name to be carried in the
+ /// client's FQDM option (partial or fully qualified).
+ /// @param exp_flags A value of flags expected in the FQDN sent by a server.
+ /// @param exp_domain_name A domain name expected in the FQDN sent by a
+ /// server.
+ void testFqdn(const uint16_t msg_type,
+ const bool use_oro,
+ const uint8_t in_flags,
+ const std::string& in_domain_name,
+ const Option6ClientFqdn::DomainNameType in_domain_type,
+ const uint8_t exp_flags,
+ const std::string& exp_domain_name) {
+ NakedDhcpv6Srv srv(0);
+ Pkt6Ptr question = generateMessage(msg_type,
+ in_flags,
+ in_domain_name,
+ in_domain_type,
+ use_oro);
+ ASSERT_TRUE(getClientFqdnOption(question));
+
+ Option6ClientFqdnPtr answ_fqdn;
+ ASSERT_NO_THROW(answ_fqdn = srv.processClientFqdn(question));
+ ASSERT_TRUE(answ_fqdn);
+
+ const bool flag_n = (exp_flags & Option6ClientFqdn::FLAG_N) != 0;
+ const bool flag_s = (exp_flags & Option6ClientFqdn::FLAG_S) != 0;
+ const bool flag_o = (exp_flags & Option6ClientFqdn::FLAG_O) != 0;
+
+ EXPECT_EQ(flag_n, answ_fqdn->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ(flag_s, answ_fqdn->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_EQ(flag_o, answ_fqdn->getFlag(Option6ClientFqdn::FLAG_O));
+
+ EXPECT_EQ(exp_domain_name, answ_fqdn->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, answ_fqdn->getDomainNameType());
+ }
+
+ /// @brief Tests that the client's message holding an FQDN is processed
+ /// and that lease is acquired.
+ ///
+ /// @param msg_type A type of the client's message.
+ /// @param hostname A domain name in the client's FQDN.
+ /// @param srv A server object, used to process the message.
+ /// @param include_oro A boolean value which indicates whether the ORO
+ /// option should be included in the client's message (if true) or not
+ /// (if false). In the former case, the function will expect that server
+ /// responds with the FQDN option. In the latter case, the function expects
+ /// that the server doesn't respond with the FQDN.
+ void testProcessMessage(const uint8_t msg_type,
+ const std::string& hostname,
+ NakedDhcpv6Srv& srv,
+ const bool include_oro = true) {
+ // Create a message of a specified type, add server id and
+ // FQDN option.
+ OptionPtr srvid = srv.getServerID();
+ Pkt6Ptr req = generateMessage(msg_type, Option6ClientFqdn::FLAG_S,
+ hostname,
+ Option6ClientFqdn::FULL,
+ include_oro, srvid);
+
+ // For different client's message types we have to invoke different
+ // functions to generate response.
+ Pkt6Ptr reply;
+ if (msg_type == DHCPV6_SOLICIT) {
+ ASSERT_NO_THROW(reply = srv.processSolicit(req));
+
+ } else if (msg_type == DHCPV6_REQUEST) {
+ ASSERT_NO_THROW(reply = srv.processRequest(req));
+
+ } else if (msg_type == DHCPV6_RENEW) {
+ ASSERT_NO_THROW(reply = srv.processRequest(req));
+
+ } else if (msg_type == DHCPV6_RELEASE) {
+ // For Release no lease will be acquired so we have to leave
+ // function here.
+ ASSERT_NO_THROW(reply = srv.processRelease(req));
+ return;
+ } else {
+ // We are not interested in testing other message types.
+ return;
+ }
+
+ // For Solicit, we will get different message type obviously.
+ if (msg_type == DHCPV6_SOLICIT) {
+ checkResponse(reply, DHCPV6_ADVERTISE, 1234);
+
+ } else {
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+ }
+
+ // Check verify that IA_NA is correct.
+ Option6IAAddrPtr addr =
+ checkIA_NA(reply, 234, subnet_->getT1(), subnet_->getT2());
+ ASSERT_TRUE(addr);
+
+ // Check that we have got the address we requested.
+ checkIAAddr(addr, IOAddress("2001:db8:1:1::dead:beef"),
+ Lease::TYPE_NA);
+
+ if (msg_type != DHCPV6_SOLICIT) {
+ // Check that the lease exists.
+ Lease6Ptr lease =
+ checkLease(duid_, reply->getOption(D6O_IA_NA), addr);
+ ASSERT_TRUE(lease);
+ }
+
+ if (include_oro) {
+ ASSERT_TRUE(reply->getOption(D6O_CLIENT_FQDN));
+ } else {
+ ASSERT_FALSE(reply->getOption(D6O_CLIENT_FQDN));
+ }
+ }
+
+ /// @brief Verify that NameChangeRequest holds valid values.
+ ///
+ /// This function picks first NameChangeRequest from the internal server's
+ /// queue and checks that it holds valid parameters. The NameChangeRequest
+ /// is removed from the queue.
+ ///
+ /// @param srv A server object holding a queue of NameChangeRequests.
+ /// @param type An expected type of the NameChangeRequest (Add or Remove).
+ /// @param reverse An expected setting of the reverse update flag.
+ /// @param forward An expected setting of the forward udpate flag.
+ /// @param addr A string representation of the IPv6 address held in the
+ /// NameChangeRequest.
+ /// @param dhcid An expected DHCID value.
+ /// @param expires A timestamp when the lease associated with the
+ /// NameChangeRequest expires.
+ /// @param len A valid lifetime of the lease associated with the
+ /// NameChangeRequest.
+ void verifyNameChangeRequest(NakedDhcpv6Srv& srv,
+ const isc::dhcp_ddns::NameChangeType type,
+ const bool reverse, const bool forward,
+ const std::string& addr,
+ const std::string& dhcid,
+ const uint16_t expires,
+ const uint16_t len) {
+ NameChangeRequest ncr = srv.name_change_reqs_.front();
+ EXPECT_EQ(type, ncr.getChangeType());
+ EXPECT_EQ(forward, ncr.isForwardChange());
+ EXPECT_EQ(reverse, ncr.isReverseChange());
+ EXPECT_EQ(addr, ncr.getIpAddress());
+ EXPECT_EQ(dhcid, ncr.getDhcid().toStr());
+ EXPECT_EQ(expires, ncr.getLeaseExpiresOn());
+ EXPECT_EQ(len, ncr.getLeaseLength());
+ EXPECT_EQ(isc::dhcp_ddns::ST_NEW, ncr.getStatus());
+ srv.name_change_reqs_.pop();
+ }
+
+ // Holds a lease used by a test.
+ Lease6Ptr lease_;
+
+};
+
+// A set of tests verifying server's behaviour when it receives the DHCPv6
+// Client Fqdn Option.
+// @todo: Extend these tests once appropriate configuration parameters are
+// implemented (ticket #3034).
+
+// Test server's response when client requests that server performs AAAA update.
+TEST_F(FqdnDhcpv6SrvTest, serverAAAAUpdate) {
+ testFqdn(DHCPV6_SOLICIT, true, Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL, Option6ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+}
+
+// Test server's response when client provides partial domain-name and requests
+// that server performs AAAA update.
+TEST_F(FqdnDhcpv6SrvTest, serverAAAAUpdatePartialName) {
+ testFqdn(DHCPV6_SOLICIT, true, Option6ClientFqdn::FLAG_S, "myhost",
+ Option6ClientFqdn::PARTIAL, Option6ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+}
+
+// Test server's response when client provides empty domain-name and requests
+// that server performs AAAA update.
+TEST_F(FqdnDhcpv6SrvTest, serverAAAAUpdateNoName) {
+ testFqdn(DHCPV6_SOLICIT, true, Option6ClientFqdn::FLAG_S, "",
+ Option6ClientFqdn::PARTIAL, Option6ClientFqdn::FLAG_S,
+ "myhost.example.com.");
+}
+
+// Test server's response when client requests no DNS update.
+TEST_F(FqdnDhcpv6SrvTest, noUpdate) {
+ testFqdn(DHCPV6_SOLICIT, true, Option6ClientFqdn::FLAG_N,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL, Option6ClientFqdn::FLAG_N,
+ "myhost.example.com.");
+}
+
+// Test server's response when client requests that server delegates the AAAA
+// update to the client and this delegation is not allowed.
+TEST_F(FqdnDhcpv6SrvTest, clientAAAAUpdateNotAllowed) {
+ testFqdn(DHCPV6_SOLICIT, true, 0, "myhost.example.com.",
+ Option6ClientFqdn::FULL,
+ Option6ClientFqdn::FLAG_S | Option6ClientFqdn::FLAG_O,
+ "myhost.example.com.");
+}
+
+// Test that exception is thrown if supplied NULL answer packet when
+// creating NameChangeRequests.
+TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAnswer) {
+ NakedDhcpv6Srv srv(0);
+
+ Pkt6Ptr answer;
+ Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL);
+ EXPECT_THROW(srv.createNameChangeRequests(answer, fqdn),
+ isc::Unexpected);
+
+}
+
+// Test that exception is thrown if supplied answer from the server
+// contains no DUID when creating NameChangeRequests.
+TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoDUID) {
+ NakedDhcpv6Srv srv(0);
+
+ Pkt6Ptr answer = Pkt6Ptr(new Pkt6(DHCPV6_REPLY, 1234));
+ Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL);
+
+ EXPECT_THROW(srv.createNameChangeRequests(answer, fqdn),
+ isc::Unexpected);
+
+}
+
+// Test no NameChangeRequests are added if FQDN option is NULL.
+TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoFQDN) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create Reply message with Client Id and Server id.
+ Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+
+ // Pass NULL FQDN option. No NameChangeRequests should be created.
+ Option6ClientFqdnPtr fqdn;
+ ASSERT_NO_THROW(srv.createNameChangeRequests(answer, fqdn));
+
+ // There should be no new NameChangeRequests.
+ EXPECT_TRUE(srv.name_change_reqs_.empty());
+}
+
+// Test that NameChangeRequests are not generated if an answer message
+// contains no addresses.
+TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequestsNoAddr) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create Reply message with Client Id and Server id.
+ Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+
+ Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL);
+
+ ASSERT_NO_THROW(srv.createNameChangeRequests(answer, fqdn));
+
+ // We didn't add any IAs, so there should be no NameChangeRequests in th
+ // queue.
+ ASSERT_TRUE(srv.name_change_reqs_.empty());
+}
+
+// Test that a number of NameChangeRequests is created as a result of
+// processing the answer message which holds 3 IAs and when FQDN is
+// specified.
+TEST_F(FqdnDhcpv6SrvTest, createNameChangeRequests) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create Reply message with Client Id and Server id.
+ Pkt6Ptr answer = generateMessageWithIds(DHCPV6_REPLY, srv);
+
+ // Create three IAs, each having different address.
+ addIA(1234, IOAddress("2001:db8:1::1"), answer);
+ addIA(2345, IOAddress("2001:db8:1::2"), answer);
+ addIA(3456, IOAddress("2001:db8:1::3"), answer);
+
+ // Use domain name in upper case. It should be converted to lower-case
+ // before DHCID is calculated. So, we should get the same result as if
+ // we typed domain name in lower-case.
+ Option6ClientFqdnPtr fqdn = createClientFqdn(Option6ClientFqdn::FLAG_S,
+ "MYHOST.EXAMPLE.COM",
+ Option6ClientFqdn::FULL);
+
+ // Create NameChangeRequests. Since we have added 3 IAs, it should
+ // result in generation of 3 distinct NameChangeRequests.
+ ASSERT_NO_THROW(srv.createNameChangeRequests(answer, fqdn));
+ ASSERT_EQ(3, srv.name_change_reqs_.size());
+
+ // Verify that NameChangeRequests are correct. Each call to the
+ // verifyNameChangeRequest will pop verified request from the queue.
+
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1::1",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 500);
+
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1::2",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 500);
+
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1::3",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 500);
+
+}
+
+// Test creation of the NameChangeRequest to remove both forward and reverse
+// mapping for the given lease.
+TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestFwdRev) {
+ NakedDhcpv6Srv srv(0);
+
+ lease_->fqdn_fwd_ = true;
+ lease_->fqdn_rev_ = true;
+ // Part of the domain name is in upper case, to test that it gets converted
+ // to lower case before DHCID is computed. So, we should get the same DHCID
+ // as if we typed domain-name in lower case.
+ lease_->hostname_ = "MYHOST.example.com.";
+
+ ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "2001:db8:1::1",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 502);
+
+}
+
+// Test creation of the NameChangeRequest to remove reverse mapping for the
+// given lease.
+TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestRev) {
+ NakedDhcpv6Srv srv(0);
+
+ lease_->fqdn_fwd_ = false;
+ lease_->fqdn_rev_ = true;
+ lease_->hostname_ = "myhost.example.com.";
+
+ ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, false,
+ "2001:db8:1::1",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 502);
+
+}
+
+// Test that NameChangeRequest to remove DNS records is not generated when
+// neither forward nor reverse DNS update has been performed for a lease.
+TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestNoUpdate) {
+ NakedDhcpv6Srv srv(0);
+
+ lease_->fqdn_fwd_ = false;
+ lease_->fqdn_rev_ = false;
+
+ ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+
+ EXPECT_TRUE(srv.name_change_reqs_.empty());
+
+}
+
+// Test that NameChangeRequest is not generated if the hostname hasn't been
+// specified for a lease for which forward and reverse mapping has been set.
+TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestNoHostname) {
+ NakedDhcpv6Srv srv(0);
+
+ lease_->fqdn_fwd_ = true;
+ lease_->fqdn_rev_ = true;
+ lease_->hostname_ = "";
+
+ ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+
+ EXPECT_TRUE(srv.name_change_reqs_.empty());
+
+}
+
+// Test that NameChangeRequest is not generated if the invalid hostname has
+// been specified for a lease for which forward and reverse mapping has been
+// set.
+TEST_F(FqdnDhcpv6SrvTest, createRemovalNameChangeRequestWrongHostname) {
+ NakedDhcpv6Srv srv(0);
+
+ lease_->fqdn_fwd_ = true;
+ lease_->fqdn_rev_ = true;
+ lease_->hostname_ = "myhost..example.com.";
+
+ ASSERT_NO_THROW(srv.createRemovalNameChangeRequest(lease_));
+
+ EXPECT_TRUE(srv.name_change_reqs_.empty());
+
+}
+
+// Test that Advertise message generated in a response to the Solicit will
+// not result in generation if the NameChangeRequests.
+TEST_F(FqdnDhcpv6SrvTest, processSolicit) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create a Solicit message with FQDN option and generate server's
+ // response using processSolicit function.
+ testProcessMessage(DHCPV6_SOLICIT, "myhost.example.com", srv);
+ EXPECT_TRUE(srv.name_change_reqs_.empty());
+}
+
+// Test that client may send two requests, each carrying FQDN option with
+// a different domain-name. Server should use existing lease for the second
+// request but modify the DNS entries for the lease according to the contents
+// of the FQDN sent in the second request.
+TEST_F(FqdnDhcpv6SrvTest, processTwoRequests) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create a Request message with FQDN option and generate server's
+ // response using processRequest function. This will result in the
+ // creation of a new lease and the appropriate NameChangeRequest
+ // to add both reverse and forward mapping to DNS.
+ testProcessMessage(DHCPV6_REQUEST, "myhost.example.com", srv);
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+
+ // Client may send another request message with a new domain-name. In this
+ // case the same lease will be returned. The existing DNS entry needs to
+ // be replaced with a new one. Server should determine that the different
+ // FQDN has been already added to the DNS. As a result, the old DNS
+ // entries should be removed and the entries for the new domain-name
+ // should be added. Therefore, we expect two NameChangeRequests. One to
+ // remove the existing entries, one to add new entries.
+ testProcessMessage(DHCPV6_REQUEST, "otherhost.example.com", srv);
+ ASSERT_EQ(2, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201D422AA463306223D269B6CB7AFE7AAD265FC"
+ "EA97F93623019B2E0D14E5323D5A",
+ 0, 4000);
+
+}
+
+// Test that client may send Request followed by the Renew, both holding
+// FQDN options, but each option holding different domain-name. The Renew
+// should result in generation of the two NameChangeRequests, one to remove
+// DNS entry added previously when Request was processed, another one to
+// add a new entry for the FQDN held in the Renew.
+TEST_F(FqdnDhcpv6SrvTest, processRequestRenew) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create a Request message with FQDN option and generate server's
+ // response using processRequest function. This will result in the
+ // creation of a new lease and the appropriate NameChangeRequest
+ // to add both reverse and forward mapping to DNS.
+ testProcessMessage(DHCPV6_REQUEST, "myhost.example.com", srv);
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+
+ // Client may send Renew message with a new domain-name. In this
+ // case the same lease will be returned. The existing DNS entry needs to
+ // be replaced with a new one. Server should determine that the different
+ // FQDN has been already added to the DNS. As a result, the old DNS
+ // entries should be removed and the entries for the new domain-name
+ // should be added. Therefore, we expect two NameChangeRequests. One to
+ // remove the existing entries, one to add new entries.
+ testProcessMessage(DHCPV6_RENEW, "otherhost.example.com", srv);
+ ASSERT_EQ(2, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201D422AA463306223D269B6CB7AFE7AAD265FC"
+ "EA97F93623019B2E0D14E5323D5A",
+ 0, 4000);
+
+}
+
+TEST_F(FqdnDhcpv6SrvTest, processRequestRelease) {
+ NakedDhcpv6Srv srv(0);
+
+ // Create a Request message with FQDN option and generate server's
+ // response using processRequest function. This will result in the
+ // creation of a new lease and the appropriate NameChangeRequest
+ // to add both reverse and forward mapping to DNS.
+ testProcessMessage(DHCPV6_REQUEST, "myhost.example.com", srv);
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+
+ // Client may send Release message. In this case the lease should be
+ // removed and all existing DNS entries for this lease should be
+ // also removed. Therefore, we expect that single NameChangeRequest to
+ // remove DNS entries is generated.
+ testProcessMessage(DHCPV6_RELEASE, "otherhost.example.com", srv);
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_REMOVE, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+
+}
+
+// Checks that the server does not include DHCPv6 Client FQDN option in its
+// response when client doesn't include ORO option in the Request.
+TEST_F(FqdnDhcpv6SrvTest, processRequestWithoutFqdn) {
+ NakedDhcpv6Srv srv(0);
+
+ // The last parameter disables use of the ORO to request FQDN option
+ // In this case, we expect that the FQDN option will not be included
+ // in the server's response. The testProcessMessage will check that.
+ testProcessMessage(DHCPV6_REQUEST, "myhost.example.com", srv, false);
+ ASSERT_EQ(1, srv.name_change_reqs_.size());
+ verifyNameChangeRequest(srv, isc::dhcp_ddns::CHG_ADD, true, true,
+ "2001:db8:1:1::dead:beef",
+ "000201415AA33D1187D148275136FA30300478"
+ "FAAAA3EBD29826B5C907B2C9268A6F52",
+ 0, 4000);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp6/tests/hooks_unittest.cc b/src/bin/dhcp6/tests/hooks_unittest.cc
new file mode 100644
index 0000000..d4e8e5e
--- /dev/null
+++ b/src/bin/dhcp6/tests/hooks_unittest.cc
@@ -0,0 +1,1434 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/duid.h>
+#include <dhcp6/config_parser.h>
+#include <dhcp/dhcp6.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/lease_mgr_factory.h>
+#include <dhcpsrv/utils.h>
+#include <util/buffer.h>
+#include <util/range_utilities.h>
+#include <hooks/server_hooks.h>
+
+#include <dhcp6/tests/dhcp6_test_utils.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+#include <unistd.h>
+#include <fstream>
+#include <iostream>
+#include <sstream>
+
+using namespace isc;
+using namespace isc::test;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc::hooks;
+using namespace std;
+
+// namespace has to be named, because friends are defined in Dhcpv6Srv class
+// Maybe it should be isc::test?
+namespace {
+
+// Checks if hooks are implemented properly.
+TEST_F(Dhcpv6SrvTest, Hooks) {
+ NakedDhcpv6Srv srv(0);
+
+ // check if appropriate hooks are registered
+ int hook_index_buffer6_receive = -1;
+ int hook_index_buffer6_send = -1;
+ int hook_index_lease6_renew = -1;
+ int hook_index_lease6_release = -1;
+ int hook_index_pkt6_received = -1;
+ int hook_index_select_subnet = -1;
+ int hook_index_pkt6_send = -1;
+
+ // check if appropriate indexes are set
+ EXPECT_NO_THROW(hook_index_buffer6_receive = ServerHooks::getServerHooks()
+ .getIndex("buffer6_receive"));
+ EXPECT_NO_THROW(hook_index_buffer6_send = ServerHooks::getServerHooks()
+ .getIndex("buffer6_send"));
+ EXPECT_NO_THROW(hook_index_lease6_renew = ServerHooks::getServerHooks()
+ .getIndex("lease6_renew"));
+ EXPECT_NO_THROW(hook_index_lease6_release = ServerHooks::getServerHooks()
+ .getIndex("lease6_release"));
+ EXPECT_NO_THROW(hook_index_pkt6_received = ServerHooks::getServerHooks()
+ .getIndex("pkt6_receive"));
+ EXPECT_NO_THROW(hook_index_select_subnet = ServerHooks::getServerHooks()
+ .getIndex("subnet6_select"));
+ EXPECT_NO_THROW(hook_index_pkt6_send = ServerHooks::getServerHooks()
+ .getIndex("pkt6_send"));
+
+ EXPECT_TRUE(hook_index_pkt6_received > 0);
+ EXPECT_TRUE(hook_index_select_subnet > 0);
+ EXPECT_TRUE(hook_index_pkt6_send > 0);
+ EXPECT_TRUE(hook_index_buffer6_receive > 0);
+ EXPECT_TRUE(hook_index_buffer6_send > 0);
+ EXPECT_TRUE(hook_index_lease6_renew > 0);
+ EXPECT_TRUE(hook_index_lease6_release > 0);
+}
+
+/// @brief a class dedicated to Hooks testing in DHCPv6 server
+///
+/// This class has a number of static members, because each non-static
+/// method has implicit 'this' parameter, so it does not match callout
+/// signature and couldn't be registered. Furthermore, static methods
+/// can't modify non-static members (for obvious reasons), so many
+/// fields are declared static. It is still better to keep them as
+/// one class rather than unrelated collection of global objects.
+class HooksDhcpv6SrvTest : public Dhcpv6SrvTest {
+
+public:
+
+ /// @brief creates Dhcpv6Srv and prepares buffers for callouts
+ HooksDhcpv6SrvTest() {
+
+ // Allocate new DHCPv6 Server
+ srv_.reset(new NakedDhcpv6Srv(0));
+
+ // Clear static buffers
+ resetCalloutBuffers();
+ }
+
+ /// @brief destructor (deletes Dhcpv6Srv)
+ ~HooksDhcpv6SrvTest() {
+ }
+
+ /// @brief creates an option with specified option code
+ ///
+ /// This method is static, because it is used from callouts
+ /// that do not have a pointer to HooksDhcpv6SSrvTest object
+ ///
+ /// @param option_code code of option to be created
+ ///
+ /// @return pointer to create option object
+ static OptionPtr createOption(uint16_t option_code) {
+
+ uint8_t payload[] = {
+ 0xa, 0xb, 0xc, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14
+ };
+
+ OptionBuffer tmp(payload, payload + sizeof(payload));
+ return OptionPtr(new Option(Option::V6, option_code, tmp));
+ }
+
+ /// test callback that stores received callout name and pkt6 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt6_receive");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// test callback that changes client-id value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_receive_change_clientid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("query6", pkt);
+
+ // Get rid of the old client-id
+ pkt->delOption(D6O_CLIENTID);
+
+ // Add a new option
+ pkt->addOption(createOption(D6O_CLIENTID));
+
+ // Carry on as usual
+ return pkt6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that deletes client-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("query6", pkt);
+
+ // Get rid of the old client-id
+ pkt->delOption(D6O_CLIENTID);
+
+ // Carry on as usual
+ return pkt6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_receive_skip(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("query6", pkt);
+
+ callout_handle.setSkip(true);
+
+ // Carry on as usual
+ return pkt6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that stores received callout name and pkt6 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer6_receive_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("buffer6_receive");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that changes first byte of client-id value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer6_receive_change_clientid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("query6", pkt);
+
+ // If there is at least one option with data
+ if (pkt->data_.size() > Pkt6::DHCPV6_PKT_HDR_LEN + Option::OPTION6_HDR_LEN) {
+ // Offset of the first byte of the first option. Let's set this byte
+ // to some new value that we could later check
+ pkt->data_[Pkt6::DHCPV6_PKT_HDR_LEN + Option::OPTION6_HDR_LEN] = 0xff;
+ }
+
+ // Carry on as usual
+ return buffer6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that deletes client-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer6_receive_delete_clientid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("query6", pkt);
+
+ // this is modified SOLICIT (with missing mandatory client-id)
+ uint8_t data[] = {
+ 1, // type 1 = SOLICIT
+ 0xca, 0xfe, 0x01, // trans-id = 0xcafe01
+ 0, 3, // option type 3 (IA_NA)
+ 0, 12, // option length 12
+ 0, 0, 0, 1, // iaid = 1
+ 0, 0, 0, 0, // T1 = 0
+ 0, 0, 0, 0 // T2 = 0
+ };
+
+ OptionBuffer modifiedMsg(data, data + sizeof(data));
+
+ pkt->data_ = modifiedMsg;
+
+ // carry on as usual
+ return buffer6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ buffer6_receive_skip(CalloutHandle& callout_handle) {
+ callout_handle.setSkip(true);
+
+ // Carry on as usual
+ return buffer6_receive_callout(callout_handle);
+ }
+
+ /// Test callback that stores received callout name and pkt6 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_send_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("pkt6_send");
+
+ callout_handle.getArgument("response6", callback_pkt6_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ // Test callback that changes server-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_send_change_serverid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("response6", pkt);
+
+ // Get rid of the old server-id
+ pkt->delOption(D6O_SERVERID);
+
+ // Add a new option
+ pkt->addOption(createOption(D6O_SERVERID));
+
+ // Carry on as usual
+ return pkt6_send_callout(callout_handle);
+ }
+
+ /// Test callback that deletes server-id
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_send_delete_serverid(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("response6", pkt);
+
+ // Get rid of the old client-id
+ pkt->delOption(D6O_SERVERID);
+
+ // Carry on as usual
+ return pkt6_send_callout(callout_handle);
+ }
+
+ /// Test callback that sets skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ pkt6_send_skip(CalloutHandle& callout_handle) {
+
+ Pkt6Ptr pkt;
+ callout_handle.getArgument("response6", pkt);
+
+ callout_handle.setSkip(true);
+
+ // carry on as usual
+ return pkt6_send_callout(callout_handle);
+ }
+
+ /// Test callback that stores received callout name and subnet6 values
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet6_select_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("subnet6_select");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+ callout_handle.getArgument("subnet6", callback_subnet6_);
+ callout_handle.getArgument("subnet6collection", callback_subnet6collection_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that picks the other subnet if possible.
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ subnet6_select_different_subnet_callout(CalloutHandle& callout_handle) {
+
+ // Call the basic calllout to record all passed values
+ subnet6_select_callout(callout_handle);
+
+ const Subnet6Collection* subnets;
+ Subnet6Ptr subnet;
+ callout_handle.getArgument("subnet6", subnet);
+ callout_handle.getArgument("subnet6collection", subnets);
+
+ // Let's change to a different subnet
+ if (subnets->size() > 1) {
+ subnet = (*subnets)[1]; // Let's pick the other subnet
+ callout_handle.setArgument("subnet6", subnet);
+ }
+
+ return (0);
+ }
+
+ /// Test callback that stores received callout name and pkt6 value
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease6_renew_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease6_renew");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+ callout_handle.getArgument("lease6", callback_lease6_);
+ callout_handle.getArgument("ia_na", callback_ia_na_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// The following values are used by the callout to override
+ /// renewed lease parameters
+ static const uint32_t override_iaid_;
+ static const uint32_t override_t1_;
+ static const uint32_t override_t2_;
+ static const uint32_t override_preferred_;
+ static const uint32_t override_valid_;
+
+ /// Test callback that overrides received lease. It updates
+ /// T1, T2, preferred and valid lifetimes
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease6_renew_update_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease6_renew");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+ callout_handle.getArgument("lease6", callback_lease6_);
+ callout_handle.getArgument("ia_na", callback_ia_na_);
+
+ // Let's override some values in the lease
+ callback_lease6_->iaid_ = override_iaid_;
+ callback_lease6_->t1_ = override_t1_;
+ callback_lease6_->t2_ = override_t2_;
+ callback_lease6_->preferred_lft_ = override_preferred_;
+ callback_lease6_->valid_lft_ = override_valid_;
+
+ // Override the values to be sent to the client as well
+ callback_ia_na_->setIAID(override_iaid_);
+ callback_ia_na_->setT1(override_t1_);
+ callback_ia_na_->setT2(override_t2_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that sets the skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease6_renew_skip_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease6_renew");
+
+ callout_handle.setSkip(true);
+
+ return (0);
+ }
+
+ /// Test callback that stores received callout name passed parameters
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease6_release_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease6_release");
+
+ callout_handle.getArgument("query6", callback_pkt6_);
+ callout_handle.getArgument("lease6", callback_lease6_);
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// Test callback that sets the skip flag
+ /// @param callout_handle handle passed by the hooks framework
+ /// @return always 0
+ static int
+ lease6_release_skip_callout(CalloutHandle& callout_handle) {
+ callback_name_ = string("lease6_release");
+
+ callout_handle.setSkip(true);
+
+ return (0);
+ }
+
+ /// Resets buffers used to store data received by callouts
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_pkt6_.reset();
+ callback_subnet6_.reset();
+ callback_lease6_.reset();
+ callback_ia_na_.reset();
+ callback_subnet6collection_ = NULL;
+ callback_argument_names_.clear();
+ }
+
+ /// Pointer to Dhcpv6Srv that is used in tests
+ boost::scoped_ptr<NakedDhcpv6Srv> srv_;
+
+ // The following fields are used in testing pkt6_receive_callout
+
+ /// String name of the received callout
+ static string callback_name_;
+
+ /// Pkt6 structure returned in the callout
+ static Pkt6Ptr callback_pkt6_;
+
+ /// Pointer to lease6
+ static Lease6Ptr callback_lease6_;
+
+ /// Pointer to IA_NA option being renewed
+ static boost::shared_ptr<Option6IA> callback_ia_na_;
+
+ /// Pointer to a subnet received by callout
+ static Subnet6Ptr callback_subnet6_;
+
+ /// A list of all available subnets (received by callout)
+ static const Subnet6Collection* callback_subnet6collection_;
+
+ /// A list of all received arguments
+ static vector<string> callback_argument_names_;
+};
+
+// The following parameters are used by callouts to override
+// renewed lease parameters
+const uint32_t HooksDhcpv6SrvTest::override_iaid_ = 1000;
+const uint32_t HooksDhcpv6SrvTest::override_t1_ = 1001;
+const uint32_t HooksDhcpv6SrvTest::override_t2_ = 1002;
+const uint32_t HooksDhcpv6SrvTest::override_preferred_ = 1003;
+const uint32_t HooksDhcpv6SrvTest::override_valid_ = 1004;
+
+// The following fields are used in testing pkt6_receive_callout.
+// See fields description in the class for details
+string HooksDhcpv6SrvTest::callback_name_;
+Pkt6Ptr HooksDhcpv6SrvTest::callback_pkt6_;
+Subnet6Ptr HooksDhcpv6SrvTest::callback_subnet6_;
+const Subnet6Collection* HooksDhcpv6SrvTest::callback_subnet6collection_;
+vector<string> HooksDhcpv6SrvTest::callback_argument_names_;
+Lease6Ptr HooksDhcpv6SrvTest::callback_lease6_;
+boost::shared_ptr<Option6IA> HooksDhcpv6SrvTest::callback_ia_na_;
+
+// Checks if callouts installed on pkt6_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "buffer6_receive".
+TEST_F(HooksDhcpv6SrvTest, simple_buffer6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer6_receive", buffer6_receive_callout));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered buffer6_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("buffer6_receive", callback_name_);
+
+ // Check that pkt6 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query6"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on buffer6_receive is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv6SrvTest, valueChange_buffer6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer6_receive", buffer6_receive_change_clientid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server did send a reposonce
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt6Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(D6O_CLIENTID);
+
+ ASSERT_TRUE(clientid);
+
+ // ... and check if it is the modified value
+ EXPECT_EQ(0xff, clientid->getData()[0]);
+}
+
+// Checks if callouts installed on buffer6_receive is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv6SrvTest, deleteClientId_buffer6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer6_receive", buffer6_receive_delete_clientid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not send a response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on buffer6_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_buffer6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "buffer6_receive", buffer6_receive_skip));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt6_receive are indeed called and the
+// all necessary parameters are passed.
+//
+// Note that the test name does not follow test naming convention,
+// but the proper hook name is "pkt6_receive".
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_receive", pkt6_receive_callout));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt6_receive", callback_name_);
+
+ // Check that pkt6 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("query6"));
+
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_received is able to change
+// the values and the parameters are indeed used by the server.
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_receive", pkt6_receive_change_clientid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server did send a reposonce
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt6Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(D6O_CLIENTID);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(D6O_CLIENTID);
+ EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_received is able to delete
+// existing options and that change impacts server processing (mandatory
+// client-id option is deleted, so the packet is expected to be dropped)
+TEST_F(HooksDhcpv6SrvTest, deleteClientId_pkt6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_receive", pkt6_receive_delete_clientid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not send a response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+// Checks if callouts installed on pkt6_received is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_receive) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_receive", pkt6_receive_skip));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server dropped the packet and did not produce any response
+ ASSERT_EQ(0, srv_->fake_sent_.size());
+}
+
+
+// Checks if callouts installed on pkt6_send are indeed called and the
+// all necessary parameters are passed.
+TEST_F(HooksDhcpv6SrvTest, simple_pkt6_send) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_send", pkt6_send_callout));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("pkt6_send", callback_name_);
+
+ // Check that there is one packet sent
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+ Pkt6Ptr adv = srv_->fake_sent_.front();
+
+ // Check that pkt6 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt6_.get() == adv.get());
+
+ // Check that all expected parameters are there
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back(string("response6"));
+ EXPECT_TRUE(expected_argument_names == callback_argument_names_);
+}
+
+// Checks if callouts installed on pkt6_send is able to change
+// the values and the packet sent contains those changes
+TEST_F(HooksDhcpv6SrvTest, valueChange_pkt6_send) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_send", pkt6_send_change_serverid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server did send a response
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Make sure that we received a response
+ Pkt6Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Get client-id...
+ OptionPtr clientid = adv->getOption(D6O_SERVERID);
+
+ // ... and check if it is the modified value
+ OptionPtr expected = createOption(D6O_SERVERID);
+ EXPECT_TRUE(clientid->equal(expected));
+}
+
+// Checks if callouts installed on pkt6_send is able to delete
+// existing options and that server applies those changes. In particular,
+// we are trying to send a packet without server-id. The packet should
+// be sent
+TEST_F(HooksDhcpv6SrvTest, deleteServerId_pkt6_send) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_send", pkt6_send_delete_serverid));
+
+ // Let's create a simple SOLICIT
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server indeed sent a malformed ADVERTISE
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // Get that ADVERTISE
+ Pkt6Ptr adv = srv_->fake_sent_.front();
+ ASSERT_TRUE(adv);
+
+ // Make sure that it does not have server-id
+ EXPECT_FALSE(adv->getOption(D6O_SERVERID));
+}
+
+// Checks if callouts installed on pkt6_skip is able to set skip flag that
+// will cause the server to not process the packet (drop), even though it is valid.
+TEST_F(HooksDhcpv6SrvTest, skip_pkt6_send) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "pkt6_send", pkt6_send_skip));
+
+ // Let's create a simple REQUEST
+ Pkt6Ptr sol = Pkt6Ptr(captureSimpleSolicit());
+
+ // Simulate that we have received that traffic
+ srv_->fakeReceive(sol);
+
+ // Server will now process to run its normal loop, but instead of calling
+ // IfaceMgr::receive6(), it will read all packets from the list set by
+ // fakeReceive()
+ // In particular, it should call registered pkt6_receive callback.
+ srv_->run();
+
+ // Check that the server send the packet
+ ASSERT_EQ(1, srv_->fake_sent_.size());
+
+ // But the sent packet should have 0 length (we told the server to
+ // skip pack(), but did not do packing outselves)
+ Pkt6Ptr sent = srv_->fake_sent_.front();
+
+ // The actual size of sent packet should be 0
+ EXPECT_EQ(0, sent->getBuffer().getLength());
+}
+
+// This test checks if subnet6_select callout is triggered and reports
+// valid parameters
+TEST_F(HooksDhcpv6SrvTest, subnet6_select) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet6_select", subnet6_select_callout));
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/64\" ],"
+ " \"subnet\": \"2001:db8:1::/48\", "
+ " \"interface\": \"" + valid_iface_ + "\" "
+ " }, {"
+ " \"pool\": [ \"2001:db8:2::/64\" ],"
+ " \"subnet\": \"2001:db8:2::/48\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Prepare solicit packet. Server should select first subnet for it
+ Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ sol->setRemoteAddr(IOAddress("fe80::abcd"));
+ sol->setIface(valid_iface_);
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt6Ptr adv = srv_->processSolicit(sol);
+
+ // Check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("subnet6_select", callback_name_);
+
+ // Check that pkt6 argument passing was successful and returned proper value
+ EXPECT_TRUE(callback_pkt6_.get() == sol.get());
+
+ const Subnet6Collection* exp_subnets = CfgMgr::instance().getSubnets6();
+
+ // The server is supposed to pick the first subnet, because of matching
+ // interface. Check that the value is reported properly.
+ ASSERT_TRUE(callback_subnet6_);
+ EXPECT_EQ(callback_subnet6_.get(), exp_subnets->front().get());
+
+ // Server is supposed to report two subnets
+ ASSERT_EQ(exp_subnets->size(), callback_subnet6collection_->size());
+
+ // Compare that the available subnets are reported as expected
+ EXPECT_TRUE((*exp_subnets)[0].get() == (*callback_subnet6collection_)[0].get());
+ EXPECT_TRUE((*exp_subnets)[1].get() == (*callback_subnet6collection_)[1].get());
+}
+
+// This test checks if callout installed on subnet6_select hook point can pick
+// a different subnet.
+TEST_F(HooksDhcpv6SrvTest, subnet_select_change) {
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "subnet6_select", subnet6_select_different_subnet_callout));
+
+ // Configure 2 subnets, both directly reachable over local interface
+ // (let's not complicate the matter with relays)
+ string config = "{ \"interfaces\": [ \"*\" ],"
+ "\"preferred-lifetime\": 3000,"
+ "\"rebind-timer\": 2000, "
+ "\"renew-timer\": 1000, "
+ "\"subnet6\": [ { "
+ " \"pool\": [ \"2001:db8:1::/64\" ],"
+ " \"subnet\": \"2001:db8:1::/48\", "
+ " \"interface\": \"" + valid_iface_ + "\" "
+ " }, {"
+ " \"pool\": [ \"2001:db8:2::/64\" ],"
+ " \"subnet\": \"2001:db8:2::/48\" "
+ " } ],"
+ "\"valid-lifetime\": 4000 }";
+
+ ElementPtr json = Element::fromJSON(config);
+ ConstElementPtr status;
+
+ // Configure the server and make sure the config is accepted
+ EXPECT_NO_THROW(status = configureDhcp6Server(*srv_, json));
+ ASSERT_TRUE(status);
+ comment_ = parseAnswer(rcode_, status);
+ ASSERT_EQ(0, rcode_);
+
+ // Prepare solicit packet. Server should select first subnet for it
+ Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234));
+ sol->setRemoteAddr(IOAddress("fe80::abcd"));
+ sol->setIface(valid_iface_);
+ sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000));
+ OptionPtr clientid = generateClientId();
+ sol->addOption(clientid);
+
+ // Pass it to the server and get an advertise
+ Pkt6Ptr adv = srv_->processSolicit(sol);
+
+ // Check if we get response at all
+ ASSERT_TRUE(adv);
+
+ // The response should have an address from second pool, so let's check it
+ OptionPtr tmp = adv->getOption(D6O_IA_NA);
+ ASSERT_TRUE(tmp);
+ boost::shared_ptr<Option6IA> ia = boost::dynamic_pointer_cast<Option6IA>(tmp);
+ ASSERT_TRUE(ia);
+ tmp = ia->getOption(D6O_IAADDR);
+ ASSERT_TRUE(tmp);
+ boost::shared_ptr<Option6IAAddr> addr_opt =
+ boost::dynamic_pointer_cast<Option6IAAddr>(tmp);
+ ASSERT_TRUE(addr_opt);
+
+ // Get all subnets and use second subnet for verification
+ const Subnet6Collection* subnets = CfgMgr::instance().getSubnets6();
+ ASSERT_EQ(2, subnets->size());
+
+ // Advertised address must belong to the second pool (in subnet's range,
+ // in dynamic pool)
+ EXPECT_TRUE((*subnets)[1]->inRange(addr_opt->getAddress()));
+ EXPECT_TRUE((*subnets)[1]->inPool(Lease::TYPE_NA, addr_opt->getAddress()));
+}
+
+// This test verifies that incoming (positive) RENEW can be handled properly,
+// and the lease6_renew callouts are triggered.
+TEST_F(HooksDhcpv6SrvTest, basic_lease6_renew) {
+ NakedDhcpv6Srv srv(0);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_renew", lease6_renew_callout));
+
+ const IOAddress addr("2001:db8:1:1::cafe:babe");
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(), 0));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt really set and not using
+ // previous (500, 501, etc.) values
+ EXPECT_NE(l->t1_, subnet_->getT1());
+ EXPECT_NE(l->t2_, subnet_->getT2());
+ EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_NE(l->valid_lft_, subnet_->getValid());
+ EXPECT_NE(l->cltt_, time(NULL));
+
+ // Let's create a RENEW
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+
+ OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ ia->addOption(renewed_addr_opt);
+ req->addOption(ia);
+ req->addOption(clientid);
+
+ // Server-id is mandatory in RENEW
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRenew(req);
+ ASSERT_TRUE(reply);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease6_renew", callback_name_);
+
+ // Check that appropriate parameters are passed to the callouts
+ EXPECT_TRUE(callback_pkt6_);
+ EXPECT_TRUE(callback_lease6_);
+ EXPECT_TRUE(callback_ia_na_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query6");
+ expected_argument_names.push_back("lease6");
+ expected_argument_names.push_back("ia_na");
+
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+
+ OptionPtr tmp = reply->getOption(D6O_IA_NA);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAAddr> addr_opt = checkIA_NA(reply, 234, subnet_->getT1(),
+ subnet_->getT2());
+
+ ASSERT_TRUE(addr_opt);
+ // Check that the lease is really in the database
+ l = checkLease(duid_, reply->getOption(D6O_IA_NA), addr_opt);
+ ASSERT_TRUE(l);
+
+ // Check that the lease has been returned
+ ASSERT_TRUE(callback_lease6_);
+
+ // Check that the returned lease6 in callout is the same as the one in the
+ // database
+ EXPECT_TRUE(*callback_lease6_ == *l);
+}
+
+// This test verifies that incoming (positive) RENEW can be handled properly,
+// and the lease6_renew callouts are able to change the lease being updated.
+TEST_F(HooksDhcpv6SrvTest, leaseUpdate_lease6_renew) {
+ NakedDhcpv6Srv srv(0);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_renew", lease6_renew_update_callout));
+
+ const IOAddress addr("2001:db8:1:1::cafe:babe");
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(), 0));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt really set and not using
+ // previous (500, 501, etc.) values
+ EXPECT_NE(l->t1_, subnet_->getT1());
+ EXPECT_NE(l->t2_, subnet_->getT2());
+ EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_NE(l->valid_lft_, subnet_->getValid());
+ EXPECT_NE(l->cltt_, time(NULL));
+
+ // Let's create a RENEW
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+
+ OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ ia->addOption(renewed_addr_opt);
+ req->addOption(ia);
+ req->addOption(clientid);
+
+ // Server-id is mandatory in RENEW
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRenew(req);
+ ASSERT_TRUE(reply);
+
+ // Check if we get response at all
+ checkResponse(reply, DHCPV6_REPLY, 1234);
+
+ OptionPtr tmp = reply->getOption(D6O_IA_NA);
+ ASSERT_TRUE(tmp);
+
+ // Check that IA_NA was returned and that there's an address included
+ boost::shared_ptr<Option6IAAddr> addr_opt = checkIA_NA(reply, 1000, 1001, 1002);
+
+ ASSERT_TRUE(addr_opt);
+ // Check that the lease is really in the database
+ l = checkLease(duid_, reply->getOption(D6O_IA_NA), addr_opt);
+ ASSERT_TRUE(l);
+
+ // Check that we chose the distinct override values
+ ASSERT_NE(override_t1_, subnet_->getT1());
+ ASSERT_NE(override_t2_, subnet_->getT2());
+ ASSERT_NE(override_preferred_, subnet_->getPreferred());
+ EXPECT_NE(override_valid_, subnet_->getValid());
+
+ // Check that T1, T2, preferred, valid were overridden the the callout
+ EXPECT_EQ(override_t1_, l->t1_);
+ EXPECT_EQ(override_t2_, l->t2_);
+ EXPECT_EQ(override_preferred_, l->preferred_lft_);
+ EXPECT_EQ(override_valid_, l->valid_lft_);
+
+ // Checking for CLTT is a bit tricky if we want to avoid off by 1 errors
+ int32_t cltt = static_cast<int32_t>(l->cltt_);
+ int32_t expected = static_cast<int32_t>(time(NULL));
+ // Equality or difference by 1 between cltt and expected is ok.
+ EXPECT_GE(1, abs(cltt - expected));
+
+ EXPECT_TRUE(LeaseMgrFactory::instance().deleteLease(addr_opt->getAddress()));
+}
+
+// This test verifies that incoming (positive) RENEW can be handled properly,
+// and the lease6_renew callouts are able to set the skip flag that will
+// reject the renewal
+TEST_F(HooksDhcpv6SrvTest, skip_lease6_renew) {
+ NakedDhcpv6Srv srv(0);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_renew", lease6_renew_skip_callout));
+
+ const IOAddress addr("2001:db8:1:1::cafe:babe");
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(), 0));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Check that T1, T2, preferred, valid and cltt really set and not using
+ // previous (500, 501, etc.) values
+ EXPECT_NE(l->t1_, subnet_->getT1());
+ EXPECT_NE(l->t2_, subnet_->getT2());
+ EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_NE(l->valid_lft_, subnet_->getValid());
+ EXPECT_NE(l->cltt_, time(NULL));
+
+ // Let's create a RENEW
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RENEW, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+
+ OptionPtr renewed_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ ia->addOption(renewed_addr_opt);
+ req->addOption(ia);
+ req->addOption(clientid);
+
+ // Server-id is mandatory in RENEW
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRenew(req);
+ ASSERT_TRUE(reply);
+
+ // Check that our callback was called
+ EXPECT_EQ("lease6_renew", callback_name_);
+
+ l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, addr);
+
+ // Check that the old values are still there and they were not
+ // updated by the renewal
+ EXPECT_NE(l->t1_, subnet_->getT1());
+ EXPECT_NE(l->t2_, subnet_->getT2());
+ EXPECT_NE(l->preferred_lft_, subnet_->getPreferred());
+ EXPECT_NE(l->valid_lft_, subnet_->getValid());
+ EXPECT_NE(l->cltt_, time(NULL));
+}
+
+// This test verifies that incoming (positive) RELEASE can be handled properly,
+// that a REPLY is generated, that the response has status code and that the
+// lease is indeed removed from the database.
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA that does not include an IAADDR
+// - lease is actually removed from LeaseMgr
+TEST_F(HooksDhcpv6SrvTest, basic_lease6_release) {
+ NakedDhcpv6Srv srv(0);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_release", lease6_release_callout));
+
+ const IOAddress addr("2001:db8:1:1::cafe:babe");
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(), 0));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RELEASE, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+
+ OptionPtr released_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ ia->addOption(released_addr_opt);
+ req->addOption(ia);
+ req->addOption(clientid);
+
+ // Server-id is mandatory in RELEASE
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRelease(req);
+
+ ASSERT_TRUE(reply);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease6_release", callback_name_);
+
+ // Check that appropriate parameters are passed to the callouts
+ EXPECT_TRUE(callback_pkt6_);
+ EXPECT_TRUE(callback_lease6_);
+
+ // Check if all expected parameters were really received
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("query6");
+ expected_argument_names.push_back("lease6");
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+
+ // Check that the lease is really gone in the database
+ // get lease by address
+ l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, addr);
+ ASSERT_FALSE(l);
+
+ // Get lease by subnetid/duid/iaid combination
+ l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, *duid_, iaid,
+ subnet_->getID());
+ ASSERT_FALSE(l);
+}
+
+// This test verifies that incoming (positive) RELEASE can be handled properly,
+// that a REPLY is generated, that the response has status code and that the
+// lease is indeed removed from the database.
+//
+// expected:
+// - returned REPLY message has copy of client-id
+// - returned REPLY message has server-id
+// - returned REPLY message has IA that does not include an IAADDR
+// - lease is actually removed from LeaseMgr
+TEST_F(HooksDhcpv6SrvTest, skip_lease6_release) {
+ NakedDhcpv6Srv srv(0);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_release", lease6_release_skip_callout));
+
+ const IOAddress addr("2001:db8:1:1::cafe:babe");
+ const uint32_t iaid = 234;
+
+ // Generate client-id also duid_
+ OptionPtr clientid = generateClientId();
+
+ // Check that the address we are about to use is indeed in pool
+ ASSERT_TRUE(subnet_->inPool(Lease::TYPE_NA, addr));
+
+ // Note that preferred, valid, T1 and T2 timers and CLTT are set to invalid
+ // value on purpose. They should be updated during RENEW.
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, duid_, iaid,
+ 501, 502, 503, 504, subnet_->getID(), 0));
+ lease->cltt_ = 1234;
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
+
+ // Check that the lease is really in the database
+ Lease6Ptr l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Let's create a RELEASE
+ Pkt6Ptr req = Pkt6Ptr(new Pkt6(DHCPV6_RELEASE, 1234));
+ req->setRemoteAddr(IOAddress("fe80::abcd"));
+ boost::shared_ptr<Option6IA> ia = generateIA(D6O_IA_NA, iaid, 1500, 3000);
+
+ OptionPtr released_addr_opt(new Option6IAAddr(D6O_IAADDR, addr, 300, 500));
+ ia->addOption(released_addr_opt);
+ req->addOption(ia);
+ req->addOption(clientid);
+
+ // Server-id is mandatory in RELEASE
+ req->addOption(srv.getServerID());
+
+ // Pass it to the server and hope for a REPLY
+ Pkt6Ptr reply = srv.processRelease(req);
+
+ ASSERT_TRUE(reply);
+
+ // Check that the callback called is indeed the one we installed
+ EXPECT_EQ("lease6_release", callback_name_);
+
+ // Check that the lease is still there
+ // get lease by address
+ l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
+ ASSERT_TRUE(l);
+
+ // Get lease by subnetid/duid/iaid combination
+ l = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA, *duid_, iaid,
+ subnet_->getID());
+ ASSERT_TRUE(l);
+}
+
+} // end of anonymous namespace
diff --git a/src/bin/dhcp6/tests/marker_file.cc b/src/bin/dhcp6/tests/marker_file.cc
new file mode 100644
index 0000000..d1c4aba
--- /dev/null
+++ b/src/bin/dhcp6/tests/marker_file.cc
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include "marker_file.h"
+
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+using namespace std;
+
+// Check the marker file.
+
+bool
+checkMarkerFile(const char* name, const char* expected) {
+ // Open the file for input
+ fstream file(name, fstream::in);
+
+ // Is it open?
+ if (!file.is_open()) {
+ ADD_FAILURE() << "Unable to open " << name << ". It was expected "
+ << "to be present and to contain the string '"
+ << expected << "'";
+ return (false);
+ }
+
+ // OK, is open, so read the data and see what we have. Compare it
+ // against what is expected.
+ string content;
+ getline(file, content);
+
+ string expected_str(expected);
+ EXPECT_EQ(expected_str, content) << "Marker file " << name
+ << "did not contain the expected data";
+ file.close();
+
+ return (expected_str == content);
+}
+
+// Check if the marker file exists - this is a wrapper for "access(2)" and
+// really tests if the file exists and is accessible
+
+bool
+checkMarkerFileExists(const char* name) {
+ return (access(name, F_OK) == 0);
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/bin/dhcp6/tests/marker_file.h.in b/src/bin/dhcp6/tests/marker_file.h.in
new file mode 100644
index 0000000..52fc006
--- /dev/null
+++ b/src/bin/dhcp6/tests/marker_file.h.in
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef MARKER_FILE_H
+#define MARKER_FILE_H
+
+/// @file
+/// Define a marker file that is used in tests to prove that an "unload"
+/// function has been called
+
+namespace {
+const char* const LOAD_MARKER_FILE = "@abs_builddir@/load_marker.txt";
+const char* const UNLOAD_MARKER_FILE = "@abs_builddir@/unload_marker.txt";
+}
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Check marker file
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Marker files are used by the load/unload functions in the hooks
+/// libraries in these tests to signal whether they have been loaded or
+/// unloaded. The file (if present) contains a single line holding
+/// a set of characters.
+///
+/// This convenience function checks the file to see if the characters
+/// are those expected.
+///
+/// @param name Name of the marker file.
+/// @param expected Characters expected. If a marker file is present,
+/// it is expected to contain characters.
+///
+/// @return true if all tests pass, false if not (in which case a failure
+/// will have been logged).
+bool
+checkMarkerFile(const char* name, const char* expected);
+
+/// @brief Check marker file exists
+///
+/// This function is used in some of the DHCP server tests.
+///
+/// Checkes that the specified file does NOT exist.
+///
+/// @param name Name of the marker file.
+///
+/// @return true if file exists, false if not.
+bool
+checkMarkerFileExists(const char* name);
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+#endif // MARKER_FILE_H
+
diff --git a/src/bin/dhcp6/tests/test_data_files_config.h.in b/src/bin/dhcp6/tests/test_data_files_config.h.in
new file mode 100644
index 0000000..8b09164
--- /dev/null
+++ b/src/bin/dhcp6/tests/test_data_files_config.h.in
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @brief Path to DHCP6 source dir so tests against the dhcp6.spec file
+/// can find it reliably.
+
+#ifndef TEST_DATA_FILES_CONFIG_H
+#define TEST_DATA_FILES_CONFIG_H
+
+#define DHCP6_SRC_DIR "@abs_top_srcdir@/src/bin/dhcp6"
+
+#endif
diff --git a/src/bin/dhcp6/tests/test_libraries.h.in b/src/bin/dhcp6/tests/test_libraries.h.in
new file mode 100644
index 0000000..f25d9e2
--- /dev/null
+++ b/src/bin/dhcp6/tests/test_libraries.h.in
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// shared library.
+
+// Library with load/unload functions creating marker files to check their
+// operation.
+const char* const CALLOUT_LIBRARY_1 = "@abs_builddir@/.libs/libco1.so";
+const char* const CALLOUT_LIBRARY_2 = "@abs_builddir@/.libs/libco2.so";
+
+// Name of a library which is not present.
+const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/bin/dhcp6/tests/wireshark.cc b/src/bin/dhcp6/tests/wireshark.cc
new file mode 100644
index 0000000..cd29096
--- /dev/null
+++ b/src/bin/dhcp6/tests/wireshark.cc
@@ -0,0 +1,303 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp6/tests/dhcp6_test_utils.h>
+#include <string>
+
+/// @file wireshark.cc
+///
+/// @brief contains packet captures imported from Wireshark
+///
+/// These are actual packets captured over wire. They are used in various
+/// tests.
+///
+/// The procedure to export Wireshark -> unit-tests is manual, but rather
+/// easy to follow:
+/// 1. Open a file in wireshark
+/// 2. Find the packet you want to export
+/// 3. There's a protocol stack (Frame, Ethernet, IPv6, UDP, DHCPv6, ...)
+/// 4. Right click on DHCPv6 -> Copy -> Bytes -> Hex Stream
+/// 5. Paste it as: string hex_string="[paste here]";
+/// 6. Coding guidelines line restrictions apply, so wrap your code as necessary
+/// 7. Make sure you decribe the capture appropriately
+/// 8. Follow whatever rest of the methods are doing (set ports, ifaces etc.)
+/// 9. To easily copy packet description, click File... -> Extract packet
+/// dissections -> as plain text file...
+/// (Make sure that the packet is expanded in the view. The text file will
+/// contain whatever expansion level you have in the graphical tree.)
+
+using namespace std;
+
+namespace isc {
+namespace test {
+
+void Dhcpv6SrvTest::captureSetDefaultFields(const Pkt6Ptr& pkt) {
+ pkt->setRemotePort(546);
+ pkt->setRemoteAddr(IOAddress("fe80::1"));
+ pkt->setLocalPort(0);
+ pkt->setLocalAddr(IOAddress("ff02::1:2"));
+ pkt->setIndex(2);
+ pkt->setIface("eth0");
+}
+
+// This function returns buffer for very simple Solicit
+Pkt6Ptr Dhcpv6SrvTest::captureSimpleSolicit() {
+ uint8_t data[] = {
+ 1, // type 1 = SOLICIT
+ 0xca, 0xfe, 0x01, // trans-id = 0xcafe01
+ 0, 1, // option type 1 (client-id)
+ 0, 10, // option lenth 10
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // DUID
+ 0, 3, // option type 3 (IA_NA)
+ 0, 12, // option length 12
+ 0, 0, 0, 1, // iaid = 1
+ 0, 0, 0, 0, // T1 = 0
+ 0, 0, 0, 0 // T2 = 0
+ };
+
+ Pkt6Ptr pkt(new Pkt6(data, sizeof(data)));
+ captureSetDefaultFields(pkt);
+
+ return (pkt);
+}
+
+Pkt6Ptr Dhcpv6SrvTest::captureRelayedSolicit() {
+
+ // This is a very simple relayed SOLICIT message:
+ // RELAY-FORW
+ // - interface-id
+ // - relay-message
+ // - SOLICIT
+ // - client-id
+ // - IA_NA (iaid=1, t1=0, t2=0)
+ // - ORO (7)
+
+ // string exported from Wireshark
+ string hex_string =
+ "0c0500000000000000000000000000000000fc00000000000000000000000000000900"
+ "12000231350009002c010517100001000e0001000151b5e46208002758f1e80003000c"
+ "000000010000000000000000000600020007";
+
+ std::vector<uint8_t> bin;
+
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(hex_string, bin);
+
+ Pkt6Ptr pkt(new Pkt6(&bin[0], bin.size()));
+ captureSetDefaultFields(pkt);
+
+ return (pkt);
+}
+
+/// returns a buffer with relayed SOLICIT (from DOCSIS3.0 cable modem)
+Pkt6Ptr isc::test::Dhcpv6SrvTest::captureDocsisRelayedSolicit() {
+
+ // This is an actual DOCSIS packet
+ // RELAY-FORW (12)
+ // - Relay Message
+ // - SOLICIT (1)
+ // - client-id
+ // - IA_NA (iaid=7f000788, t2=0, t2=0)
+ // - IAAddress (::, pref=0,valid=0)
+ // - rapid-commit
+ // - ORO
+ // - Reconfigure-accept
+ // - Vendor-Class ("docsis3.0")
+ // - Vendor-specific Info
+ // - subopt 1: Option request = 32,33,34,37,38
+ // - subopt 36: Device identifier
+ // - subopt 35: TLV5
+ // - subopt 2: Device type = ECM
+ // - subopt 3: Embedded components
+ // - subopt 4: Serial Number
+ // - subopt 5: Hardware version
+ // - subopt 6: Software version
+ // - subopt 7: Boot ROM Version
+ // - subopt 8: Organization Unique Identifier
+ // - subopt 9: Model Number
+ // - subopt 10: Vendor Name (Netgear)
+ // - subopt 15: unknown
+ // - Interface-Id
+ // - Vendor-specific Information
+ // - Suboption 1025: CMTS capabilities
+ // - Suboption 1026: Cable Modem MAC addr = 10:0d:7f:00:07:88
+
+ // string exported from Wireshark
+ string hex_string =
+ "0c002a0288fe00fe00015a8d09fffe7af955fe80000000000000120d7ffffe00078800"
+ "090189010d397f0001000a00030001100d7f000788000300287f000788000000000000"
+ "000000050018000000000000000000000000000000000000000000000000000e000000"
+ "0800020000000600020011001400000010000f0000118b0009646f63736973332e3000"
+ "1101200000118b0001000a0020002100220025002600240006100d7f00078800230081"
+ "0101010201030301010401010501010601010701180801080901000a01010b01180c01"
+ "010d0200400e0200100f01011004000000021101011301011401001501381601011701"
+ "011801041901041a01041b01281c01021d01081e01201f011020011821010222010123"
+ "010124011825010126020040270101120701100d7f00078a0002000345434d0003000b"
+ "45434d3a45524f555445520004000d3335463132395550303030353200050004332e31"
+ "310006000956312e30312e31315400070013505350552d426f6f7420312e302e31362e"
+ "323200080006303030393542000900084347343030305444000a00074e657467656172"
+ "000f000745524f5554455200120012427531264361312f3000100d7f00078800000011"
+ "00160000118b040100040102030004020006100d7f000788";
+
+ std::vector<uint8_t> bin;
+
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(hex_string, bin);
+
+ Pkt6Ptr pkt(new Pkt6(&bin[0], bin.size()));
+ captureSetDefaultFields(pkt);
+ return (pkt);
+}
+
+/// returns a buffer with relayed SOLICIT (from DOCSIS3.0 eRouter)
+Pkt6Ptr isc::test::Dhcpv6SrvTest::captureeRouterRelayedSolicit() {
+
+/* Packet description exported from wireshark:
+DHCPv6
+ Message type: Relay-forw (12)
+ Hopcount: 0
+ Link address: 2001:558:ffa8::1 (2001:558:ffa8::1)
+ Peer address: fe80::22e5:2aff:feb8:1515 (fe80::22e5:2aff:feb8:1515)
+ Relay Message
+ Option: Relay Message (9)
+ Length: 241
+ Value: 01a90044000e000000140000000600080011001700180019...
+ DHCPv6
+ Message type: Solicit (1)
+ Transaction ID: 0xa90044
+ Rapid Commit
+ Option: Rapid Commit (14)
+ Length: 0
+ Reconfigure Accept
+ Option: Reconfigure Accept (20)
+ Length: 0
+ Option Request
+ Option: Option Request (6)
+ Length: 8
+ Value: 0011001700180019
+ Requested Option code: Vendor-specific Information (17)
+ Requested Option code: DNS recursive name server (23)
+ Requested Option code: Domain Search List (24)
+ Requested Option code: Identity Association for Prefix Delegation (25)
+ Vendor Class
+ Option: Vendor Class (16)
+ Length: 16
+ Value: 0000118b000a65526f75746572312e30
+ Enterprise ID: Cable Television Laboratories, Inc. (4491)
+ vendor-class-data: eRouter1.0
+ Vendor-specific Information
+ Option: Vendor-specific Information (17)
+ Length: 112
+ Value: 0000118b0002000745524f555445520003000b45434d3a45...
+ Enterprise ID: Cable Television Laboratories, Inc. (4491)
+ Suboption: Device Type = (2)"EROUTER"
+ Suboption: Embedded Components = (3)"ECM:EROUTER"
+ Suboption: Serial Number = (4)"2BR229U40044C"
+ Suboption: Hardware Version = (5)"1.04"
+ Suboption: Software Version = (6)"V1.33.03"
+ Suboption: Boot ROM Version = (7)"2.3.0R2"
+ Suboption: Organization Unique Identifier = (8)"00095B"
+ Suboption: Model Number = (9)"CG3000DCR"
+ Suboption: Vendor Name = (10)"Netgear"
+ Client Identifier
+ Option: Client Identifier (1)
+ Length: 10
+ Value: 0003000120e52ab81515
+ DUID: 0003000120e52ab81515
+ DUID Type: link-layer address (3)
+ Hardware type: Ethernet (1)
+ Link-layer address: 20:e5:2a:b8:15:15
+ Identity Association for Prefix Delegation
+ Option: Identity Association for Prefix Delegation (25)
+ Length: 41
+ Value: 2ab815150000000000000000001a00190000000000000000...
+ IAID: 2ab81515
+ T1: 0
+ T2: 0
+ IA Prefix
+ Option: IA Prefix (26)
+ Length: 25
+ Value: 000000000000000038000000000000000000000000000000...
+ Preferred lifetime: 0
+ Valid lifetime: 0
+ Prefix length: 56
+ Prefix address: :: (::)
+ Identity Association for Non-temporary Address
+ Option: Identity Association for Non-temporary Address (3)
+ Length: 12
+ Value: 2ab815150000000000000000
+ IAID: 2ab81515
+ T1: 0
+ T2: 0
+ Elapsed time
+ Option: Elapsed time (8)
+ Length: 2
+ Value: 0000
+ Elapsed time: 0 ms
+ Vendor-specific Information
+ Option: Vendor-specific Information (17)
+ Length: 22
+ Value: 0000118b0402000620e52ab815140401000401020300
+ Enterprise ID: Cable Television Laboratories, Inc. (4491)
+ Suboption: CM MAC Address Option = (1026)20:e5:2a:b8:15:14
+ Suboption: CMTS Capabilities Option : (1025)
+ Interface-Id
+ Option: Interface-Id (18)
+ Length: 4
+ Value: 00000022
+ Interface-ID:
+ Remote Identifier
+ Option: Remote Identifier (37)
+ Length: 14
+ Value: 0000101300015c228d4110000122
+ Enterprise ID: Arris Interactive LLC (4115)
+ Remote-ID: 00015c228d4110000122
+ DHCP option 53
+ Option: Unknown (53)
+ Length: 10
+ Value: 0003000100015c228d3d
+ DUID: 0003000100015c228d3d
+ DUID Type: link-layer address (3)
+ Hardware type: Ethernet (1)
+ Link-layer address: 00:01:5c:22:8d:3d */
+
+ // string exported from Wireshark
+ string hex_string =
+ "0c0020010558ffa800000000000000000001fe8000000000000022e52afffeb8151500"
+ "0900f101a90044000e000000140000000600080011001700180019001000100000118b"
+ "000a65526f75746572312e30001100700000118b0002000745524f555445520003000b"
+ "45434d3a45524f555445520004000d3242523232395534303034344300050004312e30"
+ "340006000856312e33332e303300070007322e332e3052320008000630303039354200"
+ "090009434733303030444352000a00074e6574676561720001000a0003000120e52ab8"
+ "1515001900292ab815150000000000000000001a001900000000000000003800000000"
+ "0000000000000000000000000003000c2ab81515000000000000000000080002000000"
+ "1100160000118b0402000620e52ab81514040100040102030000120004000000220025"
+ "000e0000101300015c228d41100001220035000a0003000100015c228d3d";
+
+ std::vector<uint8_t> bin;
+
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(hex_string, bin);
+
+ Pkt6Ptr pkt(new Pkt6(&bin[0], bin.size()));
+ captureSetDefaultFields(pkt);
+ return (pkt);
+}
+
+}; // end of isc::test namespace
+}; // end of isc namespace
diff --git a/src/bin/loadzone/.gitignore b/src/bin/loadzone/.gitignore
index 41e280a..286abba 100644
--- a/src/bin/loadzone/.gitignore
+++ b/src/bin/loadzone/.gitignore
@@ -1,5 +1,4 @@
/b10-loadzone
-/b10-loadzone.py
/loadzone.py
/run_loadzone.sh
/b10-loadzone.8
diff --git a/src/bin/loadzone/b10-loadzone.xml b/src/bin/loadzone/b10-loadzone.xml
index aa14053..b47421f 100644
--- a/src/bin/loadzone/b10-loadzone.xml
+++ b/src/bin/loadzone/b10-loadzone.xml
@@ -52,6 +52,12 @@
<arg choice="req">zone name</arg>
<arg choice="req">zone file</arg>
</cmdsynopsis>
+ <cmdsynopsis>
+ <command>b10-loadzone</command>
+ <arg><option>-e</option></arg>
+ <arg><option>other options</option></arg>
+ <arg choice="req">zone name</arg>
+ </cmdsynopsis>
</refsynopsisdiv>
<refsect1>
@@ -61,6 +67,9 @@
in a BIND 10 ready data source format.
Master files are text files that contain DNS Resource Records
in text form.
+ This utility can also be used to create an empty zone on the
+ specified data source so the existence of the zone is recognized
+ in the data source without any content (resource records).
</para>
<note><simpara>Currently only the SQLITE3 data source is supported.
</simpara></note>
@@ -104,6 +113,18 @@
old version will still remain accessible for other applications.
</para>
+ <para>
+ If the <command>-e</command> command line option is specified,
+ <command>b10-loadzone</command> does not take the zone name
+ argument.
+ In this case it creates an empty zone without any content
+ while the data source still recognizes the existence of the
+ zone.
+ If the specified zone already has some content, this mode of
+ operation will remove it (but the existence of the zone in the
+ data source will be still recognized).
+ </para>
+
</refsect1>
<refsect1>
@@ -142,6 +163,18 @@
</varlistentry>
<varlistentry>
+ <term>-e</term>
+ <listitem><para>
+ Create an empty zone, or empty existing zone content
+ instead of loading new one.
+ When this option is specified, the zone file command line
+ argument must not be provided.
+ The <command>-i</command> option has no effect, but it
+ does not cause a failure; it will be simply ignored.
+ </para></listitem>
+ </varlistentry>
+
+ <varlistentry>
<term>-i <replaceable class="parameter">report_interval</replaceable></term>
<listitem><para>
Specifies the interval of status update by the number of RRs
diff --git a/src/bin/loadzone/loadzone.py.in b/src/bin/loadzone/loadzone.py.in
index 736aa31..955223a 100755
--- a/src/bin/loadzone/loadzone.py.in
+++ b/src/bin/loadzone/loadzone.py.in
@@ -23,6 +23,7 @@ from optparse import OptionParser
from isc.dns import *
from isc.datasrc import *
import isc.util.process
+import isc.util.traceback_handler
import isc.log
from isc.log_messages.loadzone_messages import *
from datetime import timedelta
@@ -66,6 +67,8 @@ Example: '{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
parser.add_option("-d", "--debug", dest="debug_level",
type='int', action="store", default=None,
help="enable debug logs with the specified level [0-99]")
+ parser.add_option("-e", "--empty", dest="empty_zone",
+ action="store_true", help="empty zone content (no load)")
parser.add_option("-i", "--report-interval", dest="report_interval",
type='int', action="store",
default=LOAD_INTERVAL_DEFAULT,
@@ -113,6 +116,7 @@ class LoadZoneRunner:
self._datasrc_type = None
self._log_severity = 'INFO'
self._log_debuglevel = 0
+ self._empty_zone = False
self._report_interval = LOAD_INTERVAL_DEFAULT
self._start_time = None
# This one will be used in (rare) cases where we want to allow tests to
@@ -140,7 +144,8 @@ class LoadZoneRunner:
'''
usage_txt = \
- 'usage: %prog [options] -c datasrc_config zonename zonefile'
+ 'usage: %prog [options] -c datasrc_config zonename zonefile\n' + \
+ ' %prog [options] -c datasrc_config -e zonename'
parser = OptionParser(usage=usage_txt)
set_cmd_options(parser)
(options, args) = parser.parse_args(args=self.__command_args)
@@ -174,15 +179,22 @@ class LoadZoneRunner:
'Invalid report interval (must be non negative): %d' %
self._report_interval)
- if len(args) != 2:
- raise BadArgument('Unexpected number of arguments: %d (must be 2)'
- % (len(args)))
+ if options.empty_zone:
+ self._empty_zone = True
+
+ # Check number of non option arguments: must be 1 with -e; 2 otherwise.
+ num_args = 1 if self._empty_zone else 2
+
+ if len(args) != num_args:
+ raise BadArgument('Unexpected number of arguments: %d (must be %d)'
+ % (len(args), num_args))
try:
self._zone_name = Name(args[0])
except Exception as ex: # too broad, but there's no better granurality
raise BadArgument("Invalid zone name '" + args[0] + "': " +
str(ex))
- self._zone_file = args[1]
+ if len(args) > 1:
+ self._zone_file = args[1]
def _get_datasrc_config(self, datasrc_type):
''''Return the default data source configuration of given type.
@@ -254,6 +266,34 @@ class LoadZoneRunner:
else:
logger.info(LOADZONE_ZONE_UPDATING, self._zone_name,
self._zone_class)
+ if self._empty_zone:
+ self.__make_empty_zone(datasrc_client)
+ else:
+ self.__load_from_file(datasrc_client)
+ except Exception as ex:
+ if created:
+ datasrc_client.delete_zone(self._zone_name)
+ logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
+ self._zone_class)
+ raise LoadFailure(str(ex))
+
+ def __make_empty_zone(self, datasrc_client):
+ """Subroutine of _do_load(), create an empty zone or make it empty."""
+ try:
+ updater = datasrc_client.get_updater(self._zone_name, True)
+ updater.commit()
+ logger.info(LOADZONE_EMPTY_DONE, self._zone_name,
+ self._zone_class)
+ except Exception:
+ # once updater is created, it's very unlikely that commit() fails,
+ # but in case it happens, clear updater to release any remaining
+ # lock.
+ updater = None
+ raise
+
+ def __load_from_file(self, datasrc_client):
+ """Subroutine of _do_load(), load a zone file into data source."""
+ try:
loader = ZoneLoader(datasrc_client, self._zone_name,
self._zone_file)
self._start_time = time.time()
@@ -279,14 +319,14 @@ class LoadZoneRunner:
sys.stdout.write('\n')
# record the final count of the loaded RRs for logging
self._loaded_rrs = loader.get_rr_count()
- except Exception as ex:
+
+ total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
+ logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
+ self._zone_class, total_elapsed_txt)
+ except Exception:
# release any remaining lock held in the loader
loader = None
- if created:
- datasrc_client.delete_zone(self._zone_name)
- logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
- self._zone_class)
- raise LoadFailure(str(ex))
+ raise
def _set_signal_handlers(self):
signal.signal(signal.SIGINT, self._interrupt_handler)
@@ -302,9 +342,6 @@ class LoadZoneRunner:
self._set_signal_handlers()
self._parse_args()
self._do_load()
- total_elapsed_txt = "%.2f" % (time.time() - self._start_time)
- logger.info(LOADZONE_DONE, self._loaded_rrs, self._zone_name,
- self._zone_class, total_elapsed_txt)
return 0
except BadArgument as ex:
logger.error(LOADZONE_ARGUMENT_ERROR, ex)
@@ -315,11 +352,14 @@ class LoadZoneRunner:
logger.error(LOADZONE_UNEXPECTED_FAILURE, ex)
return 1
-if '__main__' == __name__:
+def main():
runner = LoadZoneRunner(sys.argv[1:])
ret = runner.run()
sys.exit(ret)
+if '__main__' == __name__:
+ isc.util.traceback_handler.traceback_handler(main)
+
## Local Variables:
## mode: python
## End:
diff --git a/src/bin/loadzone/loadzone_messages.mes b/src/bin/loadzone/loadzone_messages.mes
index 744a1a4..206b02e 100644
--- a/src/bin/loadzone/loadzone_messages.mes
+++ b/src/bin/loadzone/loadzone_messages.mes
@@ -33,6 +33,11 @@ an old version of the zone in the data source, it is now deleted.
It also prints the number of RRs that have been loaded
and the time spent for the loading.
+% LOADZONE_EMPTY_DONE Completed emptying zone %1/%2
+b10-loadzone has successfully emptied content of the specified zone.
+This includes the case where the content didn't previously exist, in which
+case it just still reamins empty.
+
% LOADZONE_LOAD_ERROR Failed to load zone %1/%2: %3
Loading a zone by b10-loadzone fails for some reason in the middle of
the loading. This is most likely due to an error in the specified
diff --git a/src/bin/loadzone/tests/loadzone_test.py b/src/bin/loadzone/tests/loadzone_test.py
index 16499ba..83894bd 100755
--- a/src/bin/loadzone/tests/loadzone_test.py
+++ b/src/bin/loadzone/tests/loadzone_test.py
@@ -82,6 +82,7 @@ class TestLoadZoneRunner(unittest.TestCase):
self.assertEqual(RRClass.IN, self.__runner._zone_class) # default
self.assertEqual('INFO', self.__runner._log_severity) # default
self.assertEqual(0, self.__runner._log_debuglevel)
+ self.assertFalse(self.__runner._empty_zone)
def test_set_loglevel(self):
runner = LoadZoneRunner(['-d', '1'] + self.__args)
@@ -90,13 +91,19 @@ class TestLoadZoneRunner(unittest.TestCase):
self.assertEqual(1, runner._log_debuglevel)
def test_parse_bad_args(self):
- # There must be exactly 2 non-option arguments: zone name and zone file
+ # There must usually be exactly 2 non-option arguments: zone name and
+ # zone file.
self.assertRaises(BadArgument, LoadZoneRunner([])._parse_args)
self.assertRaises(BadArgument, LoadZoneRunner(['example']).
_parse_args)
self.assertRaises(BadArgument, LoadZoneRunner(self.__args + ['0']).
_parse_args)
+ # With -e it must be only zone name
+ self.assertRaises(BadArgument, LoadZoneRunner(
+ ['-e', 'example', 'example.zone'])._parse_args)
+ self.assertRaises(BadArgument, LoadZoneRunner(['-e'])._parse_args)
+
# Bad zone name
args = ['example.org', 'example.zone'] # otherwise valid args
self.assertRaises(BadArgument,
@@ -134,22 +141,24 @@ class TestLoadZoneRunner(unittest.TestCase):
self.assertRaises(BadArgument, self.__runner._get_datasrc_config,
'memory')
- def __common_load_setup(self):
+ def __common_load_setup(self, empty=False):
self.__runner._zone_class = RRClass.IN
self.__runner._zone_name = TEST_ZONE_NAME
self.__runner._zone_file = NEW_ZONE_TXT_FILE
self.__runner._datasrc_type = 'sqlite3'
self.__runner._datasrc_config = DATASRC_CONFIG
self.__runner._report_interval = 1
+ self.__runner._empty_zone = empty
self.__reports = []
self.__runner._report_progress = lambda x, _: self.__reports.append(x)
def __check_zone_soa(self, soa_txt, zone_name=TEST_ZONE_NAME):
"""Check that the given SOA RR exists and matches the expected string
- If soa_txt is None, the zone is expected to be non-existent.
- Otherwise, if soa_txt is False, the zone should exist but SOA is
- expected to be missing.
+ If soa_txt is None, the zone is expected to be non-existent;
+ if it's 'empty', the zone should exist but is expected to be empty;
+ if soa_txt is False, the zone should exist but SOA is expected to be
+ missing.
"""
@@ -160,7 +169,10 @@ class TestLoadZoneRunner(unittest.TestCase):
return
self.assertEqual(client.SUCCESS, result)
result, rrset, _ = finder.find(zone_name, RRType.SOA)
- if soa_txt:
+ if soa_txt == 'empty':
+ self.assertEqual(finder.NXDOMAIN, result)
+ self.assertIsNone(rrset)
+ elif soa_txt:
self.assertEqual(finder.SUCCESS, result)
self.assertEqual(soa_txt, rrset.to_text())
else:
@@ -269,6 +281,19 @@ class TestLoadZoneRunner(unittest.TestCase):
# _do_load() should have once created the zone but then canceled it.
self.__check_zone_soa(None, zone_name=Name('example.com'))
+ def test_create_and_empty(self):
+ self.__common_load_setup(True)
+ self.__runner._zone_name = Name('example.com')
+ self.__check_zone_soa(None, zone_name=Name('example.com'))
+ self.__runner._do_load()
+ self.__check_zone_soa('empty', zone_name=Name('example.com'))
+
+ def test_empty(self):
+ self.__common_load_setup(True)
+ self.__check_zone_soa(ORIG_SOA_TXT)
+ self.__runner._do_load()
+ self.__check_zone_soa('empty')
+
def __common_post_load_setup(self, zone_file):
'''Common setup procedure for post load tests which should fail.'''
# replace the LoadZoneRunner's original _post_load_warning() for
diff --git a/src/bin/memmgr/.gitignore b/src/bin/memmgr/.gitignore
new file mode 100644
index 0000000..b92e3f5
--- /dev/null
+++ b/src/bin/memmgr/.gitignore
@@ -0,0 +1,5 @@
+/b10-memmgr
+/memmgr.py
+/memmgr.spec
+/b10-memmgr.8
+/memmgr.spec.pre
diff --git a/src/bin/memmgr/Makefile.am b/src/bin/memmgr/Makefile.am
new file mode 100644
index 0000000..55c4601
--- /dev/null
+++ b/src/bin/memmgr/Makefile.am
@@ -0,0 +1,62 @@
+SUBDIRS = . tests
+
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+
+pkglibexec_SCRIPTS = b10-memmgr
+
+b10_memmgrdir = $(pkgdatadir)
+b10_memmgr_DATA = memmgr.spec
+
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/memmgr_messages.py
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
+
+CLEANFILES = b10-memmgr memmgr.pyc
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/memmgr_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/memmgr_messages.pyc
+CLEANFILES += memmgr.spec
+
+EXTRA_DIST = memmgr_messages.mes
+
+man_MANS = b10-memmgr.8
+DISTCLEANFILES = $(man_MANS)
+EXTRA_DIST += $(man_MANS) b10-memmgr.xml
+
+if GENERATE_DOCS
+
+b10-memmgr.8: b10-memmgr.xml
+ @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-memmgr.xml
+
+else
+
+$(man_MANS):
+ @echo Man generation disabled. Creating dummy $@. Configure with --enable-generate-docs to enable it.
+ @echo Man generation disabled. Remove this file, configure with --enable-generate-docs, and rebuild BIND 10 > $@
+
+endif
+
+# Define rule to build logging source files from message file
+$(PYTHON_LOGMSGPKG_DIR)/work/memmgr_messages.py : memmgr_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message \
+ -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/memmgr_messages.mes
+
+memmgr.spec: memmgr.spec.pre
+ $(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" memmgr.spec.pre > $@
+
+# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
+b10-memmgr: memmgr.py $(PYTHON_LOGMSGPKG_DIR)/work/memmgr_messages.py
+ $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" memmgr.py >$@
+ chmod a+x $@
+
+CLEANDIRS = __pycache__
+
+# install the default directory for memory-mapped files. Note that the
+# path must be identical to the default value in memmgr.spec. We'll make
+# it readable only for the owner to minimize the risk of accidents.
+install-data-local:
+ $(mkinstalldirs) $(DESTDIR)@localstatedir@/@PACKAGE@/mapped_files
+
+install-data-hook:
+ -chmod 700 $(DESTDIR)@localstatedir@/@PACKAGE@/mapped_files
+
+clean-local:
+ rm -rf $(CLEANDIRS)
diff --git a/src/bin/memmgr/b10-memmgr.xml b/src/bin/memmgr/b10-memmgr.xml
new file mode 100644
index 0000000..ee7fee2
--- /dev/null
+++ b/src/bin/memmgr/b10-memmgr.xml
@@ -0,0 +1,109 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+ [<!ENTITY mdash "—">]>
+<!--
+ - Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or distribute this software for any
+ - purpose with or without fee is hereby granted, provided that the above
+ - copyright notice and this permission notice appear in all copies.
+ -
+ - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ - PERFORMANCE OF THIS SOFTWARE.
+-->
+
+<refentry>
+
+ <refentryinfo>
+ <date>June 11, 2013</date>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>b10-memmgr</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo>BIND10</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>b10-memmgr</refname>
+ <refpurpose>BIND 10 memory manager daemon</refpurpose>
+ </refnamediv>
+
+ <docinfo>
+ <copyright>
+ <year>2013</year>
+ <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+ </copyright>
+ </docinfo>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>b10-memmgr</command>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>DESCRIPTION</title>
+ <para>The <command>b10-memmgr</command> daemon manages shared
+ memory segments storing in-memory DNS zone data, and
+ communicates with other BIND 10 modules about the segment
+ information so the entire system will use these segments
+ in a consistent manner.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>ARGUMENTS</title>
+
+ <para>The <command>b10-memmgr</command> daemon does not take
+ any command line arguments.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>CONFIGURATION AND COMMANDS</title>
+ <para>
+ The configurable settings are:
+ </para>
+ <para>
+ <varname>mapped_file_dir</varname>
+ A path to store files to be mapped to memory. This must be
+ writable to the <command>b10-memmgr</command> daemon.
+ </para>
+
+ <para>
+ The module commands are:
+ </para>
+ <para>
+ <command>shutdown</command> exits <command>b10-memmgr</command>.
+ </para>
+ </refsect1>
+
+
+ <refsect1>
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>,
+ <citetitle>BIND 10 Guide</citetitle>.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>HISTORY</title>
+ <para>
+ The <command>b10-memmgr</command> daemon was first implemented
+ in 2013 for the ISC BIND 10 project.
+ </para>
+ </refsect1>
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->
diff --git a/src/bin/memmgr/memmgr.py.in b/src/bin/memmgr/memmgr.py.in
new file mode 100755
index 0000000..d1b93a8
--- /dev/null
+++ b/src/bin/memmgr/memmgr.py.in
@@ -0,0 +1,261 @@
+#!@PYTHON@
+
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import copy
+import os
+import sys
+import signal
+import socket
+import threading
+
+sys.path.append('@@PYTHONPATH@@')
+import isc.log
+from isc.config import ModuleSpecError, ModuleCCSessionError
+from isc.log_messages.memmgr_messages import *
+from isc.server_common.bind10_server import BIND10Server, BIND10ServerFatal
+from isc.server_common.datasrc_clients_mgr \
+ import DataSrcClientsMgr, ConfigError
+from isc.memmgr.datasrc_info import DataSrcInfo, SegmentInfo
+from isc.memmgr.builder import MemorySegmentBuilder
+import isc.util.process
+import isc.util.traceback_handler
+
+MODULE_NAME = 'memmgr'
+
+isc.log.init('b10-memmgr', buffer=True)
+logger = isc.log.Logger(MODULE_NAME)
+
+isc.util.process.rename()
+
+class ConfigError(Exception):
+ """An exception class raised for configuration errors of Memmgr."""
+ pass
+
+class Memmgr(BIND10Server):
+ def __init__(self):
+ BIND10Server.__init__(self)
+ # Running configurable parameters: on initial configuration this will
+ # be a dict: str=>config_value.
+ # This is defined as "protected" so tests can inspect it; others
+ # shouldn't use it directly.
+ self._config_params = None
+
+ # The manager to keep track of data source configuration. Allow
+ # tests to inspect/tweak it.
+ self._datasrc_clients_mgr = DataSrcClientsMgr(use_cache=True)
+
+ # List of DataSrcInfo. Used as a queue to maintain info for all
+ # active configuration generations. Allow tests to inspec it.
+ self._datasrc_info_list = []
+
+ self._builder_setup = False
+ self._builder_command_queue = []
+ self._builder_response_queue = []
+
+ def _config_handler(self, new_config):
+ """Configuration handler, called via BIND10Server.
+
+ This method must be exception free. We assume minimum validity
+ about the parameter, though: it should be a valid dict, and conform
+ to the type specification of the spec file.
+
+ """
+ logger.debug(logger.DBGLVL_TRACE_BASIC, MEMMGR_CONFIG_UPDATE)
+
+ # Default answer:
+ answer = isc.config.create_answer(0)
+
+ # If this is the first time, initialize the local attributes with the
+ # latest full config data, which consist of the defaults with
+ # possibly overridden by user config. Otherwise, just apply the latest
+ # diff.
+ if self._config_params is None:
+ new_config = self.mod_ccsession.get_full_config()
+ try:
+ self.__update_config(new_config)
+ except Exception as ex:
+ logger.error(MEMMGR_CONFIG_FAIL, ex)
+ answer = isc.config.create_answer(
+ 1, 'Memmgr failed to apply configuration updates: ' + str(ex))
+
+ return answer
+
+ def __update_config(self, new_config):
+ """Apply config changes to local attributes.
+
+ This is a subroutine of _config_handler. It's supposed to provide
+ strong exception guarantee: either all changes successfully apply
+ or, if any error is found, none applies. In the latter case the
+ entire original configuration should be kept.
+
+ Errors are to be reported as an exception.
+
+ """
+ # If this is the first time, build everything from the scratch.
+ # Otherwise, make a full local copy and update it.
+ if self._config_params is None:
+ new_config_params = {}
+ else:
+ new_config_params = copy.deepcopy(self._config_params)
+
+ new_mapped_file_dir = new_config.get('mapped_file_dir')
+ if new_mapped_file_dir is not None:
+ if not os.path.isdir(new_mapped_file_dir):
+ raise ConfigError('mapped_file_dir is not a directory: ' +
+ new_mapped_file_dir)
+ if not os.access(new_mapped_file_dir, os.W_OK):
+ raise ConfigError('mapped_file_dir is not writable: ' +
+ new_mapped_file_dir)
+ new_config_params['mapped_file_dir'] = new_mapped_file_dir
+
+ # All copy, switch to the new configuration.
+ self._config_params = new_config_params
+
+ def _cmd_to_builder(self, cmd):
+ """
+ Send a command to the builder, with proper synchronization.
+ """
+ assert isinstance(cmd, tuple)
+ with self._builder_cv:
+ self._builder_command_queue.append(cmd)
+ self._builder_cv.notify_all()
+
+ def _notify_from_builder(self):
+ """
+ Read the notifications from the builder thread.
+ """
+ self._master_sock.recv(1) # Clear the wake-up data
+ notifications = None
+ with self._builder_lock:
+ # Copy the notifications out and clear them from the
+ # original list. We may not assign [] to
+ # self._builder_response_queue to clear it, because there's
+ # another reference to it from the other thread and it would
+ # not keep the original list.
+ notifications = self._builder_response_queue[:]
+ del self._builder_response_queue[:]
+ for notification in notifications:
+ notif_name = notification[0]
+ if notif_name == 'load-completed':
+ (_, dsrc_info, rrclass, dsrc_name) = notification
+ sgmt_info = dsrc_info.segment_info_map[(rrclass, dsrc_name)]
+ cmd = sgmt_info.complete_update()
+ # It may return another load command on the same data source.
+ # If it is so, we execute it too, before we start
+ # synchronizing with the readers.
+ if cmd is not None:
+ self._cmd_to_builder(cmd)
+ else:
+ pass
+ # TODO: Send to the readers, #2858
+ else:
+ raise ValueError('Unknown notification name: ' + notif_name)
+
+ def __create_builder_thread(self):
+ # We get responses from the builder thread on this socket pair.
+ (self._master_sock, self._builder_sock) = \
+ socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.watch_fileno(self._master_sock, rcallback=self._notify_from_builder)
+
+ # See the documentation for MemorySegmentBuilder on how the
+ # following are used.
+ self._builder_lock = threading.Lock()
+ self._builder_cv = threading.Condition(lock=self._builder_lock)
+
+ self._builder = MemorySegmentBuilder(self._builder_sock,
+ self._builder_cv,
+ self._builder_command_queue,
+ self._builder_response_queue)
+ self._builder_thread = threading.Thread(target=self._builder.run)
+ self._builder_thread.start()
+
+ self._builder_setup = True
+
+ def __shutdown_builder_thread(self):
+ # Some unittests do not create the builder thread, so we check
+ # that.
+ if not self._builder_setup:
+ return
+
+ self._builder_setup = False
+
+ # This makes the MemorySegmentBuilder exit its main loop. It
+ # should make the builder thread joinable.
+ self._cmd_to_builder(('shutdown',))
+
+ self._builder_thread.join()
+
+ self._master_sock.close()
+ self._builder_sock.close()
+
+ def _setup_module(self):
+ """Module specific initialization for BIND10Server."""
+ try:
+ # memmgr isn't usable if data source is not configured, and
+ # as long as cfgmgr is ready there's no timing issue. So we
+ # immediately shut it down if it's missing. See ddns.py.in
+ # about exceptions to catch.
+ self.mod_ccsession.add_remote_config_by_name(
+ 'data_sources', self._datasrc_config_handler)
+ except (ModuleSpecError, ModuleCCSessionError) as ex:
+ logger.error(MEMMGR_NO_DATASRC_CONF, ex)
+ raise BIND10ServerFatal('failed to setup memmgr module')
+
+ self.__create_builder_thread()
+
+ def _shutdown_module(self):
+ """Module specific finalization."""
+ self.__shutdown_builder_thread()
+
+ def _datasrc_config_handler(self, new_config, config_data):
+ """Callback of data_sources configuration update.
+
+ This method must be exception free, so we catch all expected
+ exceptions internally; unexpected ones should mean a programming
+ error and will terminate the program.
+
+ """
+ try:
+ self._datasrc_clients_mgr.reconfigure(new_config, config_data)
+ genid, clients_map = self._datasrc_clients_mgr.get_clients_map()
+ datasrc_info = DataSrcInfo(genid, clients_map, self._config_params)
+ self._datasrc_info_list.append(datasrc_info)
+ self._init_segments(datasrc_info)
+
+ # Full datasrc reconfig will be rare, so would be worth logging
+ # at the info level.
+ logger.info(MEMMGR_DATASRC_RECONFIGURED, genid)
+
+ except isc.server_common.datasrc_clients_mgr.ConfigError as ex:
+ logger.error(MEMMGR_DATASRC_CONFIG_ERROR, ex)
+
+ def _init_segments(self, datasrc_info):
+ for key, sgmt_info in datasrc_info.segment_info_map.items():
+ rrclass, dsrc_name = key
+ cmd = ('load', None, datasrc_info, rrclass, dsrc_name)
+ sgmt_info.add_event(cmd)
+ send_cmd = sgmt_info.start_update()
+ assert cmd == send_cmd and sgmt_info.get_state() == \
+ SegmentInfo.UPDATING
+ self._cmd_to_builder(cmd)
+
+def main():
+ mgr = Memmgr()
+ sys.exit(mgr.run(MODULE_NAME))
+
+if '__main__' == __name__:
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/memmgr/memmgr.spec.pre.in b/src/bin/memmgr/memmgr.spec.pre.in
new file mode 100644
index 0000000..6729690
--- /dev/null
+++ b/src/bin/memmgr/memmgr.spec.pre.in
@@ -0,0 +1,25 @@
+{
+ "module_spec": {
+ "module_name": "Memmgr",
+ "config_data": [
+ { "item_name": "mapped_file_dir",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "@@LOCALSTATEDIR@@/@PACKAGE@/mapped_files"
+ }
+ ],
+ "commands": [
+ {
+ "command_name": "shutdown",
+ "command_description": "Shut down Memmgr",
+ "command_args": [
+ {
+ "item_name": "pid",
+ "item_type": "integer",
+ "item_optional": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/src/bin/memmgr/memmgr_messages.mes b/src/bin/memmgr/memmgr_messages.mes
new file mode 100644
index 0000000..6ca5c0f
--- /dev/null
+++ b/src/bin/memmgr/memmgr_messages.mes
@@ -0,0 +1,51 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# When you add a message to this file, it is a good idea to run
+# <topsrcdir>/tools/reorder_message_file.py to make sure the
+# messages are in the correct order.
+
+% MEMMGR_CONFIG_FAIL failed to apply configuration updates: %1
+The memmgr daemon tried to apply configuration updates but found an error.
+The cause of the error is included in the message. None of the received
+updates applied, and the daemon keeps running with the previous configuration.
+
+% MEMMGR_CONFIG_UPDATE received new configuration
+A debug message. The memmgr daemon receives configuratiopn updates
+and is now applying them to its running configurations.
+
+% MEMMGR_DATASRC_CONFIG_ERROR failed to update data source configuration: %1
+Configuration for the global data sources is updated, but the update
+cannot be applied to memmgr. The memmgr module will still keep running
+with the previous configuration, but the cause of the failure and
+other log messages must be carefully examined because if only memmgr
+rejects the new configuration then the entire BIND 10 system will have
+inconsistent state among different modules. If other modules accept
+the update but memmgr produces this error, it's quite likely that the
+system isn't working as expected, and is probably better to be shut down
+to figure out and fix the cause.
+
+% MEMMGR_DATASRC_RECONFIGURED data source configuration has been updated, generation ID %1
+The memmgr daemon received a new version of data source configuration,
+and has successfully applied it to the local state. Loading of new zone
+data into memory will possibly take place.
+
+% MEMMGR_NO_DATASRC_CONF failed to add data source configuration: %1
+The memmgr daemon tried to incorporate data source configuration on
+its startup but failed to do so. Due to internal implementation
+details this shouldn't happen as long as the BIND 10 system has been
+installed correctly. So, if this error message is logged, you should
+probably reinstall the entire system, preferably from the scratch, and
+see if it still happens. The memmgr daemon cannot do any meaningful
+work without data sources, so it immediately terminates itself.
diff --git a/src/bin/memmgr/tests/Makefile.am b/src/bin/memmgr/tests/Makefile.am
new file mode 100644
index 0000000..347ff87
--- /dev/null
+++ b/src/bin/memmgr/tests/Makefile.am
@@ -0,0 +1,30 @@
+PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
+PYTESTS = memmgr_test.py
+EXTRA_DIST = $(PYTESTS)
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+LIBRARY_PATH_PLACEHOLDER =
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$(abs_top_builddir)/src/lib/acl/.libs:$$$(ENV_LIBRARY_PATH)
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+# We set B10_FROM_BUILD below, so that the test can refer to the in-source
+# spec file.
+check-local:
+if ENABLE_PYTHON_COVERAGE
+ touch $(abs_top_srcdir)/.coverage
+ rm -f .coverage
+ ${LN_S} $(abs_top_srcdir)/.coverage .coverage
+endif
+ for pytest in $(PYTESTS) ; do \
+ echo Running test: $$pytest ; \
+ B10_FROM_SOURCE=$(abs_top_srcdir) \
+ B10_FROM_BUILD=$(abs_top_builddir) \
+ $(LIBRARY_PATH_PLACEHOLDER) \
+ PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/memmgr:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/util/io/.libs \
+ TESTDATASRCDIR=$(abs_srcdir)/testdata/ \
+ TESTDATA_PATH=$(abs_top_srcdir)/src/lib/testutils/testdata \
+ $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+ done
diff --git a/src/bin/memmgr/tests/memmgr_test.py b/src/bin/memmgr/tests/memmgr_test.py
new file mode 100755
index 0000000..32d5c15
--- /dev/null
+++ b/src/bin/memmgr/tests/memmgr_test.py
@@ -0,0 +1,341 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import os
+import re
+import threading
+
+import isc.log
+from isc.dns import RRClass
+import isc.config
+from isc.config import parse_answer
+import memmgr
+from isc.memmgr.datasrc_info import SegmentInfo
+from isc.testutils.ccsession_mock import MockModuleCCSession
+
+class MyCCSession(MockModuleCCSession, isc.config.ConfigData):
+ def __init__(self, specfile, config_handler, command_handler):
+ super().__init__()
+ specfile = os.environ['B10_FROM_BUILD'] + '/src/bin/memmgr/memmgr.spec'
+ module_spec = isc.config.module_spec_from_file(specfile)
+ isc.config.ConfigData.__init__(self, module_spec)
+ self.add_remote_params = [] # for inspection
+ self.add_remote_exception = None # to raise exception from the method
+
+ def start(self):
+ pass
+
+ def add_remote_config_by_name(self, mod_name, handler):
+ if self.add_remote_exception is not None:
+ raise self.add_remote_exception
+ self.add_remote_params.append((mod_name, handler))
+
+class MockMemmgr(memmgr.Memmgr):
+ def _setup_ccsession(self):
+ orig_cls = isc.config.ModuleCCSession
+ isc.config.ModuleCCSession = MyCCSession
+ try:
+ super()._setup_ccsession()
+ finally:
+ isc.config.ModuleCCSession = orig_cls
+
+# Defined for easier tests with DataSrcClientsMgr.reconfigure(), which
+# only needs get_value() method
+class MockConfigData:
+ def __init__(self, data):
+ self.__data = data
+
+ def get_value(self, identifier):
+ return self.__data[identifier], False
+
+class TestMemmgr(unittest.TestCase):
+ def setUp(self):
+ # Some tests use this directory. Make sure it doesn't pre-exist.
+ self.__test_mapped_file_dir = \
+ os.environ['B10_FROM_BUILD'] + \
+ '/src/bin/memmgr/tests/test_mapped_files'
+ if os.path.isdir(self.__test_mapped_file_dir):
+ os.rmdir(self.__test_mapped_file_dir)
+
+ self.__mgr = MockMemmgr()
+ # Fake some 'os' module functions for easier tests
+ self.__orig_os_access = os.access
+ self.__orig_isdir = os.path.isdir
+
+ def tearDown(self):
+ # Not all unittests cause this method to be called, so we call
+ # it explicitly as it may be necessary in some cases where the
+ # builder thread has been created.
+ self.__mgr._shutdown_module()
+
+ # Assert that all commands sent to the builder thread were
+ # handled.
+ self.assertEqual(len(self.__mgr._builder_command_queue), 0)
+
+ # Restore faked values
+ os.access = self.__orig_os_access
+ os.path.isdir = self.__orig_isdir
+
+ # If at test created a mapped-files directory, delete it.
+ if os.path.isdir(self.__test_mapped_file_dir):
+ os.rmdir(self.__test_mapped_file_dir)
+
+ def test_init(self):
+ """Check some initial conditions"""
+ self.assertIsNone(self.__mgr._config_params)
+ self.assertEqual([], self.__mgr._datasrc_info_list)
+
+ # Try to configure a data source clients with the manager. This
+ # should confirm the manager object is instantiated enabling in-memory
+ # cache.
+ cfg_data = MockConfigData(
+ {"classes": {"IN": [{"type": "MasterFiles",
+ "cache-enable": True, "params": {}}]}})
+ self.__mgr._datasrc_clients_mgr.reconfigure({}, cfg_data)
+ clist = \
+ self.__mgr._datasrc_clients_mgr.get_client_list(RRClass.IN)
+ self.assertEqual(1, len(clist.get_status()))
+
+ def test_configure(self):
+ self.__mgr._setup_ccsession()
+
+ # Pretend specified directories exist and writable
+ os.path.isdir = lambda x: True
+ os.access = lambda x, y: True
+
+ # At the initial configuration, if mapped_file_dir isn't specified,
+ # the default value will be set.
+ self.assertEqual((0, None),
+ parse_answer(self.__mgr._config_handler({})))
+ self.assertEqual('mapped_files',
+ self.__mgr._config_params['mapped_file_dir'].
+ split('/')[-1])
+
+ # Update the configuration.
+ user_cfg = {'mapped_file_dir': '/some/path/dir'}
+ self.assertEqual((0, None),
+ parse_answer(self.__mgr._config_handler(user_cfg)))
+ self.assertEqual('/some/path/dir',
+ self.__mgr._config_params['mapped_file_dir'])
+
+ # Bad update: diretory doesn't exist (we assume it really doesn't
+ # exist in the tested environment). Update won't be made.
+ os.path.isdir = self.__orig_isdir # use real library
+ user_cfg = {'mapped_file_dir': '/another/path/dir'}
+ answer = parse_answer(self.__mgr._config_handler(user_cfg))
+ self.assertEqual(1, answer[0])
+ self.assertIsNotNone(re.search('not a directory', answer[1]))
+
+ @unittest.skipIf(os.getuid() == 0,
+ 'test cannot be run as root user')
+ def test_configure_bad_permissions(self):
+ self.__mgr._setup_ccsession()
+
+ # Pretend specified directories exist and writable
+ os.path.isdir = lambda x: True
+ os.access = lambda x, y: True
+
+ # Initial configuration.
+ self.assertEqual((0, None),
+ parse_answer(self.__mgr._config_handler({})))
+
+ os.path.isdir = self.__orig_isdir
+ os.access = self.__orig_os_access
+
+ # Bad update: directory exists but is not writable.
+ os.mkdir(self.__test_mapped_file_dir, 0o500) # drop writable bit
+ user_cfg = {'mapped_file_dir': self.__test_mapped_file_dir}
+ answer = parse_answer(self.__mgr._config_handler(user_cfg))
+ self.assertEqual(1, answer[0])
+ self.assertIsNotNone(re.search('not writable', answer[1]))
+
+ def test_setup_module(self):
+ # _setup_module should add data_sources remote module with
+ # expected parameters.
+ self.__mgr._setup_ccsession()
+ self.assertEqual([], self.__mgr.mod_ccsession.add_remote_params)
+ self.__mgr._setup_module()
+ self.assertEqual([('data_sources',
+ self.__mgr._datasrc_config_handler)],
+ self.__mgr.mod_ccsession.add_remote_params)
+
+ # If data source isn't configured it's considered fatal (checking the
+ # same scenario with two possible exception types)
+ self.__mgr.mod_ccsession.add_remote_exception = \
+ isc.config.ModuleCCSessionError('faked exception')
+ self.assertRaises(isc.server_common.bind10_server.BIND10ServerFatal,
+ self.__mgr._setup_module)
+
+ self.__mgr.mod_ccsession.add_remote_exception = \
+ isc.config.ModuleSpecError('faked exception')
+ self.assertRaises(isc.server_common.bind10_server.BIND10ServerFatal,
+ self.__mgr._setup_module)
+
+ def test_datasrc_config_handler(self):
+ self.__mgr._config_params = {'mapped_file_dir': '/some/path'}
+
+ # A simple (boring) case with real class implementations. This
+ # confirms the methods are called as expected.
+ cfg_data = MockConfigData(
+ {"classes": {"IN": [{"type": "MasterFiles",
+ "cache-enable": True, "params": {}}]}})
+ self.__init_called = None
+ def mock_init_segments(param):
+ self.__init_called = param
+ self.__mgr._init_segments = mock_init_segments
+ self.__mgr._datasrc_config_handler({}, cfg_data)
+ self.assertEqual(1, len(self.__mgr._datasrc_info_list))
+ self.assertEqual(1, self.__mgr._datasrc_info_list[0].gen_id)
+ self.assertEqual(self.__init_called, self.__mgr._datasrc_info_list[0])
+
+ # Below we're using a mock DataSrcClientMgr for easier tests
+ class MockDataSrcClientMgr:
+ def __init__(self, status_list, raise_on_reconfig=False):
+ self.__status_list = status_list
+ self.__raise_on_reconfig = raise_on_reconfig
+
+ def reconfigure(self, new_config, config_data):
+ if self.__raise_on_reconfig:
+ raise isc.server_common.datasrc_clients_mgr.ConfigError(
+ 'test error')
+ # otherwise do nothing
+
+ def get_clients_map(self):
+ return 42, {RRClass.IN: self}
+
+ def get_status(self): # mocking get_clients_map()[1].get_status()
+ return self.__status_list
+
+ # This confirms memmgr's config is passed and handled correctly.
+ # From memmgr's point of view it should be enough we have an object
+ # in segment_info_map. Note also that the new DataSrcInfo is appended
+ # to the list
+ self.__mgr._datasrc_clients_mgr = \
+ MockDataSrcClientMgr([('sqlite3', 'mapped', None)])
+ self.__mgr._datasrc_config_handler(None, None) # params don't matter
+ self.assertEqual(2, len(self.__mgr._datasrc_info_list))
+ self.assertEqual(self.__init_called, self.__mgr._datasrc_info_list[1])
+ self.assertIsNotNone(
+ self.__mgr._datasrc_info_list[1].segment_info_map[
+ (RRClass.IN, 'sqlite3')])
+
+ # Emulate the case reconfigure() fails. Exception isn't propagated,
+ # but the list doesn't change.
+ self.__mgr._datasrc_clients_mgr = MockDataSrcClientMgr(None, True)
+ self.__mgr._datasrc_config_handler(None, None)
+ self.assertEqual(2, len(self.__mgr._datasrc_info_list))
+
+ def test_init_segments(self):
+ """
+ Test the initialization of segments â just load everything found in there.
+ """
+ # Fake a lot of things. These are objects hard to set up, so this is
+ # easier.
+ class SgmtInfo:
+ def __init__(self):
+ self.events = []
+ self.__state = None
+
+ def add_event(self, cmd):
+ self.events.append(cmd)
+ self.__state = SegmentInfo.UPDATING
+
+ def start_update(self):
+ return self.events[0]
+
+ def get_state(self):
+ return self.__state
+
+ sgmt_info = SgmtInfo()
+ class DataSrcInfo:
+ def __init__(self):
+ self.segment_info_map = \
+ {(isc.dns.RRClass.IN, "name"): sgmt_info}
+ dsrc_info = DataSrcInfo()
+
+ # Pretend to have the builder thread
+ self.__mgr._builder_cv = threading.Condition()
+
+ # Run the initialization
+ self.__mgr._init_segments(dsrc_info)
+
+ # The event was pushed into the segment info
+ command = ('load', None, dsrc_info, isc.dns.RRClass.IN, 'name')
+ self.assertEqual([command], sgmt_info.events)
+ self.assertEqual([command], self.__mgr._builder_command_queue)
+ del self.__mgr._builder_command_queue[:]
+
+ def test_notify_from_builder(self):
+ """
+ Check the notify from builder thing eats the notifications and
+ handles them.
+ """
+ # Some mocks
+ class SgmtInfo:
+ def complete_update():
+ return 'command'
+ sgmt_info = SgmtInfo
+ class DataSrcInfo:
+ def __init__(self):
+ self.segment_info_map = \
+ {(isc.dns.RRClass.IN, "name"): sgmt_info}
+ dsrc_info = DataSrcInfo()
+ class Sock:
+ def recv(self, size):
+ pass
+ self.__mgr._master_sock = Sock()
+ commands = []
+ def mock_cmd_to_builder(cmd):
+ commands.append(cmd)
+ self.__mgr._cmd_to_builder = mock_cmd_to_builder
+
+ self.__mgr._builder_lock = threading.Lock()
+ # Extract the reference for the queue. We get a copy of the reference
+ # to check it is cleared, not a new empty one installed
+ notif_ref = self.__mgr._builder_response_queue
+ notif_ref.append(('load-completed', dsrc_info, isc.dns.RRClass.IN,
+ 'name'))
+ # Wake up the main thread and let it process the notifications
+ self.__mgr._notify_from_builder()
+ # All notifications are now eaten
+ self.assertEqual([], notif_ref)
+ self.assertEqual(['command'], commands)
+ del commands[:]
+ # The new command is sent
+ # Once again the same, but with the last command - nothing new pushed
+ sgmt_info.complete_update = lambda: None
+ notif_ref.append(('load-completed', dsrc_info, isc.dns.RRClass.IN,
+ 'name'))
+ self.__mgr._notify_from_builder()
+ self.assertEqual([], notif_ref)
+ self.assertEqual([], commands)
+ # This is invalid (unhandled) notification name
+ notif_ref.append(('unhandled',))
+ self.assertRaises(ValueError, self.__mgr._notify_from_builder)
+ self.assertEqual([], notif_ref)
+
+ def test_send_to_builder(self):
+ """
+ Send command to the builder test.
+ """
+ self.__mgr._builder_cv = threading.Condition()
+ self.__mgr._cmd_to_builder(('test',))
+ self.assertEqual([('test',)], self.__mgr._builder_command_queue)
+ del self.__mgr._builder_command_queue[:]
+
+if __name__== "__main__":
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/bin/msgq/msgq.py.in b/src/bin/msgq/msgq.py.in
index efa3cbd..c68567c 100755
--- a/src/bin/msgq/msgq.py.in
+++ b/src/bin/msgq/msgq.py.in
@@ -33,6 +33,7 @@ import threading
import isc.config.ccsession
from optparse import OptionParser, OptionValueError
import isc.util.process
+import isc.util.traceback_handler
from isc.cc.proto_defs import *
import isc.log
from isc.log_messages.msgq_messages import *
@@ -61,16 +62,20 @@ VERSION = "b10-msgq 20110127 (BIND 10 @PACKAGE_VERSION@)"
# If B10_FROM_BUILD is set in the environment, we use data files
# from a directory relative to that, otherwise we use the ones
# installed on the system
-if "B10_FROM_BUILD" in os.environ:
- SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/msgq"
+if "B10_FROM_SOURCE" in os.environ:
+ SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/msgq"
else:
PREFIX = "@prefix@"
DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+ SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}",
+ DATAROOTDIR). \
+ replace("${prefix}", PREFIX)
SPECFILE_LOCATION = SPECFILE_PATH + "/msgq.spec"
class MsgQReceiveError(Exception): pass
+class MsgQRunningError(Exception): pass
+
class MsgQCloseOnReceive(Exception):
"""Exception raised when reading data from a socket results in 'shutdown'.
@@ -122,12 +127,17 @@ class SubscriptionManager:
if target in self.subscriptions:
if socket in self.subscriptions[target]:
self.subscriptions[target].remove(socket)
+ return True
+ return False
def unsubscribe_all(self, socket):
"""Remove the socket from all subscriptions."""
- for socklist in self.subscriptions.values():
+ removed_from = []
+ for subs, socklist in self.subscriptions.items():
if socket in socklist:
socklist.remove(socket)
+ removed_from.append(subs)
+ return removed_from
def find_sub(self, group, instance):
"""Return an array of sockets which want this specific group,
@@ -175,8 +185,6 @@ class MsgQ:
self.socket_file = socket_file
self.verbose = verbose
- self.poller = None
- self.kqueue = None
self.runnable = False
self.listen_socket = False
self.sockets = {}
@@ -184,6 +192,7 @@ class MsgQ:
self.hostname = socket.gethostname()
self.subs = SubscriptionManager(self.cfgmgr_ready)
self.lnames = {}
+ self.fd_to_lname = {}
self.sendbuffs = {}
self.running = False
self.__cfgmgr_ready = None
@@ -195,6 +204,34 @@ class MsgQ:
# not for performance, so we use wide lock scopes to be on the safe
# side.
self.__lock = threading.Lock()
+ self._session = None
+ self.__poller_sock = None
+
+ def members_notify(self, event, params):
+ """
+ Thin wrapper around ccs's notify. Send a notification about change
+ of some list that can be requested by the members command.
+
+ The event is one of:
+ - connected (client connected to MsgQ)
+ - disconected (client disconnected from MsgQ)
+ - subscribed (client subscribed to a group)
+ - unsubscribed (client unsubscribed from a group)
+
+ The params is dict containing:
+ - client: The lname of the client in question.
+ - group (for 'subscribed' and 'unsubscribed' events):
+ The group the client subscribed or unsubscribed from.
+
+ The notification occurs after the event, so client a subscribing for
+ notifications will get a notification about its own subscription, but
+ will not get a notification when it unsubscribes.
+ """
+ # Due to the interaction between threads (and fear it might influence
+ # sending stuff), we test this method in msgq_run_test, instead of
+ # mocking the ccs.
+ if self._session: # Don't send before we have started up
+ self._session.notify('cc_members', event, params)
def cfgmgr_ready(self, ready=True):
"""Notify that the config manager is either subscribed, or
@@ -229,46 +266,30 @@ class MsgQ:
self.__cfgmgr_ready_cond.wait()
return self.__cfgmgr_ready
- def setup_poller(self):
- """Set up the poll thing. Internal function."""
- try:
- self.kqueue = select.kqueue()
- except AttributeError:
- self.poller = select.poll()
-
- def add_kqueue_socket(self, socket, write_filter=False):
- """Add a kqueue filter for a socket. By default the read
- filter is used; if write_filter is set to True, the write
- filter is used. We use a boolean value instead of a specific
- filter constant, because kqueue filter values do not seem to
- be defined on some systems. The use of boolean makes the
- interface restrictive because there are other filters, but this
- method is mostly only for our internal use, so it should be
- acceptable at least for now."""
- filter_type = select.KQ_FILTER_WRITE if write_filter else \
- select.KQ_FILTER_READ
- event = select.kevent(socket.fileno(), filter_type,
- select.KQ_EV_ADD | select.KQ_EV_ENABLE)
- self.kqueue.control([event], 0)
-
- def delete_kqueue_socket(self, socket, write_filter=False):
- """Delete a kqueue filter for socket. See add_kqueue_socket()
- for the semantics and notes about write_filter."""
- filter_type = select.KQ_FILTER_WRITE if write_filter else \
- select.KQ_FILTER_READ
- event = select.kevent(socket.fileno(), filter_type,
- select.KQ_EV_DELETE)
- self.kqueue.control([event], 0)
-
def setup_listener(self):
"""Set up the listener socket. Internal function."""
logger.debug(TRACE_BASIC, MSGQ_LISTENER_SETUP, self.socket_file)
- self.listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-
if os.path.exists(self.socket_file):
+ # Rather than just blindly removing the socket file, attempt to
+ # connect to the existing socket to see if there is an existing
+ # msgq running. Only if that fails do we remove the file and
+ # attempt to create a new socket.
+ existing_msgq = None
+ try:
+ existing_msgq = isc.cc.Session(self.socket_file)
+ except isc.cc.session.SessionError:
+ existing_msgq = None
+
+ if existing_msgq:
+ existing_msgq.close()
+ logger.fatal(MSGQ_ALREADY_RUNNING)
+ raise MsgQRunningError("b10-msgq already running")
+
os.remove(self.socket_file)
+
try:
+ self.listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.listen_socket.bind(self.socket_file)
self.listen_socket.listen(1024)
except Exception as e:
@@ -280,11 +301,6 @@ class MsgQ:
logger.fatal(MSGQ_LISTENER_FAILED, self.socket_file, e)
raise e
- if self.poller:
- self.poller.register(self.listen_socket, select.POLLIN)
- else:
- self.add_kqueue_socket(self.listen_socket)
-
def setup_signalsock(self):
"""Create a socket pair used to signal when we want to finish.
Using a socket is easy and thread/signal safe way to signal
@@ -294,18 +310,12 @@ class MsgQ:
# closed, we should shut down.
(self.__poller_sock, self.__control_sock) = socket.socketpair()
- if self.poller:
- self.poller.register(self.__poller_sock, select.POLLIN)
- else:
- self.add_kqueue_socket(self.__poller_sock)
-
def setup(self):
"""Configure listener socket, polling, etc.
Raises a socket.error if the socket_file cannot be
created.
"""
- self.setup_poller()
self.setup_signalsock()
self.setup_listener()
@@ -323,36 +333,39 @@ class MsgQ:
def register_socket(self, newsocket):
"""
- Internal function to insert a socket. Used by process_accept and some tests.
+ Internal function to insert a socket. Used by process_accept and some
+ tests.
"""
self.sockets[newsocket.fileno()] = newsocket
lname = self.newlname()
self.lnames[lname] = newsocket
+ self.fd_to_lname[newsocket.fileno()] = lname
logger.debug(TRACE_BASIC, MSGQ_SOCKET_REGISTERED, newsocket.fileno(),
lname)
- if self.poller:
- self.poller.register(newsocket, select.POLLIN)
- else:
- self.add_kqueue_socket(newsocket)
+ self.members_notify('connected', {'client': lname})
def kill_socket(self, fd, sock):
"""Fully close down the socket."""
- # Unregister events on the socket. Note that we don't have to do
- # this for kqueue because the registered events are automatically
- # deleted when the corresponding socket is closed.
- if self.poller:
- self.poller.unregister(sock)
- self.subs.unsubscribe_all(sock)
- lname = [ k for k, v in self.lnames.items() if v == sock ][0]
+ unsubscribed_from = self.subs.unsubscribe_all(sock)
+ lname = self.fd_to_lname[fd]
+ del self.fd_to_lname[fd]
del self.lnames[lname]
sock.close()
del self.sockets[fd]
if fd in self.sendbuffs:
del self.sendbuffs[fd]
logger.debug(TRACE_BASIC, MSGQ_SOCK_CLOSE, fd)
+ # Filter out just the groups.
+ unsubscribed_from_groups = set(map(lambda x: x[0], unsubscribed_from))
+ for group in unsubscribed_from_groups:
+ self.members_notify('unsubscribed', {
+ 'client': lname,
+ 'group': group
+ })
+ self.members_notify('disconnected', {'client': lname})
def __getbytes(self, fd, sock, length, continued):
"""Get exactly the requested bytes, or raise an exception if
@@ -529,22 +542,19 @@ class MsgQ:
# Append it to buffer (but check the data go away)
if fileno in self.sendbuffs:
(last_sent, buff) = self.sendbuffs[fileno]
- if now - last_sent > 0.1:
+ tdelta = now - last_sent
+ if tdelta > 0.1:
+ logger.error(MSGQ_SOCKET_TIMEOUT_ERROR, fileno, tdelta)
self.kill_socket(fileno, sock)
return False
buff += msg
else:
buff = msg[amount_sent:]
last_sent = now
- if self.poller:
- self.poller.register(fileno, select.POLLIN |
- select.POLLOUT)
- else:
- self.add_kqueue_socket(sock, True)
self.sendbuffs[fileno] = (last_sent, buff)
return True
- def __process_write(self, fileno):
+ def _process_write(self, fileno):
# Try to send some data from the buffer
(_, msg) = self.sendbuffs[fileno]
sock = self.sockets[fileno]
@@ -554,10 +564,6 @@ class MsgQ:
msg = msg[amount_sent:]
if len(msg) == 0:
# If there's no more, stop requesting for write availability
- if self.poller:
- self.poller.register(fileno, select.POLLIN)
- else:
- self.delete_kqueue_socket(sock, True)
del self.sendbuffs[fileno]
else:
self.sendbuffs[fileno] = (time.clock(), msg)
@@ -567,7 +573,8 @@ class MsgQ:
This is done by using an increasing counter and the current
time."""
self.connection_counter += 1
- return "%x_%x@%s" % (time.time(), self.connection_counter, self.hostname)
+ return "%x_%x@%s" % (time.time(), self.connection_counter,
+ self.hostname)
def process_command_ping(self, sock, routing, data):
self.sendmsg(sock, { CC_HEADER_TYPE : CC_COMMAND_PONG }, data)
@@ -644,102 +651,70 @@ class MsgQ:
if group == None or instance == None:
return # ignore invalid packets entirely
self.subs.subscribe(group, instance, sock)
+ lname = self.fd_to_lname[sock.fileno()]
+ self.members_notify('subscribed',
+ {
+ 'client': lname,
+ 'group': group
+ })
def process_command_unsubscribe(self, sock, routing, data):
group = routing[CC_HEADER_GROUP]
instance = routing[CC_HEADER_INSTANCE]
if group == None or instance == None:
return # ignore invalid packets entirely
- self.subs.unsubscribe(group, instance, sock)
+ if self.subs.unsubscribe(group, instance, sock):
+ lname = self.fd_to_lname[sock.fileno()]
+ self.members_notify('unsubscribed',
+ {
+ 'client': lname,
+ 'group': group
+ })
def run(self):
"""Process messages. Forever. Mostly."""
self.running = True
- if self.poller:
- self.run_poller()
- else:
- self.run_kqueue()
+ self.run_select()
- def run_poller(self):
+ def run_select(self):
while self.running:
+ reads = list(self.fd_to_lname.keys())
+ if self.listen_socket.fileno() != -1: # Skip in tests
+ reads.append(self.listen_socket.fileno())
+ if self.__poller_sock and self.__poller_sock.fileno() != -1:
+ reads.append(self.__poller_sock.fileno())
+ writes = list(self.sendbuffs.keys())
+ (read_ready, write_ready) = ([], [])
try:
- # Poll with a timeout so that every once in a while,
- # the loop checks for self.running.
- events = self.poller.poll()
+ (read_ready, write_ready, _) = select.select(reads, writes,
+ []);
except select.error as err:
if err.args[0] == errno.EINTR:
- events = []
+ continue # Just try it again if interrupted.
else:
- logger.fatal(MSGQ_POLL_ERROR, err)
+ logger.fatal(MSGQ_SELECT_ERROR, err)
break
with self.__lock:
- for (fd, event) in events:
+ write_ready = set(write_ready)
+ for fd in read_ready:
+ # Do only one operation per loop iteration on the given fd.
+ # It could be possible to perform both, but it may have
+ # undesired side effects in special situations (like, if the
+ # read closes the socket).
+ if fd in write_ready:
+ write_ready.remove(fd)
if fd == self.listen_socket.fileno():
self.process_accept()
- elif fd == self.__poller_sock.fileno():
- # If it's the signal socket, we should terminate now.
+ elif self.__poller_sock and fd == \
+ self.__poller_sock.fileno():
+ # The signal socket. We should terminate now.
self.running = False
break
else:
- writable = event & select.POLLOUT
- # Note: it may be okay to read data if available
- # immediately after write some, but due to unexpected
- # regression (see comments on the kqueue version below)
- # we restrict one operation per iteration for now.
- # In future we may clarify the point and enable the
- # "read/write" mode.
- readable = not writable and (event & select.POLLIN)
- if not writable and not readable:
- logger.error(MSGQ_POLL_UNKNOWN_EVENT, fd, event)
- self._process_fd(fd, writable, readable, False)
-
- def run_kqueue(self):
- while self.running:
- # Check with a timeout so that every once in a while,
- # the loop checks for self.running.
- events = self.kqueue.control(None, 10)
- if not events:
- raise RuntimeError('serve: kqueue returned no events')
-
- with self.__lock:
- for event in events:
- if event.ident == self.listen_socket.fileno():
- self.process_accept()
- elif event.ident == self.__poller_sock.fileno():
- # If it's the signal socket, we should terminate now.
- self.running = False
- break;
- else:
- fd = event.ident
- writable = event.filter == select.KQ_FILTER_WRITE
- readable = (event.filter == select.KQ_FILTER_READ and
- event.data > 0)
- # It seems to break some of our test cases if we
- # immediately close the socket on EOF after reading
- # some data. It may be possible to avoid by tweaking
- # the test, but unless we can be sure we'll hold off.
- closed = (not readable and
- (event.flags & select.KQ_EV_EOF))
- self._process_fd(fd, writable, readable, closed)
-
- def _process_fd(self, fd, writable, readable, closed):
- '''Process a single FD: unified subroutine of run_kqueue/poller.
-
- closed can be True only in the case of kqueue. This is essentially
- private but is defined as if it were "protected" so it's callable
- from tests.
-
- '''
- # We need to check if FD is still in the sockets dict, because
- # it's possible that the socket has been "killed" while processing
- # other FDs; it's even possible it's killed within this method.
- if writable and fd in self.sockets:
- self.__process_write(fd)
- if readable and fd in self.sockets:
- self.process_packet(fd, self.sockets[fd])
- if closed and fd in self.sockets:
- self.kill_socket(fd, self.sockets[fd])
+ self.process_packet(fd, self.sockets[fd])
+ for fd in write_ready:
+ self._process_write(fd)
def stop(self):
# Signal it should terminate.
@@ -795,16 +770,27 @@ class MsgQ:
return isc.config.create_answer(0)
def command_handler(self, command, args):
- """The command handler (run in a separate thread).
- Not tested, currently effectively empty.
- """
+ """The command handler (run in a separate thread)."""
config_logger.debug(TRACE_DETAIL, MSGQ_COMMAND, command, args)
with self.__lock:
if not self.running:
return
- # TODO: Any commands go here
+ # TODO: Who does validation? The ModuleCCSession or must we?
+
+ if command == 'members':
+ # List all members of MsgQ or of a group.
+ if args is None:
+ args = {}
+ group = args.get('group')
+ if group:
+ return isc.config.create_answer(0,
+ list(map(lambda sock: self.fd_to_lname[sock.fileno()],
+ self.subs.find(group, ''))))
+ else:
+ return isc.config.create_answer(0,
+ list(self.lnames.keys()))
config_logger.error(MSGQ_COMMAND_UNKNOWN, command)
return isc.config.create_answer(1, 'unknown command: ' + command)
@@ -813,13 +799,14 @@ def signal_handler(msgq, signal, frame):
if msgq:
msgq.stop()
-if __name__ == "__main__":
+def main():
def check_port(option, opt_str, value, parser):
"""Function to insure that the port we are passed is actually
a valid port number. Used by OptionParser() on startup."""
intval = int(value)
if (intval < 0) or (intval > 65535):
- raise OptionValueError("%s requires a port number (0-65535)" % opt_str)
+ raise OptionValueError("%s requires a port number (0-65535)" %
+ opt_str)
parser.values.msgq_port = intval
# Parse any command-line options.
@@ -840,6 +827,12 @@ if __name__ == "__main__":
signal.signal(signal.SIGTERM,
lambda signal, frame: signal_handler(msgq, signal, frame))
+ # Ignore SIGPIPE. We handle errors writing to children using try in the
+ # appropriate places and the delivery of a signal is very heavy handed.
+ # Windows doesn't support SIGPIPE so don't try it there.
+ if not sys.platform.startswith('win'):
+ signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+
try:
msgq.setup()
except Exception as e:
@@ -861,13 +854,23 @@ if __name__ == "__main__":
msgq.command_handler,
None, True,
msgq.socket_file)
+ msgq._session = session
session.start()
# And we create a thread that'll just wait for commands and
# handle them. We don't terminate the thread, we set it to
# daemon. Once the main thread terminates, it'll just die.
def run_session():
while True:
- session.check_command(False)
+ # As the check_command has internal mutex that is shared
+ # with sending part (which includes notify). So we don't
+ # want to hold it long-term and block using select.
+ fileno = session.get_socket().fileno()
+ try:
+ (reads, _, _) = select.select([fileno], [], [])
+ except select.error as se:
+ if se.args[0] != errno.EINTR:
+ raise
+ session.check_command(True)
background_thread = threading.Thread(target=run_session)
background_thread.daemon = True
background_thread.start()
@@ -878,3 +881,6 @@ if __name__ == "__main__":
msgq.shutdown()
logger.info(MSGQ_EXITING)
+
+if __name__ == "__main__":
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/msgq/msgq.spec b/src/bin/msgq/msgq.spec
index 93204fa..4b388c5 100644
--- a/src/bin/msgq/msgq.spec
+++ b/src/bin/msgq/msgq.spec
@@ -3,6 +3,18 @@
"module_name": "Msgq",
"module_description": "The message queue",
"config_data": [],
- "commands": []
+ "commands": [
+ {
+ "command_name": "members",
+ "command_description": "Provide the list of members of a group or of the whole MsgQ if no group is given.",
+ "command_args": [
+ {
+ "item_name": "group",
+ "item_optional": true,
+ "item_type": "string"
+ }
+ ]
+ }
+ ]
}
}
diff --git a/src/bin/msgq/msgq_messages.mes b/src/bin/msgq/msgq_messages.mes
index 09c9030..3f6e6bc 100644
--- a/src/bin/msgq/msgq_messages.mes
+++ b/src/bin/msgq/msgq_messages.mes
@@ -19,6 +19,10 @@
# <topsrcdir>/tools/reorder_message_file.py to make sure the
# messages are in the correct order.
+% MSGQ_ALREADY_RUNNING Another copy of b10-msgq is already running.
+Only a single instance of b10-msgq should ever be run at one time.
+This instance will now terminate.
+
% MSGQ_CFGMGR_SUBSCRIBED The config manager subscribed to message queue
This is a debug message. The message queue has little bit of special handling
for the configuration manager. This special handling is happening now.
@@ -85,17 +89,6 @@ Debug message. The listener is trying to open a listening socket.
Debug message. The message queue successfully opened a listening socket and
waits for incoming connections.
-% MSGQ_POLL_ERROR Error while polling for events: %1
-A low-level error happened when waiting for events, the error is logged. The
-reason for this varies, but it usually means the system is short on some
-resources.
-
-% MSGQ_POLL_UNKNOWN_EVENT Got an unknown event from the poller for fd %1: %2
-An unknown event got out from the poll() system call. This should generally not
-happen and it is either a programmer error or OS bug. The event is ignored. The
-number noted as the event is the raw encoded value, which might be useful to
-the authors when figuring the problem out.
-
% MSGQ_RECV_ERROR Error reading from socket %1: %2
There was a low-level error when reading from a socket. The error is logged and
the corresponding socket is dropped. The errors include receiving
@@ -119,6 +112,11 @@ on shutdown unless there's really something unexpected.
% MSGQ_RECV_HDR Received header: %1
Debug message. This message includes the whole routing header of a packet.
+% MSGQ_SELECT_ERROR Error while waiting for events: %1
+A low-level error happened when waiting for events, the error is logged. The
+reason for this varies, but it usually means the system is short on some
+resources.
+
% MSGQ_SEND_ERROR Error while sending to socket %1: %2
There was a low-level error when sending data to a socket. The error is logged
and the corresponding socket is dropped.
@@ -148,3 +146,9 @@ data structure.
% MSGQ_SUBS_NEW_TARGET Creating new target for subscription to group '%1' for instance '%2'
Debug message. Creating a new subscription. Also creating a new data structure
to hold it.
+
+% MSGQ_SOCKET_TIMEOUT_ERROR Killing socket %1 because timeout exceeded (%2)
+Outgoing data was queued up on a socket connected to msgq, but the other
+side is not reading it. It could be deadlocked, or may not be monitoring
+it. Both cases are programming errors and should be corrected. The socket
+is closed on the msgq side.
diff --git a/src/bin/msgq/tests/msgq_run_test.py b/src/bin/msgq/tests/msgq_run_test.py
index 95173e0..5b0c711 100644
--- a/src/bin/msgq/tests/msgq_run_test.py
+++ b/src/bin/msgq/tests/msgq_run_test.py
@@ -272,6 +272,78 @@ class MsgqRunTest(unittest.TestCase):
conn.close()
conn = new
+ def test_notifications(self):
+ """
+ Check that the MsgQ is actually sending notifications about events.
+ We create a socket, subscribe the socket itself and see it receives
+ it's own notification.
+
+ Testing all the places where notifications happen is task for the
+ common unit tests in msgq_test.py.
+
+ The test is here, because there might be some trouble with multiple
+ threads in msgq (see the note about locking on the module CC session
+ when sending message from one thread and listening for commands in the
+ other) which would be hard to test using pure unit tests. Testing
+ runnig whole msgq tests that implicitly.
+ """
+ conn = self.__get_connection()
+ # Activate the session, pretend to be the config manager.
+ conn.group_subscribe('ConfigManager')
+ # Answer request for logging config
+ (msg, env) = conn.group_recvmsg(nonblock=False)
+ self.assertEqual({'command': ['get_config',
+ {'module_name': 'Logging'}]},
+ msg)
+ conn.group_reply(env, {'result': [0, {}]})
+ # It sends its spec.
+ (msg, env) = conn.group_recvmsg(nonblock=False)
+ self.assertEqual('module_spec', msg['command'][0])
+ conn.group_reply(env, {'result': [0]})
+ # It asks for its own config
+ (msg, env) = conn.group_recvmsg(nonblock=False)
+ self.assertEqual({'command': ['get_config',
+ {'module_name': 'Msgq'}]},
+ msg)
+ conn.group_reply(env, {'result': [0, {}]})
+ # Synchronization - make sure the session is running before
+ # we continue, so we get the notification. Similar synchronisation
+ # as in b10-init, but we don't have full ccsession here, so we
+ # do so manually.
+ synchronised = False
+ attempts = 100
+ while not synchronised and attempts > 0:
+ time.sleep(0.1)
+ seq = conn.group_sendmsg({'command': ['Are you running?']},
+ 'Msgq', want_answer=True)
+ msg = conn.group_recvmsg(nonblock=False, seq=seq)
+ synchronised = msg[0] != -1
+ attempts -= 1
+ self.assertTrue(synchronised)
+ # The actual test
+ conn.group_subscribe('notifications/cc_members')
+ (msg, env) = conn.group_recvmsg(nonblock=False)
+ self.assertEqual({'notification': ['subscribed', {
+ 'client': conn.lname,
+ 'group': 'notifications/cc_members'
+ }]}, msg)
+
+ def test_multiple_invocations(self):
+ """
+ Check to make sure that an attempt to start a second copy of the MsgQ
+ daemon fails.
+ """
+
+ self.assertTrue (os.path.exists(SOCKET_PATH))
+ self.__retcode = subprocess.call([MSGQ_PATH, '-s', SOCKET_PATH])
+ self.assertNotEqual(self.__retcode, 0)
+
+ # Verify that the socket still exists and works. We re-call
+ # test_send_direct as a means of testing that the existing
+ # daemon is still behaving correctly.
+ self.assertTrue (os.path.exists(SOCKET_PATH))
+ self.test_send_direct()
+
if __name__ == '__main__':
isc.log.init("msgq-tests")
isc.log.resetUnitTestRootLogger()
diff --git a/src/bin/msgq/tests/msgq_test.py b/src/bin/msgq/tests/msgq_test.py
index e5a5656..210246d 100644
--- a/src/bin/msgq/tests/msgq_test.py
+++ b/src/bin/msgq/tests/msgq_test.py
@@ -19,6 +19,7 @@ from msgq import SubscriptionManager, MsgQ
import unittest
import os
import socket
+import select # needed only for #3014. can be removed once it's solved
import signal
import sys
import time
@@ -63,8 +64,11 @@ class TestSubscriptionManager(unittest.TestCase):
socks = [ 's1', 's2', 's3', 's4', 's5' ]
for s in socks:
self.sm.subscribe("a", "*", s)
- self.sm.unsubscribe("a", "*", 's3')
- self.assertEqual(self.sm.find_sub("a", "*"), [ 's1', 's2', 's4', 's5' ])
+ self.assertTrue(self.sm.unsubscribe("a", "*", 's3'))
+ # Unsubscribe from group it is not in
+ self.assertFalse(self.sm.unsubscribe("a", "*", 's42'))
+ self.assertEqual(self.sm.find_sub("a", "*"),
+ [ 's1', 's2', 's4', 's5' ])
def test_unsubscribe_all(self):
self.sm.subscribe('g1', 'i1', 's1')
@@ -75,7 +79,9 @@ class TestSubscriptionManager(unittest.TestCase):
self.sm.subscribe('g2', 'i1', 's2')
self.sm.subscribe('g2', 'i2', 's1')
self.sm.subscribe('g2', 'i2', 's2')
- self.sm.unsubscribe_all('s1')
+ self.assertEqual(set([('g1', 'i1'), ('g1', 'i2'), ('g2', 'i1'),
+ ('g2', 'i2')]),
+ set(self.sm.unsubscribe_all('s1')))
self.assertEqual(self.sm.find_sub("g1", "i1"), [ 's2' ])
self.assertEqual(self.sm.find_sub("g1", "i2"), [ 's2' ])
self.assertEqual(self.sm.find_sub("g2", "i1"), [ 's2' ])
@@ -178,6 +184,157 @@ class MsgQTest(unittest.TestCase):
data = json.loads(msg[6 + header_len:].decode('utf-8'))
return (header, data)
+ def test_unknown_command(self):
+ """
+ Test the command handler returns error when the command is unknown.
+ """
+ # Fake we are running, to disable test workarounds
+ self.__msgq.running = True
+ self.assertEqual({'result': [1, "unknown command: unknown"]},
+ self.__msgq.command_handler('unknown', {}))
+
+ def test_get_members(self):
+ """
+ Test getting members of a group or of all connected clients.
+ """
+ # Push two dummy "clients" into msgq (the ugly way, by directly
+ # tweaking relevant data structures).
+ class Sock:
+ def __init__(self, fileno):
+ self.fileno = lambda: fileno
+ self.__msgq.lnames['first'] = Sock(1)
+ self.__msgq.lnames['second'] = Sock(2)
+ self.__msgq.fd_to_lname[1] = 'first'
+ self.__msgq.fd_to_lname[2] = 'second'
+ # Subscribe them to some groups
+ self.__msgq.process_command_subscribe(self.__msgq.lnames['first'],
+ {'group': 'G1', 'instance': '*'},
+ None)
+ self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
+ {'group': 'G1', 'instance': '*'},
+ None)
+ self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
+ {'group': 'G2', 'instance': '*'},
+ None)
+ # Now query content of some groups through the command handler.
+ self.__msgq.running = True # Enable the command handler
+ def check_both(result):
+ """
+ Check the result is successful one and it contains both lnames (in
+ any order).
+ """
+ array = result['result'][1]
+ self.assertEqual(set(['first', 'second']), set(array))
+ self.assertEqual({'result': [0, array]}, result)
+ # Make sure the result can be encoded as JSON
+ # (there seems to be types that look like a list but JSON choks
+ # on them)
+ json.dumps(result)
+ # Members of the G1 and G2
+ self.assertEqual({'result': [0, ['second']]},
+ self.__msgq.command_handler('members',
+ {'group': 'G2'}))
+ check_both(self.__msgq.command_handler('members', {'group': 'G1'}))
+ # We pretend that all the possible groups exist, just that most
+ # of them are empty. So requesting for Empty is request for an empty
+ # group and should not fail.
+ self.assertEqual({'result': [0, []]},
+ self.__msgq.command_handler('members',
+ {'group': 'Empty'}))
+ # Without the name of the group, we just get all the clients.
+ check_both(self.__msgq.command_handler('members', {}))
+ # Omitting the parameters completely in such case is OK
+ check_both(self.__msgq.command_handler('members', None))
+
+ def notifications_setup(self):
+ """
+ Common setup of some notifications tests. Mock several things.
+ """
+ # Mock the method to send notifications (we don't really want
+ # to send them now, just see they'd be sent).
+ # Mock the poller, as we don't need it at all (and we don't have
+ # real socket to give it now).
+ notifications = []
+ def send_notification(event, params):
+ notifications.append((event, params))
+ class FakePoller:
+ def register(self, socket, mode):
+ pass
+ def unregister(self, sock):
+ pass
+ self.__msgq.members_notify = send_notification
+ self.__msgq.poller = FakePoller()
+
+ # Create a socket
+ class Sock:
+ def __init__(self, fileno):
+ self.fileno = lambda: fileno
+ def close(self):
+ pass
+ sock = Sock(1)
+ return notifications, sock
+
+ def test_notifies(self):
+ """
+ Test the message queue sends notifications about connecting,
+ disconnecting and subscription changes.
+ """
+ notifications, sock = self.notifications_setup()
+
+ # We should notify about new cliend when we register it
+ self.__msgq.register_socket(sock)
+ lname = self.__msgq.fd_to_lname[1] # Steal the lname
+ self.assertEqual([('connected', {'client': lname})], notifications)
+ del notifications[:]
+
+ # A notification should happen for a subscription to a group
+ self.__msgq.process_command_subscribe(sock, {'group': 'G',
+ 'instance': '*'},
+ None)
+ self.assertEqual([('subscribed', {'client': lname, 'group': 'G'})],
+ notifications)
+ del notifications[:]
+
+ # As well for unsubscription
+ self.__msgq.process_command_unsubscribe(sock, {'group': 'G',
+ 'instance': '*'},
+ None)
+ self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'})],
+ notifications)
+ del notifications[:]
+
+ # Unsubscription from a group it isn't subscribed to
+ self.__msgq.process_command_unsubscribe(sock, {'group': 'H',
+ 'instance': '*'},
+ None)
+ self.assertEqual([], notifications)
+
+ # And, finally, for removal of client
+ self.__msgq.kill_socket(sock.fileno(), sock)
+ self.assertEqual([('disconnected', {'client': lname})], notifications)
+
+ def test_notifies_implicit_kill(self):
+ """
+ Test that the unsubscription notifications are sent before the socket
+ is dropped, even in case it does not unsubscribe explicitly.
+ """
+ notifications, sock = self.notifications_setup()
+
+ # Register and subscribe. Notifications for these are in above test.
+ self.__msgq.register_socket(sock)
+ lname = self.__msgq.fd_to_lname[1] # Steal the lname
+ self.__msgq.process_command_subscribe(sock, {'group': 'G',
+ 'instance': '*'},
+ None)
+ del notifications[:]
+
+ self.__msgq.kill_socket(sock.fileno(), sock)
+ # Now, the notification for unsubscribe should be first, second for
+ # the disconnection.
+ self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'}),
+ ('disconnected', {'client': lname})
+ ], notifications)
+
def test_undeliverable_errors(self):
"""
Send several packets through the MsgQ and check it generates
@@ -412,12 +569,16 @@ class SendNonblock(unittest.TestCase):
The write end is put into the message queue, so we can check it.
It returns (msgq, read_end, write_end). It is expected the sockets
are closed by the caller afterwards.
+
+ Also check the sockets are registered correctly (eg. internal data
+ structures are there for them).
'''
msgq = MsgQ()
# We do only partial setup, so we don't create the listening socket
- msgq.setup_poller()
(read, write) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
msgq.register_socket(write)
+ self.assertEqual(1, len(msgq.lnames))
+ self.assertEqual(write, msgq.lnames[msgq.fd_to_lname[write.fileno()]])
return (msgq, read, write)
def infinite_sender(self, sender):
@@ -437,8 +598,15 @@ class SendNonblock(unittest.TestCase):
# Explicitly close temporary socket pair as the Python
# interpreter expects it. It may not be 100% exception safe,
# but since this is only for tests we prefer brevity.
+ # Actually, the write end is often closed by the sender.
+ if write.fileno() != -1:
+ # Some of the senders passed here kill the socket internally.
+ # So kill it only if not yet done so. If the socket is closed,
+ # it gets -1 as fileno().
+ msgq.kill_socket(write.fileno(), write)
+ self.assertFalse(msgq.lnames)
+ self.assertFalse(msgq.fd_to_lname)
read.close()
- write.close()
def test_infinite_sendmsg(self):
"""
@@ -504,7 +672,6 @@ class SendNonblock(unittest.TestCase):
queue_pid = os.fork()
if queue_pid == 0:
signal.alarm(120)
- msgq.setup_poller()
msgq.setup_signalsock()
msgq.register_socket(queue)
msgq.run()
@@ -581,7 +748,6 @@ class SendNonblock(unittest.TestCase):
msgq = MsgQ()
# Don't need a listen_socket
msgq.listen_socket = DummySocket
- msgq.setup_poller()
msgq.setup_signalsock()
msgq.register_socket(write)
msgq.register_socket(control_write)
@@ -640,9 +806,11 @@ class SendNonblock(unittest.TestCase):
send_exception is raised by BadSocket.
"""
(write, read) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
- (control_write, control_read) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+ (control_write, control_read) = socket.socketpair(socket.AF_UNIX,
+ socket.SOCK_STREAM)
badwrite = BadSocket(write, raise_on_send, send_exception)
- self.do_send(badwrite, read, control_write, control_read, expect_answer, expect_send_exception)
+ self.do_send(badwrite, read, control_write, control_read,
+ expect_answer, expect_send_exception)
write.close()
read.close()
control_write.close()
@@ -822,9 +990,11 @@ class SocketTests(unittest.TestCase):
self.__killed_socket = None
self.__logger = self.LoggerWrapper(msgq.logger)
msgq.logger = self.__logger
+ self.__orig_select = msgq.select.select
def tearDown(self):
msgq.logger = self.__logger.orig_logger
+ msgq.select.select = self.__orig_select
def test_send_data(self):
# Successful case: _send_data() returns the hardcoded value, and
@@ -872,32 +1042,6 @@ class SocketTests(unittest.TestCase):
self.assertEqual(expected_errors, self.__logger.error_called)
self.assertEqual(expected_warns, self.__logger.warn_called)
- def test_process_fd_read_after_bad_write(self):
- '''Check the specific case of write fail followed by read attempt.
-
- The write failure results in kill_socket, then read shouldn't tried.
-
- '''
- self.__sock_error.errno = errno.EPIPE
- self.__sock.ex_on_send = self.__sock_error
- self.__msgq.process_socket = None # if called, trigger an exception
- self.__msgq._process_fd(42, True, True, False) # shouldn't crash
-
- # check the socket is deleted from the fileno=>sock dictionary
- self.assertEqual({}, self.__msgq.sockets)
-
- def test_process_fd_close_after_bad_write(self):
- '''Similar to the previous, but for checking dup'ed kill attempt'''
- self.__sock_error.errno = errno.EPIPE
- self.__sock.ex_on_send = self.__sock_error
- self.__msgq._process_fd(42, True, False, True) # shouldn't crash
- self.assertEqual({}, self.__msgq.sockets)
-
- def test_process_fd_writer_after_close(self):
- '''Emulate a "writable" socket has been already closed and killed.'''
- # This just shouldn't crash
- self.__msgq._process_fd(4200, True, False, False)
-
def test_process_packet(self):
'''Check some failure cases in handling an incoming message.'''
expected_errors = 0
@@ -931,6 +1075,47 @@ class SocketTests(unittest.TestCase):
self.assertEqual(expected_errors, self.__logger.error_called)
self.assertEqual(expected_debugs, self.__logger.debug_called)
+ def test_do_select(self):
+ """
+ Check the behaviour of the run_select method.
+
+ In particular, check that we skip writing to the sockets we read,
+ because a read may have side effects (like closing the socket) and
+ we want to prevent strange behavior.
+ """
+ self.__read_called = []
+ self.__write_called = []
+ self.__reads = None
+ self.__writes = None
+ def do_read(fd, socket):
+ self.__read_called.append(fd)
+ self.__msgq.running = False
+ def do_write(fd):
+ self.__write_called.append(fd)
+ self.__msgq.running = False
+ self.__msgq.process_packet = do_read
+ self.__msgq._process_write = do_write
+ self.__msgq.fd_to_lname = {42: 'lname', 44: 'other', 45: 'unused'}
+ # The do_select does index it, but just passes the value. So reuse
+ # the dict to safe typing in the test.
+ self.__msgq.sockets = self.__msgq.fd_to_lname
+ self.__msgq.sendbuffs = {42: 'data', 43: 'data'}
+ def my_select(reads, writes, errors):
+ self.__reads = reads
+ self.__writes = writes
+ self.assertEqual([], errors)
+ return ([42, 44], [42, 43], [])
+ msgq.select.select = my_select
+ self.__msgq.listen_socket = DummySocket
+
+ self.__msgq.running = True
+ self.__msgq.run_select()
+
+ self.assertEqual([42, 44], self.__read_called)
+ self.assertEqual([43], self.__write_called)
+ self.assertEqual({42, 44, 45}, set(self.__reads))
+ self.assertEqual({42, 43}, set(self.__writes))
+
if __name__ == '__main__':
isc.log.resetUnitTestRootLogger()
unittest.main()
diff --git a/src/bin/resolver/.gitignore b/src/bin/resolver/.gitignore
index b3abbc9..a4c02b0 100644
--- a/src/bin/resolver/.gitignore
+++ b/src/bin/resolver/.gitignore
@@ -6,3 +6,4 @@
/spec_config.h
/spec_config.h.pre
/b10-resolver.8
+/s-messages
diff --git a/src/bin/resolver/Makefile.am b/src/bin/resolver/Makefile.am
index a549e6a..36a302e 100644
--- a/src/bin/resolver/Makefile.am
+++ b/src/bin/resolver/Makefile.am
@@ -20,7 +20,7 @@ pkglibexecdir = $(libexecdir)/@PACKAGE@
CLEANFILES = *.gcno *.gcda
CLEANFILES += resolver.spec spec_config.h
-CLEANFILES += resolver_messages.cc resolver_messages.h
+CLEANFILES += resolver_messages.cc resolver_messages.h s-messages
man_MANS = b10-resolver.8
DISTCLEANFILES = $(man_MANS)
@@ -46,9 +46,11 @@ spec_config.h: spec_config.h.pre
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" spec_config.h.pre >$@
# Define rule to build logging source files from message file
-resolver_messages.h resolver_messages.cc: resolver_messages.mes
- $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/resolver/resolver_messages.mes
+resolver_messages.h resolver_messages.cc: s-messages
+s-messages: resolver_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/bin/resolver/resolver_messages.mes
+ touch $@
BUILT_SOURCES = spec_config.h resolver_messages.cc resolver_messages.h
@@ -71,7 +73,6 @@ b10_resolver_LDADD += $(top_builddir)/src/lib/acl/libb10-dnsacl.la
b10_resolver_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
b10_resolver_LDADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
b10_resolver_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
-b10_resolver_LDADD += $(top_builddir)/src/lib/xfr/libb10-xfr.la
b10_resolver_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
b10_resolver_LDADD += $(top_builddir)/src/lib/server_common/libb10-server-common.la
b10_resolver_LDADD += $(top_builddir)/src/lib/cache/libb10-cache.la
@@ -83,4 +84,3 @@ b10_resolver_LDFLAGS = -pthread
# and can't use @datadir@ because doesn't expand default ${prefix}
b10_resolverdir = $(pkgdatadir)
b10_resolver_DATA = resolver.spec
-
diff --git a/src/bin/resolver/bench/.gitignore b/src/bin/resolver/bench/.gitignore
new file mode 100644
index 0000000..5af2afe
--- /dev/null
+++ b/src/bin/resolver/bench/.gitignore
@@ -0,0 +1 @@
+/resolver-bench
diff --git a/src/bin/resolver/bench/Makefile.am b/src/bin/resolver/bench/Makefile.am
index e4689fb..346e007 100644
--- a/src/bin/resolver/bench/Makefile.am
+++ b/src/bin/resolver/bench/Makefile.am
@@ -19,7 +19,6 @@ resolver_bench_SOURCES += fake_resolution.h fake_resolution.cc
resolver_bench_SOURCES += dummy_work.h dummy_work.cc
resolver_bench_SOURCES += naive_resolver.h naive_resolver.cc
-resolver_bench_LDADD = $(GTEST_LDADD)
-resolver_bench_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+resolver_bench_LDADD = $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
resolver_bench_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
diff --git a/src/bin/resolver/main.cc b/src/bin/resolver/main.cc
index 457f285..9149ebb 100644
--- a/src/bin/resolver/main.cc
+++ b/src/bin/resolver/main.cc
@@ -168,7 +168,6 @@ main(int argc, char* argv[]) {
resolver = boost::shared_ptr<Resolver>(new Resolver());
LOG_DEBUG(resolver_logger, RESOLVER_DBG_INIT, RESOLVER_CREATED);
- SimpleCallback* checkin = resolver->getCheckinProvider();
DNSLookup* lookup = resolver->getDNSLookupProvider();
DNSAnswer* answer = resolver->getDNSAnswerProvider();
@@ -217,7 +216,7 @@ main(int argc, char* argv[]) {
cache.update(root_a_rrset);
cache.update(root_aaaa_rrset);
- DNSService dns_service(io_service, checkin, lookup, answer);
+ DNSService dns_service(io_service, lookup, answer);
resolver->setDNSService(dns_service);
LOG_DEBUG(resolver_logger, RESOLVER_DBG_INIT, RESOLVER_SERVICE_CREATED);
diff --git a/src/bin/resolver/resolver.cc b/src/bin/resolver/resolver.cc
index a3de340..2ac638f 100644
--- a/src/bin/resolver/resolver.cc
+++ b/src/bin/resolver/resolver.cc
@@ -338,25 +338,9 @@ public:
}
};
-// This is a derived class of \c SimpleCallback, to serve
-// as a callback in the asiolink module. It checks for queued
-// configuration messages, and executes them if found.
-class ConfigCheck : public SimpleCallback {
-public:
- ConfigCheck(Resolver* srv) : server_(srv) {}
- virtual void operator()(const IOMessage&) const {
- if (server_->getConfigSession()->hasQueuedMsgs()) {
- server_->getConfigSession()->checkCommand();
- }
- }
-private:
- Resolver* server_;
-};
-
Resolver::Resolver() :
impl_(new ResolverImpl()),
dnss_(NULL),
- checkin_(NULL),
dns_lookup_(NULL),
dns_answer_(new MessageAnswer),
nsas_(NULL),
@@ -365,13 +349,11 @@ Resolver::Resolver() :
// Operations referring to "this" must be done in the constructor body
// (some compilers will issue warnings if "this" is referred to in the
// initialization list).
- checkin_ = new ConfigCheck(this);
dns_lookup_ = new MessageLookup(this);
}
Resolver::~Resolver() {
delete impl_;
- delete checkin_;
delete dns_lookup_;
delete dns_answer_;
}
@@ -524,7 +506,8 @@ ResolverImpl::processNormalQuery(const IOMessage& io_message,
{
const ConstQuestionPtr question = *query_message->beginQuestion();
const RRType qtype = question->getType();
- const RRClass qclass = question->getClass();
+ // Make cppcheck happy with the reference.
+ const RRClass& qclass = question->getClass();
// Apply query ACL
const Client client(io_message);
diff --git a/src/bin/resolver/resolver.h b/src/bin/resolver/resolver.h
index 725aa85..b1608c1 100644
--- a/src/bin/resolver/resolver.h
+++ b/src/bin/resolver/resolver.h
@@ -131,9 +131,6 @@ public:
/// \brief Return pointer to the DNS Answer callback function
isc::asiodns::DNSAnswer* getDNSAnswerProvider() { return (dns_answer_); }
- /// \brief Return pointer to the Checkin callback function
- isc::asiolink::SimpleCallback* getCheckinProvider() { return (checkin_); }
-
/**
* \brief Specify the list of upstream servers.
*
@@ -259,7 +256,6 @@ public:
private:
ResolverImpl* impl_;
isc::asiodns::DNSServiceBase* dnss_;
- isc::asiolink::SimpleCallback* checkin_;
isc::asiodns::DNSLookup* dns_lookup_;
isc::asiodns::DNSAnswer* dns_answer_;
isc::nsas::NameserverAddressStore* nsas_;
diff --git a/src/bin/resolver/tests/Makefile.am b/src/bin/resolver/tests/Makefile.am
index 52b39a1..8630e0e 100644
--- a/src/bin/resolver/tests/Makefile.am
+++ b/src/bin/resolver/tests/Makefile.am
@@ -45,7 +45,6 @@ run_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
run_unittests_LDADD += $(top_builddir)/src/lib/acl/libb10-dnsacl.la
run_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
-run_unittests_LDADD += $(top_builddir)/src/lib/xfr/libb10-xfr.la
run_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
run_unittests_LDADD += $(top_builddir)/src/lib/server_common/libb10-server-common.la
run_unittests_LDADD += $(top_builddir)/src/lib/resolve/libb10-resolve.la
diff --git a/src/bin/stats/stats.py.in b/src/bin/stats/stats.py.in
index 7ec530b..38d0b02 100755
--- a/src/bin/stats/stats.py.in
+++ b/src/bin/stats/stats.py.in
@@ -1,6 +1,6 @@
#!@PYTHON@
-# Copyright (C) 2010, 2011, 2012 Internet Systems Consortium.
+# Copyright (C) 2010-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -29,6 +29,7 @@ import select
import isc.cc
import isc.config
import isc.util.process
+import isc.util.traceback_handler
import isc.log
from isc.log_messages.stats_messages import *
@@ -124,8 +125,8 @@ def _accum(a, b):
if len(a) <= i ]
# If both of args are integer or float type, two
# values are added.
- elif (type(a) is int and type(b) is int) \
- or (type(a) is float or type(b) is float):
+ elif (type(a) is int or type(a) is float) \
+ and (type(b) is int or type(b) is float):
return a + b
# If both of args are string type,
@@ -186,6 +187,11 @@ class StatsError(Exception):
"""Exception class for Stats class"""
pass
+class InitSessionTimeout(isc.cc.session.SessionTimeout):
+ """SessionTimeout Exception in the session between the stats module and the
+ init module"""
+ pass
+
class Stats:
"""
Main class of stats module
@@ -243,14 +249,12 @@ class Stats:
"""
self.update_modules()
- if self.update_statistics_data(
+ self.update_statistics_data(
self.module_name,
self.cc_session.lname,
{'lname': self.cc_session.lname,
'boot_time': get_datetime(_BASETIME),
- 'last_update_time': get_datetime()}):
- logger.warn(STATS_RECEIVED_INVALID_STATISTICS_DATA,
- self.module_name)
+ 'last_update_time': get_datetime()})
# define the variable of the last time of polling
self._lasttime_poll = 0.0
@@ -258,12 +262,8 @@ class Stats:
"""return the current value of 'poll-interval'"""
return self.config['poll-interval']
- def do_polling(self):
- """Polls modules for statistics data. Return nothing. First
- search multiple instances of same module. Second requests
- each module to invoke 'getstats'. Finally updates internal
- statistics data every time it gets from each instance."""
-
+ def _get_multi_module_list(self):
+ """Returns a module list which is running as multiple modules."""
# It counts the number of instances of same module by
# examining the third value from the array result of
# 'show_processes' of Init
@@ -277,6 +277,8 @@ class Stats:
# TODO: Is it OK to just pass? As part of refactoring, preserving
# the original behaviour.
value = None
+ except isc.cc.session.SessionTimeout as e:
+ raise InitSessionTimeout(e)
modules = []
if type(value) is list:
# NOTE: For example, the "show_processes" command
@@ -306,6 +308,12 @@ class Stats:
# release.
modules = [ v[2] if type(v) is list and len(v) > 2 \
else None for v in value ]
+ return modules
+
+ def _query_statistics(self, modules):
+ """Queries each module statistics and returns sequences to use
+ for receiving that answers based on the argument 'modules' which
+ is a list of modules running as multiple modules"""
# start requesting each module to collect statistics data
sequences = []
for (module_name, data) in self.get_statistics_data().items():
@@ -327,11 +335,17 @@ class Stats:
if cnt > 1:
sequences = sequences + [ (module_name, seq) \
for i in range(cnt-1) ]
+ return sequences
+
+ def _collect_statistics(self, sequences):
+ """Based on sequences which is a list of values returned from
+ group_sendmsg(), collects statistics data from each module. If
+ a SessionTimeout exception is raised when collecting from the
+ module, skips it and goes to collect from the next module."""
# start receiving statistics data
_statistics_data = []
- while len(sequences) > 0:
+ for (module_name, seq) in sequences:
try:
- (module_name, seq) = sequences.pop(0)
answer, env = self.cc_session.group_recvmsg(False, seq)
if answer:
rcode, args = isc.config.ccsession.parse_answer(answer)
@@ -340,25 +354,37 @@ class Stats:
(module_name, env['from'], args))
# skip this module if SessionTimeout raised
except isc.cc.session.SessionTimeout:
- pass
+ logger.warn(STATS_SKIP_COLLECTING, module_name)
+ return _statistics_data
+ def _refresh_statistics(self, statistics_data):
+ """Refreshes statistics_data into internal statistics data to
+ display, which is a list of a tuple of module_name, lname, and
+ args"""
# update statistics data
+ _statistics_data = statistics_data[:]
self.update_modules()
while len(_statistics_data) > 0:
(_module_name, _lname, _args) = _statistics_data.pop(0)
- if self.update_statistics_data(_module_name, _lname, _args):
- logger.warn(
- STATS_RECEIVED_INVALID_STATISTICS_DATA,
- _module_name)
- else:
- if self.update_statistics_data(
+ if not self.update_statistics_data(_module_name, _lname, _args):
+ self.update_statistics_data(
self.module_name,
self.cc_session.lname,
- {'last_update_time': get_datetime()}):
- logger.warn(
- STATS_RECEIVED_INVALID_STATISTICS_DATA,
- self.module_name)
- # if successfully done, set the last time of polling
+ {'last_update_time': get_datetime()})
+
+ def do_polling(self):
+ """Polls modules for statistics data. Return nothing. First
+ search multiple instances of same module. Second requests
+ each module to invoke 'getstats'. Finally updates internal
+ statistics data every time it gets from each instance."""
+ try:
+ modules = self._get_multi_module_list()
+ sequences = self._query_statistics(modules)
+ _statistics_data = self._collect_statistics(sequences)
+ self._refresh_statistics(_statistics_data)
+ except InitSessionTimeout:
+ logger.warn(STATS_SKIP_POLLING)
+ # if successfully done or skipped, set the last time of polling
self._lasttime_poll = get_timestamp()
def _check_command(self, nonblock=False):
@@ -601,7 +627,10 @@ class Stats:
self.statistics_data[m],
_accum_bymodule(self.statistics_data_bymid[m]))
- if errors: return errors
+ if errors:
+ logger.warn(
+ STATS_RECEIVED_INVALID_STATISTICS_DATA, owner)
+ return errors
def command_status(self):
"""
@@ -694,7 +723,7 @@ class Stats:
1, "specified arguments are incorrect: " \
+ "owner: " + str(owner) + ", name: " + str(name))
-if __name__ == "__main__":
+def main():
try:
parser = OptionParser()
parser.add_option(
@@ -718,3 +747,6 @@ if __name__ == "__main__":
logger.info(STATS_STOPPED_BY_KEYBOARD)
logger.info(STATS_EXITING)
+
+if __name__ == "__main__":
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/stats/stats_httpd.py.in b/src/bin/stats/stats_httpd.py.in
index c3cdb76..df0c04c 100755
--- a/src/bin/stats/stats_httpd.py.in
+++ b/src/bin/stats/stats_httpd.py.in
@@ -35,6 +35,7 @@ import re
import isc.cc
import isc.config
import isc.util.process
+import isc.util.traceback_handler
from isc.util.address_formatter import AddressFormatter
import isc.log
@@ -598,7 +599,7 @@ class StatsHttpd:
"%s: %s" % (err.__class__.__name__, err))
return string.Template(lines)
-if __name__ == "__main__":
+def main():
try:
parser = OptionParser()
parser.add_option(
@@ -622,3 +623,6 @@ if __name__ == "__main__":
logger.info(STATSHTTPD_STOPPED_BY_KEYBOARD)
logger.info(STATSHTTPD_EXITING)
+
+if __name__ == "__main__":
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/stats/stats_messages.mes b/src/bin/stats/stats_messages.mes
index b6f0b16..38dddcc 100644
--- a/src/bin/stats/stats_messages.mes
+++ b/src/bin/stats/stats_messages.mes
@@ -64,6 +64,17 @@ will respond with an error and the command will be ignored.
This debug message is printed when a request is sent to the module
to send its data to the stats module.
+% STATS_SKIP_COLLECTING skipped collecting statistics from %1
+The stats module temporarily encountered an internal messaging bus error while
+collecting statistics from the module, then it skipped collecting statistics
+from it and will try to collect from the next module. The lack of statistics
+will be recovered at the next polling round.
+
+% STATS_SKIP_POLLING skipped polling statistics to modules
+The stats module temporarily encountered an internal messaging bus error while
+collecting initial information to collect statistics from the init module, then
+it skipped polling statistics and will try to do next time.
+
% STATS_STARTING starting
The stats module will be now starting.
diff --git a/src/bin/stats/tests/stats-httpd_test.py b/src/bin/stats/tests/stats-httpd_test.py
index a61ee47..a37302d 100644
--- a/src/bin/stats/tests/stats-httpd_test.py
+++ b/src/bin/stats/tests/stats-httpd_test.py
@@ -248,6 +248,7 @@ class TestHttpHandler(unittest.TestCase):
def tearDown(self):
self.client.close()
+ self.stats_httpd_server.shutdown()
# reset the signal handler
self.sig_handler.reset()
diff --git a/src/bin/stats/tests/stats_test.py b/src/bin/stats/tests/stats_test.py
index e010c97..14e49f9 100644
--- a/src/bin/stats/tests/stats_test.py
+++ b/src/bin/stats/tests/stats_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2010, 2011, 2012 Internet Systems Consortium.
+# Copyright (C) 2010-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -31,6 +31,7 @@ import sys
import stats
import isc.log
from test_utils import MyStats
+from isc.config.ccsession import create_answer
class TestUtilties(unittest.TestCase):
items = [
@@ -132,6 +133,8 @@ class TestUtilties(unittest.TestCase):
self.assertEqual(stats._accum("a", None), "a")
self.assertEqual(stats._accum(1, 2), 3)
self.assertEqual(stats._accum(0.5, 0.3), 0.8)
+ self.assertEqual(stats._accum(1, 0.3), 1.3)
+ self.assertEqual(stats._accum(0.5, 2), 2.5)
self.assertEqual(stats._accum('aa','bb'), 'bb')
self.assertEqual(stats._accum('1970-01-01T09:00:00Z','2012-08-09T09:33:31Z'),
'2012-08-09T09:33:31Z')
@@ -296,6 +299,43 @@ class TestStats(unittest.TestCase):
return isc.config.ccsession.parse_answer(
stats.command_handler(command_name, params))
+ def test_check_command(self):
+ """Test _check_command sets proper timeout values and sets proper values
+ returned from group_recvmsg"""
+ stat = MyStats()
+ set_timeouts = []
+ orig_timeout = 1
+ stat.cc_session.get_timeout = lambda: orig_timeout
+ stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
+ msg = create_answer(0, 'msg')
+ env = {'from': 'frominit'}
+ stat._answers = [(msg, env)]
+ stat._check_command()
+ self.assertListEqual([500, orig_timeout], set_timeouts)
+ self.assertEqual(msg, stat.mccs._msg)
+ self.assertEqual(env, stat.mccs._env)
+
+ def test_check_command_sessiontimeout(self):
+ """Test _check_command doesn't perform but sets proper timeout values in
+ case that a SesstionTimeout exception is caught while doing
+ group_recvmsg()"""
+ stat = MyStats()
+ set_timeouts = []
+ orig_timeout = 1
+ stat.cc_session.get_timeout = lambda: orig_timeout
+ stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
+ msg = create_answer(0, 'msg')
+ env = {'from': 'frominit'}
+ stat._answers = [(msg, env)]
+ # SessionTimeout is raised while doing group_recvmsg()
+ ex = isc.cc.session.SessionTimeout
+ def __raise(*x): raise ex(*x)
+ stat.cc_session.group_recvmsg = lambda x: __raise()
+ stat._check_command()
+ self.assertListEqual([500, orig_timeout], set_timeouts)
+ self.assertEqual(None, stat.mccs._msg)
+ self.assertEqual(None, stat.mccs._env)
+
def test_start(self):
# Define a separate exception class so we can be sure that's actually
# the one raised in __check_start() below
@@ -315,6 +355,25 @@ class TestStats(unittest.TestCase):
self.assertRaises(CheckException, self.stats.start)
self.assertEqual(self.__send_command(self.stats, "status"),
(0, "Stats is up. (PID " + str(os.getpid()) + ")"))
+ self.assertTrue(self.stats.mccs.stopped)
+
+ def test_start_set_next_polltime(self):
+ """Test start() properly sets the time next_polltime to do_poll() next
+ time"""
+ orig_get_timestamp = stats.get_timestamp
+ stats.get_timestamp = lambda : self.const_timestamp
+ stat = MyStats()
+ # manupilate next_polltime to go it through the inner if-condition
+ stat.next_polltime = self.const_timestamp - stat.get_interval() - 1
+ # stop an infinity loop at once
+ def __stop_running(): stat.running = False
+ stat.do_polling = __stop_running
+ # do nothing in _check_command()
+ stat._check_command = lambda: None
+ stat.start()
+ # check stat.next_polltime reassigned
+ self.assertEqual(self.const_timestamp, stat.next_polltime)
+ stats.get_timestamp = orig_get_timestamp
def test_shutdown(self):
def __check_shutdown(tested_stats):
@@ -697,7 +756,6 @@ class TestStats(unittest.TestCase):
# We use the knowledge of what kind of messages are sent via
# do_polling, and return the following faked answer directly.
- create_answer = isc.config.ccsession.create_answer # shortcut
self.stats._answers = [
# Answer for "show_processes"
(create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
@@ -817,7 +875,6 @@ class TestStats(unittest.TestCase):
# see the comment for test_update_statistics_data_withmid. We abuse
# do_polling here, too. With #2781 we should make it more direct.
- create_answer = isc.config.ccsession.create_answer # shortcut
stat._answers = [\
# Answer for "show_processes"
(create_answer(0, []), None),
@@ -868,7 +925,6 @@ class TestStats(unittest.TestCase):
self.stats.update_modules = lambda: None
# Test data borrowed from test_update_statistics_data_withmid
- create_answer = isc.config.ccsession.create_answer # shortcut
self.stats._answers = [
(create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
[1035, 'b10-auth-2', 'Auth']]), None),
@@ -1281,12 +1337,177 @@ class TestStats(unittest.TestCase):
isc.config.create_answer(
1, "module name is not specified"))
+ def test_get_multi_module_list(self):
+ """Test _get_multi_module_list() returns a module list which is running
+ as multiple modules."""
+ stat = MyStats()
+ # no answer
+ self.assertListEqual([], stat._get_multi_module_list())
+ # proc list returned
+ proc_list = [
+ [29317, 'b10-xfrout', 'Xfrout'],
+ [29318, 'b10-xfrin', 'Xfrin'],
+ [20061, 'b10-auth','Auth'],
+ [20103, 'b10-auth-2', 'Auth']]
+ mod_list = [ a[2] for a in proc_list ]
+ stat._answers = [
+ # Answer for "show_processes"
+ (create_answer(0, proc_list), {'from': 'init'})
+ ]
+ self.assertListEqual(mod_list, stat._get_multi_module_list())
+ # invalid proc list
+ stat._answers = [
+ # Answer for "show_processes"
+ (create_answer(0, [[999, 'invalid', 'Invalid'], 'invalid']),
+ {'from': 'init'})
+ ]
+ self.assertListEqual(['Invalid', None], stat._get_multi_module_list())
+
+ def test_get_multi_module_list_rpcrecipientmissing(self):
+ """Test _get_multi_module_list() raises an RPCRecipientMissing exception
+ if rcp_call() raise the exception"""
+ # RPCRecipientMissing case
+ stat = MyStats()
+ ex = isc.config.RPCRecipientMissing
+ def __raise(*x): raise ex(*x)
+ stat.mccs.rpc_call = lambda x,y: __raise('Error')
+ self.assertRaises(ex, stat._get_multi_module_list)
+
+ def test_get_multi_module_list_rpcerror(self):
+ """Test _get_multi_module_list() returns an empty list if rcp_call()
+ raise an RPCError exception"""
+ # RPCError case
+ stat = MyStats()
+ ex = isc.config.RPCError
+ def __raise(*x): raise ex(*x)
+ stat.mccs.rpc_call = lambda x,y: __raise(99, 'Error')
+ self.assertListEqual([], stat._get_multi_module_list())
+
+ def test_get_multi_module_list_initsessiontimeout(self):
+ """Test _get_multi_module_list() raises an InitSeeionTimeout exception
+ if a CC session times out in rcp_call()"""
+ # InitSeeionTimeout case
+ stat = MyStats()
+ ex = isc.cc.session.SessionTimeout
+ def __raise(*x): raise ex(*x)
+ stat.mccs.rpc_call = lambda x,y: __raise()
+ self.assertRaises(stats.InitSessionTimeout, stat._get_multi_module_list)
+
+ def test_query_statistics(self):
+ """Test _query_statistics returns a list of pairs of module and
+ sequences from group_sendmsg()"""
+ stat = MyStats()
+ # imitate stat.get_statistics_data().items. The order of the array
+ # returned by items() should be preserved.
+ class DummyDict:
+ def items(self):
+ return [('Init', 'dummy'), ('Stats', 'dummy'), ('Auth', 'dummy')]
+ stat.get_statistics_data = lambda: DummyDict()
+ mod = ('Init', 'Auth', 'Auth')
+ seq = [('Init', stat._seq + 1),
+ ('Auth', stat._seq + 2),
+ ('Auth', stat._seq + 2) ]
+ self.assertListEqual(seq, stat._query_statistics(mod))
+
+ def test_collect_statistics(self):
+ """Test _collect_statistics() collects statistics data from each module
+ based on the sequences which is a list of values returned from
+ group_sendmsg()"""
+ stat = MyStats()
+ seq = [ ('Init', stat._seq + 1),
+ ('Auth', stat._seq + 2),
+ ('Auth', stat._seq + 2) ]
+ ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
+ ('Auth', 'fromauth1', {'queries.tcp': 100}),
+ ('Auth', 'fromauth2', {'queries.udp': 200})]
+ stat._answers = [
+ (create_answer(0, r[2]), {'from': r[1]}) for r in ret
+ ]
+ self.assertListEqual(ret, stat._collect_statistics(seq))
+
+ def test_collect_statistics_nodata(self):
+ """Test _collect_statistics() returns empty statistics data if
+ a module returns an empty list"""
+ stat = MyStats()
+ seq = []
+ stat._answers = []
+ ret = []
+ self.assertListEqual(ret, stat._collect_statistics(seq))
+
+ def test_collect_statistics_nonzero_rcode(self):
+ """Test _collect_statistics() returns empty statistics data if
+ a module returns non-zero rcode"""
+ stat = MyStats()
+ seq = [('Init', stat._seq + 1)]
+ stat._answers = [
+ (create_answer(1, 'error'), {'from': 'frominit'})
+ ]
+ ret = []
+ self.assertListEqual(ret, stat._collect_statistics(seq))
+
+ def test_collect_statistics_sessiontimeout(self):
+ """Test _collect_statistics() collects statistics data from each module
+ based on the sequences which is a list of values returned from
+ group_sendmsg(). In this test case, SessionTimeout exceptions are raised
+ while collecting from Auth. This tests _collect_statistics skips
+ collecting from Auth."""
+ # SessionTimeout case
+ stat = MyStats()
+ ex = isc.cc.session.SessionTimeout
+ def __raise(*x): raise ex(*x)
+ # SessionTimeout is raised when asking to Auth
+ stat.cc_session.group_recvmsg = lambda x,seq: \
+ __raise() if seq == stat._seq + 2 else stat._answers.pop(0)
+ seq = [ ('Init', stat._seq + 1),
+ ('Auth', stat._seq + 2),
+ ('Auth', stat._seq + 2) ]
+ ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'})]
+ stat._answers = [
+ (create_answer(0, r[2]), {'from': r[1]}) for r in ret
+ ]
+ self.assertListEqual(ret, stat._collect_statistics(seq))
+
+ def test_refresh_statistics(self):
+ """Test _refresh_statistics() refreshes statistics data from given data
+ """
+ stat = MyStats()
+ self.assertEqual(self.const_default_datetime,
+ stat.statistics_data['Init']['boot_time'])
+ self.assertEqual(0,
+ stat.statistics_data['Auth']['queries.tcp'])
+ self.assertEqual(0,
+ stat.statistics_data['Auth']['queries.udp'])
+ # change stats.get_datetime() for testing 'last_update_time'
+ orig_get_datetime = stats.get_datetime
+ stats.get_datetime = lambda : self.const_datetime
+ arg = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
+ ('Auth', 'fromauth1', {'queries.tcp': 100}),
+ ('Auth', 'fromauth2', {'queries.udp': 200})]
+ stat._refresh_statistics(arg)
+ self.assertEqual('2013-01-01T00:00:00Z',
+ stat.statistics_data['Init']['boot_time'])
+ self.assertEqual(100,
+ stat.statistics_data['Auth']['queries.tcp'])
+ self.assertEqual(200,
+ stat.statistics_data['Auth']['queries.udp'])
+ self.assertEqual(self.const_datetime,
+ stat.statistics_data['Stats']['last_update_time'])
+ stats.get_datetime = orig_get_datetime
+
def test_polling_init(self):
"""check statistics data of 'Init'."""
stat = MyStats()
+ # At this point 'stat' is initialized with statistics for Stats,
+ # Init and Auth modules. In this test, we only need to check for Init
+ # statistics, while do_polling() can ask for module statistics in an
+ # unpredictable order (if hash randomization is enabled, which is
+ # the case by default for Python 3.3). To make it predictable and
+ # ensure the prepared answer doesn't cause disruption, we remove the
+ # information for the Auth module for this test.
+ del stat.statistics_data['Auth']
+
stat.update_modules = lambda: None
- create_answer = isc.config.ccsession.create_answer # shortcut
stat._answers = [
# Answer for "show_processes"
@@ -1305,7 +1526,6 @@ class TestStats(unittest.TestCase):
"""check statistics data of multiple instances of same module."""
stat = MyStats()
stat.update_modules = lambda: None
- create_answer = isc.config.ccsession.create_answer # shortcut
# Test data borrowed from test_update_statistics_data_withmid
stat._answers = [
@@ -1371,40 +1591,57 @@ class TestStats(unittest.TestCase):
self.assertEqual(stat.statistics_data['Stats']['lname'],
stat.mccs._session.lname)
- def test_polling2(self):
- """Test do_polling() doesn't incorporate broken statistics data.
-
- Actually, this is not a test for do_polling() itself. It's bad, but
- fixing that is a subject of different ticket.
-
+ def test_refresh_statistics_broken_statistics_data(self):
+ """Test _refresh_statistics() doesn't incorporate broken statistics data
"""
stat = MyStats()
# check default statistics data of 'Init'
self.assertEqual(
- stat.statistics_data['Init'],
- {'boot_time': self.const_default_datetime})
+ {'boot_time': self.const_default_datetime},
+ stat.statistics_data['Init'])
+ last_update_time = stat.statistics_data['Stats']['last_update_time']
+ # _refresh_statistics() should ignore the invalid statistics_data(type
+ # of boot_time is invalid); default data shouldn't be replaced.
+ arg = [('Init', 'lname', {'boot_time': 1})]
+ stat._refresh_statistics(arg)
+ self.assertEqual(
+ {'boot_time': self.const_default_datetime},
+ stat.statistics_data['Init'])
+ # 'last_update_time' doesn't change
+ self.assertEqual(
+ last_update_time,
+ stat.statistics_data['Stats']['last_update_time'])
- # set invalid statistics
- create_answer = isc.config.ccsession.create_answer # shortcut
- stat._answers = [
- # Answer for "show_processes"
- (create_answer(0, []), None),
- # Answers for "getstats" for Init (type of boot_time is invalid)
- (create_answer(0, {'boot_time': 1}), {'from': 'init'}),
- ]
- stat.update_modules = lambda: None
+ def test_polling_update_lasttime_poll(self):
+ """Test _lasttime_poll is updated after do_polling()
+ """
+ orig_get_timestamp = stats.get_timestamp
+ stats.get_timestamp = lambda : self.const_timestamp
+ stat = MyStats()
+ self.assertEqual(0.0, stat._lasttime_poll)
+ stat.do_polling()
+ self.assertEqual(self.const_timestamp, stat._lasttime_poll)
+ stats.get_timestamp = orig_get_timestamp
- # do_polling() should ignore the invalid answer;
- # default data shouldn't be replaced.
+ def test_polling_initsessiontimeout(self):
+ """Test _lasttime_poll is updated after do_polling() in case that it catches
+ InitSesionTimeout at _get_multi_module_list()
+ """
+ orig_get_timestamp = stats.get_timestamp
+ stats.get_timestamp = lambda : self.const_timestamp
+ ex = stats.InitSessionTimeout
+ def __raise(*x): raise ex(*x)
+ stat = MyStats()
+ self.assertEqual(0.0, stat._lasttime_poll)
+ stat._get_multi_module_list = lambda: __raise()
stat.do_polling()
- self.assertEqual(
- stat.statistics_data['Init'],
- {'boot_time': self.const_default_datetime})
+ self.assertEqual(self.const_timestamp, stat._lasttime_poll)
+ stats.get_timestamp = orig_get_timestamp
class Z_TestOSEnv(unittest.TestCase):
# Running this test would break logging setting. To prevent it from
- # affecting other tests we use the same workaround as
- # Z_TestStatsHttpdError.
+ # affecting other tests we use the same workaround as Z_TestOSEnv in
+ # stats-httpd_test.py.
def test_osenv(self):
"""
test for the environ variable "B10_FROM_SOURCE"
diff --git a/src/bin/stats/tests/test_utils.py b/src/bin/stats/tests/test_utils.py
index 8886ad2..99cc980 100644
--- a/src/bin/stats/tests/test_utils.py
+++ b/src/bin/stats/tests/test_utils.py
@@ -311,6 +311,8 @@ class MyModuleCCSession(isc.config.ConfigData):
self.stopped = False
self.closed = False
self.lname = 'mock_mod_ccs'
+ self._msg = None
+ self._env = None
def start(self):
pass
@@ -321,6 +323,10 @@ class MyModuleCCSession(isc.config.ConfigData):
def close(self):
self.closed = True
+ def check_command_without_recvmsg(self, msg, env):
+ self._msg = msg
+ self._env = env
+
class MyStats(stats.Stats):
"""A faked Stats class for unit tests.
@@ -338,7 +344,7 @@ class MyStats(stats.Stats):
# may want to inspect or tweak them.
# initial seq num for faked group_sendmsg, arbitrary choice.
- self.__seq = 4200
+ self._seq = 4200
# if set, use them as faked response to group_recvmsg (see below).
# it's a list of tuples, each of which is of (answer, envelope).
self._answers = []
@@ -408,10 +414,10 @@ class MyStats(stats.Stats):
generated sequence number.
"""
- self.__seq += 1
- return self.__seq
+ self._seq += 1
+ return self._seq
- def __group_recvmsg(self, nonblocking, seq):
+ def __group_recvmsg(self, nonblocking = True, seq = None):
"""Faked ModuleCCSession.group_recvmsg for tests.
Skipping actual network communication, and returning an internally
@@ -514,6 +520,14 @@ class MyStatsHttpd(stats_httpd.StatsHttpd):
self.cc_session = self.mccs._session
self.mccs.start = self.load_config # force reload
+ # check_command could be called from the main select() loop due to
+ # Linux's bug of spurious wakeup. We don't need the actual behavior
+ # of check_command in our tests, so we can basically replace it with a
+ # no-op mock function.
+ def mock_check_command(nonblock):
+ pass
+ self.mccs.check_command = mock_check_command
+
def close_mccs(self):
super().close_mccs()
if self.__dummy_sock is not None:
@@ -562,3 +576,6 @@ class MyStatsHttpd(stats_httpd.StatsHttpd):
def run(self):
self._started.set()
self.start()
+
+ def shutdown(self):
+ self.command_handler('shutdown', None)
diff --git a/src/bin/tests/process_rename_test.py.in b/src/bin/tests/process_rename_test.py.in
index ea8ad87..7e6a9c9 100644
--- a/src/bin/tests/process_rename_test.py.in
+++ b/src/bin/tests/process_rename_test.py.in
@@ -25,6 +25,11 @@ class TestRename(unittest.TestCase):
def __scan(self, directory, script, fun):
# Scan one script if it contains call to the renaming function
filename = os.path.join(directory, script)
+ if not os.path.exists(filename):
+ # We either didn't compile it yet or it is not compiled based
+ # on some environmental condition. That is OK, we don't want
+ # to break on that.
+ return
with open(filename) as f:
data = ''.join(f.readlines())
prettyname = 'src' + filename[filename.rfind('../') + 2:]
diff --git a/src/bin/usermgr/b10-cmdctl-usermgr.py.in b/src/bin/usermgr/b10-cmdctl-usermgr.py.in
index 37be304..b0fd30f 100755
--- a/src/bin/usermgr/b10-cmdctl-usermgr.py.in
+++ b/src/bin/usermgr/b10-cmdctl-usermgr.py.in
@@ -75,7 +75,7 @@ class UserManager:
# Just let any file read error bubble up; it will
# be caught in the run() method
with open(self.options.output_file, newline='') as csvfile:
- reader = csv.reader(csvfile)
+ reader = csv.reader(csvfile, strict=True)
for row in reader:
self.user_info[row[0]] = row
diff --git a/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py b/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py
index 3d0d4af..2da418d 100644
--- a/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py
+++ b/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py
@@ -133,6 +133,8 @@ class TestUserMgr(unittest.TestCase):
May be None, in which case the check is skipped.
expected_stderr, (multiline) string that is checked against stderr.
May be None, in which case the check is skipped.
+
+ Returns the standard output and error captured to a string.
"""
(returncode, stdout, stderr) = run(command)
if expected_stderr is not None:
@@ -140,6 +142,7 @@ class TestUserMgr(unittest.TestCase):
if expected_stdout is not None:
self.assertEqual(expected_stdout, stdout.decode())
self.assertEqual(expected_returncode, returncode, " ".join(command))
+ return (stdout.decode(), stderr.decode())
def test_help(self):
self.run_check(0,
@@ -396,6 +399,19 @@ Options:
'add', 'user1', 'pass1'
])
+ @unittest.skipIf(os.getuid() == 0,
+ 'test cannot be run as root user')
+ def test_bad_file_permissions(self):
+ """
+ Check for graceful handling of bad file argument
+ """
+ # Create the test file
+ self.run_check(0, None, None,
+ [ self.TOOL,
+ '-f', self.OUTPUT_FILE,
+ 'add', 'user1', 'pass1'
+ ])
+
# Make it non-writable (don't worry about cleanup, the
# file should be deleted after each test anyway
os.chmod(self.OUTPUT_FILE, stat.S_IRUSR)
@@ -468,14 +484,26 @@ Options:
# I can only think of one invalid format, an unclosed string
with open(self.OUTPUT_FILE, 'w', newline='') as f:
f.write('a,"\n')
- self.run_check(2,
- 'Using accounts file: test_users.csv\n'
- 'Error parsing csv file: newline inside string\n',
+ # Different versions of the csv library return different errors.
+ # So we need to check the output in a little more complex way.
+ # We ask the run_check not to check the output and check it
+ # ourselves.
+ (stdout, stderr) = self.run_check(2, None,
'',
[ self.TOOL,
'-f', self.OUTPUT_FILE,
'add', 'user1', 'pass1'
])
+ # This looks little bit awkward, but is probably easiest with
+ # just 2 known possibilities. If there are more, we'll have to
+ # think of something else.
+ self.assertTrue(stdout ==
+ 'Using accounts file: test_users.csv\n'
+ 'Error parsing csv file: newline inside string\n' or
+ stdout ==
+ 'Using accounts file: test_users.csv\n'
+ 'Error parsing csv file: unexpected end of data\n')
+
if __name__== '__main__':
diff --git a/src/bin/xfrin/b10-xfrin.xml b/src/bin/xfrin/b10-xfrin.xml
index 0da7ce0..0b58f9a 100644
--- a/src/bin/xfrin/b10-xfrin.xml
+++ b/src/bin/xfrin/b10-xfrin.xml
@@ -111,7 +111,7 @@ in separate zonemgr process.
<varname>class</varname> (defaults to <quote>IN</quote>),
<varname>master_addr</varname> (the zone master to transfer from),
<varname>master_port</varname> (defaults to 53),
- <varname>use_ixfr</varname> (defaults to false), and
+ <varname>request_ixfr</varname> (defaults to yes), and
<varname>tsig_key</varname> (optional TSIG key name to use).
The <varname>tsig_key</varname> is specified using a name that
corresponds to one of the TSIG keys configured in the global
@@ -231,102 +231,214 @@ operation
<variablelist>
<varlistentry>
- <term><replaceable>zonename</replaceable></term>
+ <term><replaceable>classname</replaceable></term>
<listitem><simpara>
- An actual zone name or special zone name
- <quote>_SERVER_</quote> representing the entire server.
- Zone classes (e.g. IN, CH, and HS) are mixed and counted so
- far. But these will be distinguished in future release.
+ An actual RR class name of the zone, e.g. IN, CH, and HS
</simpara>
<variablelist>
<varlistentry>
- <term>soaoutv4</term>
+ <term><replaceable>zonename</replaceable></term>
<listitem><simpara>
- Number of IPv4 SOA queries sent from Xfrin
- </simpara></listitem>
- </varlistentry>
+ An actual zone name or special zone name
+ <quote>_SERVER_</quote> representing the entire server
+ </simpara>
+ <variablelist>
+
+ <varlistentry>
+ <term>soaoutv4</term>
+ <listitem><simpara>
+ Number of IPv4 SOA queries sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>soaoutv6</term>
+ <listitem><simpara>
+ Number of IPv6 SOA queries sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>axfrreqv4</term>
+ <listitem><simpara>
+ Number of IPv4 AXFR requests sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>axfrreqv6</term>
+ <listitem><simpara>
+ Number of IPv6 AXFR requests sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>ixfrreqv4</term>
+ <listitem><simpara>
+ Number of IPv4 IXFR requests sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>ixfrreqv6</term>
+ <listitem><simpara>
+ Number of IPv6 IXFR requests sent from Xfrin
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>xfrsuccess</term>
+ <listitem><simpara>
+ Number of zone transfer requests succeeded.
+ These include the case where the zone turns
+ out to be the latest as a result of an
+ initial SOA query (and there is actually no
+ AXFR or IXFR transaction).
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>xfrfail</term>
+ <listitem><simpara>
+ Number of zone transfer requests failed
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>last_axfr_duration</term>
+ <listitem><simpara>
+ Duration in seconds of the last successful AXFR. 0.0
+ means no successful AXFR done or means a successful AXFR
+ done in less than a microsecond. If an AXFR is aborted
+ due to some failure, this duration won't be updated.
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>last_ixfr_duration</term>
+ <listitem><simpara>
+ Duration in seconds of the last successful IXFR. 0.0
+ means no successful IXFR done or means a successful IXFR
+ done in less than a microsecond. If an IXFR is aborted
+ due to some failure, this duration won't be updated.
+ </simpara></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </listitem>
+ </varlistentry><!-- end of zonename -->
- <varlistentry>
- <term>soaoutv6</term>
- <listitem><simpara>
- Number of IPv6 SOA queries sent from Xfrin
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>axfrreqv4</term>
- <listitem><simpara>
- Number of IPv4 AXFR requests sent from Xfrin
- </simpara></listitem>
- </varlistentry>
+ </variablelist>
+ </listitem>
+ </varlistentry><!-- end of classname -->
- <varlistentry>
- <term>axfrreqv6</term>
- <listitem><simpara>
- Number of IPv6 AXFR requests sent from Xfrin
- </simpara></listitem>
- </varlistentry>
+ </variablelist>
+ </listitem>
+ </varlistentry><!-- end of zones -->
- <varlistentry>
- <term>ixfrreqv4</term>
- <listitem><simpara>
- Number of IPv4 IXFR requests sent from Xfrin
- </simpara></listitem>
- </varlistentry>
+ <varlistentry>
+ <term>ixfr_running</term>
+ <listitem><simpara>
+ Number of IXFRs in progress
+ </simpara></listitem>
+ </varlistentry>
- <varlistentry>
- <term>ixfrreqv6</term>
- <listitem><simpara>
- Number of IPv6 IXFR requests sent from Xfrin
- </simpara></listitem>
- </varlistentry>
+ <varlistentry>
+ <term>axfr_running</term>
+ <listitem><simpara>
+ Number of AXFRs in progress
+ </simpara></listitem>
+ </varlistentry>
- <varlistentry>
- <term>xfrsuccess</term>
- <listitem><simpara>
- Number of zone transfer requests succeeded.
- These include the case where the zone turns
- out to be the latest as a result of an
- initial SOA query (and there is actually no
- AXFR or IXFR transaction).
- </simpara></listitem>
- </varlistentry>
+ <varlistentry>
+ <term>soa_in_progress</term>
+ <listitem><simpara>
+ Number of SOA queries in progress
+ </simpara></listitem>
+ </varlistentry>
- <varlistentry>
- <term>xfrfail</term>
- <listitem><simpara>
- Number of zone transfer requests failed
- </simpara></listitem>
- </varlistentry>
+ <varlistentry>
+ <term>socket</term>
+ <listitem><simpara>
+ A directory name of socket statistics
+ </simpara>
+ <variablelist>
- <varlistentry>
- <term>last_axfr_duration</term>
- <listitem><simpara>
- Duration in seconds of the last successful AXFR. 0.0
- means no successful AXFR done or means a successful AXFR
- done in less than a microsecond. If an AXFR is aborted
- due to some failure, this duration won't be updated.
- </simpara></listitem>
- </varlistentry>
+ <varlistentry>
+ <term><replaceable>ipversion</replaceable></term>
+ <listitem><simpara>
+ A directory name of an IP version as ipv4 or ipv6
+ </simpara>
+ <variablelist>
<varlistentry>
- <term>last_ixfr_duration</term>
+ <term>tcp</term>
<listitem><simpara>
- Duration in seconds of the last successful IXFR. 0.0
- means no successful IXFR done or means a successful IXFR
- done in less than a microsecond. If an IXFR is aborted
- due to some failure, this duration won't be updated.
- </simpara></listitem>
- </varlistentry>
+ A directory name of TCP statistics
+ </simpara>
+ <variablelist>
+
+ <varlistentry>
+ <term>open</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets opened successfully
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>openfail</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets open failures
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>close</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets closed
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>connfail</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets connection failures
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>conn</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP connections established successfully
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>senderr</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets send errors
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>recverr</term>
+ <listitem><simpara>
+ IPv4 or IPv6 TCP sockets receive errors
+ </simpara></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </listitem>
+ </varlistentry><!-- end of tcp -->
</variablelist>
</listitem>
- </varlistentry><!-- end of zonename -->
+ </varlistentry><!-- end of ipv4 | ipv6 -->
</variablelist>
</listitem>
- </varlistentry><!-- end of zones -->
+ </varlistentry><!-- end of socket -->
</variablelist>
diff --git a/src/bin/xfrin/tests/xfrin_test.py b/src/bin/xfrin/tests/xfrin_test.py
index 2305cdb..4158638 100644
--- a/src/bin/xfrin/tests/xfrin_test.py
+++ b/src/bin/xfrin/tests/xfrin_test.py
@@ -129,20 +129,17 @@ class XfrinTestException(Exception):
class XfrinTestTimeoutException(Exception):
pass
-class MockCC(MockModuleCCSession):
- def get_default_value(self, identifier):
- # The returned values should be identical to the spec file
- # XXX: these should be retrieved from the spec file
- # (see MyCCSession of xfrout_test.py.in)
- if identifier == "zones/master_port":
- return TEST_MASTER_PORT
- if identifier == "zones/class":
- return TEST_RRCLASS_STR
- if identifier == "zones/use_ixfr":
- return False
+class MockCC(MockModuleCCSession, ConfigData):
+ def __init__(self):
+ super().__init__()
+ module_spec = isc.config.module_spec_from_file(
+ xfrin.SPECFILE_LOCATION)
+ ConfigData.__init__(self, module_spec)
+ # For inspection
+ self.added_remote_modules = []
def add_remote_config_by_name(self, name, callback):
- pass
+ self.added_remote_modules.append((name, callback))
def get_remote_config_value(self, module, identifier):
if module == 'tsig_keys' and identifier == 'keys':
@@ -242,6 +239,37 @@ class MockDataSourceClient():
self.committed_diffs.append(self.diffs)
self.diffs = []
+ def create_zone(self, zone_name):
+ # pretend it just succeeds
+ pass
+
+class MockDataSrcClientsMgr():
+ def __init__(self):
+ # Default faked result of get_client_list, customizable by tests
+ self.found_datasrc_client_list = self
+
+ # Default faked result of find(), customizable by tests
+ self.found_datasrc_client = MockDataSourceClient()
+
+ self.reconfigure_param = [] # for inspection
+
+ def get_client_list(self, rrclass):
+ return self.found_datasrc_client_list
+
+ def reconfigure(self, arg1, arg2):
+ # the only current test simply needs to know this is called with
+ # the expected arguments and exceptions are handled. if we need more
+ # variations in tests, this mock method should be extended.
+ self.reconfigure_param.append((arg1, arg2))
+ raise isc.server_common.datasrc_clients_mgr.ConfigError(
+ 'reconfigure failure')
+
+ def find(self, zone_name, want_exact_match, want_finder):
+ """Pretending find method on the object returned by get_clinet_list"""
+ if issubclass(type(self.found_datasrc_client), Exception):
+ raise self.found_datasrc_client
+ return self.found_datasrc_client, None, None
+
class MockXfrin(Xfrin):
# This is a class attribute of a callable object that specifies a non
# default behavior triggered in _cc_check_command(). Specific test methods
@@ -251,35 +279,29 @@ class MockXfrin(Xfrin):
check_command_hook = None
def _cc_setup(self):
- self._tsig_key = None
self._module_cc = MockCC()
- init_keyring(self._module_cc)
- pass
-
- def _get_db_file(self):
- pass
def _cc_check_command(self):
self._shutdown_event.set()
if MockXfrin.check_command_hook:
MockXfrin.check_command_hook()
- def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
- tsig_key, request_type, check_soa=True):
+ def xfrin_start(self, zone_name, rrclass, master_addrinfo,
+ tsig_key, request_ixfr, check_soa=True):
# store some of the arguments for verification, then call this
# method in the superclass
self.xfrin_started_master_addr = master_addrinfo[2][0]
self.xfrin_started_master_port = master_addrinfo[2][1]
- self.xfrin_started_request_type = request_type
- return Xfrin.xfrin_start(self, zone_name, rrclass, None,
- master_addrinfo, tsig_key,
- request_type, check_soa)
+ self.xfrin_started_request_ixfr = request_ixfr
+ return Xfrin.xfrin_start(self, zone_name, rrclass, master_addrinfo,
+ tsig_key, request_ixfr, check_soa)
class MockXfrinConnection(XfrinConnection):
def __init__(self, sock_map, zone_name, rrclass, datasrc_client,
shutdown_event, master_addr, tsig_key=None):
super().__init__(sock_map, zone_name, rrclass, MockDataSourceClient(),
- shutdown_event, master_addr, TEST_DB_FILE)
+ shutdown_event, master_addr, begin_soa_rrset,
+ xfrin.Counters(xfrin.SPECFILE_LOCATION))
self.query_data = b''
self.reply_data = b''
self.force_time_out = False
@@ -846,7 +868,11 @@ class TestXfrinConnection(unittest.TestCase):
'''
self.conn._zone_name = zone_name
- self.conn._zone_soa = self.conn._get_zone_soa()
+ try:
+ self.conn._zone_soa = xfrin._get_zone_soa(
+ self.conn._datasrc_client, zone_name, self.conn._rrclass)
+ except XfrinException: # zone doesn't exist
+ self.conn._zone_soa = None
class TestAXFR(TestXfrinConnection):
def setUp(self):
@@ -970,7 +996,9 @@ class TestAXFR(TestXfrinConnection):
RRType.IXFR)
self._set_test_zone(Name('dup-soa.example'))
- self.conn._zone_soa = self.conn._get_zone_soa()
+ self.conn._zone_soa = xfrin._get_zone_soa(self.conn._datasrc_client,
+ self.conn._zone_name,
+ self.conn._rrclass)
self.assertRaises(XfrinException, self.conn._create_query,
RRType.IXFR)
@@ -978,7 +1006,7 @@ class TestAXFR(TestXfrinConnection):
def message_has_tsig(data):
# a simple check if the actual data contains a TSIG RR.
# At our level this simple check should suffice; other detailed
- # tests regarding the TSIG protocol are done in pydnspp.
+ # tests regarding the TSIG protocol are done in the isc.dns module.
msg = Message(Message.PARSE)
msg.from_wire(data)
return msg.get_tsig_record() is not None
@@ -1077,12 +1105,10 @@ class TestAXFR(TestXfrinConnection):
for (info, ver) in addrs:
c = MockXfrinConnection({}, TEST_ZONE_NAME, RRClass.CH, None,
threading.Event(), info)
- c.init_socket()
if ver is not None:
self.assertEqual(ver, c._get_ipver_str())
else:
self.assertRaises(ValueError, c._get_ipver_str)
- c.close()
def test_soacheck(self):
# we need to defer the creation until we know the QID, which is
@@ -2097,39 +2123,11 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
'''
self.axfr_failure_check(RRType.AXFR)
- def test_do_axfrin_nozone_sqlite3(self):
- '''AXFR test with an empty SQLite3 DB file, thus no target zone there.
-
- For now, we provide backward compatible behavior: xfrin will create
- the zone (after even setting up the entire schema) in the zone.
- Note: a future version of this test will make it fail.
-
- '''
- self.conn._db_file = self.empty_sqlite3db_obj
- self.conn._datasrc_client = DataSourceClient(
- "sqlite3",
- "{ \"database_file\": \"" + self.empty_sqlite3db_obj + "\"}")
- def create_response():
- self.conn.reply_data = self.conn.create_response_data(
- questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
- RRType.AXFR)],
- answers=[soa_rrset, self._create_ns(), soa_rrset])
- self.conn.response_generator = create_response
- self._set_test_zone(Name('example.com'))
- self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.AXFR))
- self.assertEqual(type(XfrinAXFREnd()),
- type(self.conn.get_xfrstate()))
- self.assertEqual(1234, self.get_zone_serial().get_value())
- self.assertFalse(self.record_exist(Name('dns01.example.com'),
- RRType.A))
-
class TestStatisticsXfrinConn(TestXfrinConnection):
'''Test class based on TestXfrinConnection and including paramters
and methods related to statistics tests'''
def setUp(self):
super().setUp()
- # clear all statistics counters before each test
- self.conn._counters.clear_all()
# fake datetime
self.__orig_datetime = isc.statistics.counters.datetime
self.__orig_start_timer = isc.statistics.counters._start_timer
@@ -2144,16 +2142,19 @@ class TestStatisticsXfrinConn(TestXfrinConnection):
self._const_sec = round(delta.days * 86400 + delta.seconds +
delta.microseconds * 1E-6, 6)
# List of statistics counter names and expected initial values
- self.__name_to_counter = (('axfrreqv4', 0),
- ('axfrreqv6', 0),
- ('ixfrreqv4', 0),
- ('ixfrreqv6', 0),
- ('last_axfr_duration', 0.0),
- ('last_ixfr_duration', 0.0),
- ('soaoutv4', 0),
- ('soaoutv6', 0),
- ('xfrfail', 0),
- ('xfrsuccess', 0))
+ self.__name_to_perzone_counter = (('axfrreqv4', 0),
+ ('axfrreqv6', 0),
+ ('ixfrreqv4', 0),
+ ('ixfrreqv6', 0),
+ ('last_axfr_duration', 0.0),
+ ('last_ixfr_duration', 0.0),
+ ('soaoutv4', 0),
+ ('soaoutv6', 0),
+ ('xfrfail', 0),
+ ('xfrsuccess', 0))
+ self.__name_to_counter = ('soa_in_progress',
+ 'ixfr_running',
+ 'axfr_running')
self.__zones = 'zones'
def tearDown(self):
@@ -2168,24 +2169,41 @@ class TestStatisticsXfrinConn(TestXfrinConnection):
def _check_init_statistics(self):
'''checks exception being raised if not incremented statistics
counter gotten'''
- for (name, exp) in self.__name_to_counter:
+ for (name, exp) in self.__name_to_perzone_counter:
self.assertRaises(isc.cc.data.DataNotFoundError,
self.conn._counters.get, self.__zones,
TEST_ZONE_NAME_STR, name)
+ for name in self.__name_to_counter:
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get, name)
- def _check_updated_statistics(self, overwrite):
+ def _check_updated_perzone_statistics(self, overwrite):
'''checks getting expect values after updating the pairs of
- statistics counter name and value on to the "overwrite"
+ per-zone statistics counter name and value on to the "overwrite"
dictionary'''
- name2count = dict(self.__name_to_counter)
+ name2count = dict(self.__name_to_perzone_counter)
name2count.update(overwrite)
for (name, exp) in name2count.items():
act = self.conn._counters.get(self.__zones,
+ TEST_RRCLASS_STR,
TEST_ZONE_NAME_STR,
name)
- msg = '%s is expected %s but actually %s' % (name, exp, act)
+ msg = '%s: expected %s but actually got %s' % (name, exp, act)
self.assertEqual(exp, act, msg=msg)
+ def _check_updated_statistics(self, expects):
+ '''checks counters in expects are incremented. also checks other
+ counters which are not in expects are not incremented.'''
+ for name in self.__name_to_counter:
+ if name in expects:
+ exp = expects[name]
+ act = self.conn._counters.get(name)
+ msg = '%s: expected %s but actually got %s' % (name, exp, act)
+ self.assertEqual(exp, act, msg=msg)
+ else:
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get, name)
+
class TestStatisticsXfrinAXFRv4(TestStatisticsXfrinConn):
'''Xfrin AXFR tests for IPv4 to check statistics counters'''
def test_soaout(self):
@@ -2194,7 +2212,7 @@ class TestStatisticsXfrinAXFRv4(TestStatisticsXfrinConn):
self.conn.response_generator = self._create_soa_response_data
self._check_init_statistics()
self.assertEqual(self.conn._check_soa_serial(), XFRIN_OK)
- self._check_updated_statistics({'soaout' + self._ipver: 1})
+ self._check_updated_perzone_statistics({'soaout' + self._ipver: 1})
def test_axfrreq_xfrsuccess_last_axfr_duration(self):
'''tests that axfrreqv4 or axfrreqv6 and xfrsuccess counters
@@ -2202,9 +2220,10 @@ class TestStatisticsXfrinAXFRv4(TestStatisticsXfrinConn):
self.conn.response_generator = self._create_normal_response_data
self._check_init_statistics()
self.assertEqual(self.conn.do_xfrin(False), XFRIN_OK)
- self._check_updated_statistics({'axfrreq' + self._ipver: 1,
- 'xfrsuccess': 1,
- 'last_axfr_duration': self._const_sec})
+ self._check_updated_perzone_statistics({'axfrreq' + self._ipver: 1,
+ 'xfrsuccess': 1,
+ 'last_axfr_duration':
+ self._const_sec})
def test_axfrreq_xfrsuccess_last_axfr_duration2(self):
'''tests that axfrreqv4 or axfrreqv6 and xfrsuccess counters
@@ -2215,10 +2234,10 @@ class TestStatisticsXfrinAXFRv4(TestStatisticsXfrinConn):
self.conn._handle_xfrin_responses = exception_raiser
self._check_init_statistics()
self.assertEqual(self.conn.do_xfrin(False), XFRIN_OK)
- self._check_updated_statistics({'axfrreq' + self._ipver: 1,
- 'xfrsuccess': 1,
- 'last_axfr_duration':
- self._const_sec})
+ self._check_updated_perzone_statistics({'axfrreq' + self._ipver: 1,
+ 'xfrsuccess': 1,
+ 'last_axfr_duration':
+ self._const_sec})
def test_axfrreq_xfrfail(self):
'''tests that axfrreqv4 or axfrreqv6 and xfrfail counters are
@@ -2234,8 +2253,47 @@ class TestStatisticsXfrinAXFRv4(TestStatisticsXfrinConn):
self.conn._handle_xfrin_responses = exception_raiser
self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
count += 1
- self._check_updated_statistics({'axfrreq' + self._ipver: count,
- 'xfrfail': count})
+ self._check_updated_perzone_statistics({'axfrreq' + self._ipver:
+ count,
+ 'xfrfail': count})
+
+ def test_soa_in_progress1(self):
+ '''tests that an soa_in_progress counter is incremented and decremented
+ when an soa query succeeds'''
+ self.conn.response_generator = self._create_soa_response_data
+ self._check_init_statistics()
+ self.assertEqual(self.conn._check_soa_serial(), XFRIN_OK)
+ self._check_updated_statistics({'soa_in_progress': 0})
+
+ def test_soa_in_progress2(self):
+ '''tests that an soa_in_progress counter is incremented and decremented
+ even if socket.error is raised from XfrinConnection._send_query()'''
+ def exception_raiser(x):
+ raise socket.error()
+ self.conn._send_query = exception_raiser
+ self._check_init_statistics()
+ self.assertRaises(socket.error, self.conn._check_soa_serial)
+ self._check_updated_statistics({'soa_in_progress': 0})
+
+ def test_axfr_running1(self):
+ '''tests that axfr_running counter is incremented and decremented'''
+ self.conn.response_generator = self._create_normal_response_data
+ self._check_init_statistics()
+ self.assertEqual(self.conn.do_xfrin(False), XFRIN_OK)
+ self._check_updated_statistics({'axfr_running': 0})
+
+ def test_axfr_running2(self):
+ '''tests that axfr_running counter is incremented and decremented even
+ if some failure exceptions are expected to be raised inside do_xfrin():
+ XfrinZoneError, XfrinProtocolError, XfrinException, and Exception'''
+ self._check_init_statistics()
+ for ex in [XfrinZoneError, XfrinProtocolError, XfrinException,
+ Exception]:
+ def exception_raiser():
+ raise ex()
+ self.conn._handle_xfrin_responses = exception_raiser
+ self.assertEqual(self.conn.do_xfrin(False), XFRIN_FAIL)
+ self._check_updated_statistics({'axfr_running': 0})
class TestStatisticsXfrinIXFRv4(TestStatisticsXfrinConn):
'''Xfrin IXFR tests for IPv4 to check statistics counters'''
@@ -2250,10 +2308,10 @@ class TestStatisticsXfrinIXFRv4(TestStatisticsXfrinConn):
self.conn.response_generator = create_ixfr_response
self._check_init_statistics()
self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.IXFR))
- self._check_updated_statistics({'ixfrreq' + self._ipver: 1,
- 'xfrsuccess': 1,
- 'last_ixfr_duration':
- self._const_sec})
+ self._check_updated_perzone_statistics({'ixfrreq' + self._ipver: 1,
+ 'xfrsuccess': 1,
+ 'last_ixfr_duration':
+ self._const_sec})
def test_ixfrreq_xfrsuccess_last_ixfr_duration2(self):
'''tests that ixfrreqv4 or ixfrreqv6 and xfrsuccess counters
@@ -2264,10 +2322,10 @@ class TestStatisticsXfrinIXFRv4(TestStatisticsXfrinConn):
self.conn._handle_xfrin_responses = exception_raiser
self._check_init_statistics()
self.assertEqual(self.conn.do_xfrin(False, RRType.IXFR), XFRIN_OK)
- self._check_updated_statistics({'ixfrreq' + self._ipver: 1,
- 'xfrsuccess': 1,
- 'last_ixfr_duration':
- self._const_sec})
+ self._check_updated_perzone_statistics({'ixfrreq' + self._ipver: 1,
+ 'xfrsuccess': 1,
+ 'last_ixfr_duration':
+ self._const_sec})
def test_ixfrreq_xfrfail(self):
'''tests that ixfrreqv4 or ixfrreqv6 and xfrfail counters are
@@ -2283,8 +2341,36 @@ class TestStatisticsXfrinIXFRv4(TestStatisticsXfrinConn):
self.conn._handle_xfrin_responses = exception_raiser
self.assertEqual(self.conn.do_xfrin(False, RRType.IXFR), XFRIN_FAIL)
count += 1
- self._check_updated_statistics({'ixfrreq' + self._ipver: count,
- 'xfrfail': count})
+ self._check_updated_perzone_statistics({'ixfrreq' + self._ipver:
+ count,
+ 'xfrfail': count})
+
+ def test_ixfr_running1(self):
+ '''tests that ixfr_running counter is incremented and decremented'''
+ def create_ixfr_response():
+ self.conn.reply_data = self.conn.create_response_data(
+ questions=[Question(TEST_ZONE_NAME, TEST_RRCLASS,
+ RRType.IXFR)],
+ answers=[soa_rrset, begin_soa_rrset, soa_rrset, soa_rrset])
+ self.conn.response_generator = create_ixfr_response
+ self._check_init_statistics()
+ self.assertEqual(XFRIN_OK, self.conn.do_xfrin(False, RRType.IXFR))
+ self._check_updated_statistics({'ixfr_running': 0})
+
+ def test_ixfr_running2(self):
+ '''tests that ixfr_running counter is incremented and decremented even
+ if some failure exceptions are expected to be raised inside do_xfrin():
+ XfrinZoneError, XfrinProtocolError, XfrinException, and Exception'''
+ self._check_init_statistics()
+ count = 0
+ for ex in [XfrinZoneError, XfrinProtocolError, XfrinException,
+ Exception]:
+ def exception_raiser():
+ raise ex()
+ self.conn._handle_xfrin_responses = exception_raiser
+ self.assertEqual(self.conn.do_xfrin(False, RRType.IXFR), XFRIN_FAIL)
+ count += 1
+ self._check_updated_statistics({'ixfr_running': 0})
class TestStatisticsXfrinAXFRv6(TestStatisticsXfrinAXFRv4):
'''Same tests as TestStatisticsXfrinAXFRv4 for IPv6'''
@@ -2411,16 +2497,16 @@ class TestXfrinProcess(unittest.TestCase):
# Normal, successful case. We only check that things are cleaned up
# at the tearDown time.
process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
- self.master, False, None, RRType.AXFR,
- self.create_xfrinconn)
+ None, self.master, False, None,
+ ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
def test_process_xfrin_exception_on_connect(self):
# connect_to_master() will raise an exception. Things must still be
# cleaned up.
self.do_raise_on_connect = True
process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
- self.master, False, None, RRType.AXFR,
- self.create_xfrinconn)
+ None, self.master, False, None,
+ ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
def test_process_xfrin_exception_on_close(self):
# connect() will result in exception, and even the cleanup close()
@@ -2429,38 +2515,48 @@ class TestXfrinProcess(unittest.TestCase):
self.do_raise_on_connect = True
self.do_raise_on_close = True
process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
- self.master, False, None, RRType.AXFR,
- self.create_xfrinconn)
+ None, self.master, False, None,
+ ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
def test_process_xfrin_exception_on_publish(self):
# xfr succeeds but notifying the zonemgr fails with exception.
# everything must still be cleaned up.
self.do_raise_on_publish = True
process_xfrin(self, self, TEST_ZONE_NAME, TEST_RRCLASS, None, None,
- self.master, False, None, RRType.AXFR,
- self.create_xfrinconn)
+ None, self.master, False, None,
+ ZoneInfo.REQUEST_IXFR_DISABLED, self.create_xfrinconn)
class TestXfrin(unittest.TestCase):
def setUp(self):
# redirect output
self.stderr_backup = sys.stderr
sys.stderr = open(os.devnull, 'w')
+ self.__orig_DataSrcClientsMgr = xfrin.DataSrcClientsMgr
+ xfrin.DataSrcClientsMgr = MockDataSrcClientsMgr
+
self.xfr = MockXfrin()
self.args = {}
self.args['zone_name'] = TEST_ZONE_NAME_STR
self.args['class'] = TEST_RRCLASS_STR
self.args['port'] = TEST_MASTER_PORT
self.args['master'] = TEST_MASTER_IPV4_ADDRESS
- self.args['db_file'] = TEST_DB_FILE
self.args['tsig_key'] = ''
def tearDown(self):
+ xfrin.DataSrcClientsMgr = self.__orig_DataSrcClientsMgr
self.assertFalse(self.xfr._module_cc.stopped);
self.xfr.shutdown()
self.assertTrue(self.xfr._module_cc.stopped);
sys.stderr.close()
sys.stderr = self.stderr_backup
+ def test_init(self):
+ """Check some initial configuration after construction"""
+ # data source "module" should have been registrered as a necessary
+ # remote config
+ self.assertEqual([('data_sources', self.xfr._datasrc_config_handler)],
+ self.xfr._module_cc.added_remote_modules)
+
def _do_parse_zone_name_class(self):
return self.xfr._parse_zone_name_and_class(self.args)
@@ -2471,12 +2567,10 @@ class TestXfrin(unittest.TestCase):
def test_parse_cmd_params(self):
name, rrclass = self._do_parse_zone_name_class()
master_addrinfo = self._do_parse_master_port()
- db_file = self.args.get('db_file')
self.assertEqual(master_addrinfo[2][1], int(TEST_MASTER_PORT))
self.assertEqual(name, TEST_ZONE_NAME)
self.assertEqual(rrclass, TEST_RRCLASS)
self.assertEqual(master_addrinfo[2][0], TEST_MASTER_IPV4_ADDRESS)
- self.assertEqual(db_file, TEST_DB_FILE)
def test_parse_cmd_params_default_port(self):
del self.args['port']
@@ -2534,10 +2628,13 @@ class TestXfrin(unittest.TestCase):
def test_command_handler_retransfer(self):
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 0)
- self.assertEqual(self.args['master'], self.xfr.xfrin_started_master_addr)
- self.assertEqual(int(self.args['port']), self.xfr.xfrin_started_master_port)
- # By default we use AXFR (for now)
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+ self.assertEqual(self.args['master'],
+ self.xfr.xfrin_started_master_addr)
+ self.assertEqual(int(self.args['port']),
+ self.xfr.xfrin_started_master_port)
+ # retransfer always uses AXFR
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_retransfer_short_command1(self):
# try it when only specifying the zone name (of unknown zone)
@@ -2632,13 +2729,36 @@ class TestXfrin(unittest.TestCase):
self.assertEqual(self.xfr.command_handler("retransfer",
self.args)['result'][0], 1)
- def test_command_handler_retransfer_nomodule(self):
- dns_module = sys.modules['pydnspp'] # this must exist
- del sys.modules['pydnspp']
- self.assertEqual(self.xfr.command_handler("retransfer",
- self.args)['result'][0], 1)
- # sys.modules is global, so we must recover it
- sys.modules['pydnspp'] = dns_module
+ def test_command_handler_retransfer_datasrc_error(self):
+ # Failure cases due to various errors at the data source (config/data)
+ # level
+
+ # No data source client list for the RR class
+ self.xfr._datasrc_clients_mgr.found_datasrc_client_list = None
+ self.assertEqual(1, self.xfr.command_handler("retransfer",
+ self.args)['result'][0])
+
+ # No data source client for the zone name
+ self.xfr._datasrc_clients_mgr.found_datasrc_client_list = \
+ self.xfr._datasrc_clients_mgr # restore the original
+ self.xfr._datasrc_clients_mgr.found_datasrc_client = None
+ self.assertEqual(1, self.xfr.command_handler("retransfer",
+ self.args)['result'][0])
+
+ # list.find() raises an exception
+ self.xfr._datasrc_clients_mgr.found_datasrc_client = \
+ isc.datasrc.Error('test exception')
+ self.assertEqual(1, self.xfr.command_handler("retransfer",
+ self.args)['result'][0])
+
+ # datasrc.find() raises an exception
+ class RaisingkDataSourceClient(MockDataSourceClient):
+ def find_zone(self, zone_name):
+ raise isc.datasrc.Error('test exception')
+ self.xfr._datasrc_clients_mgr.found_datasrc_client = \
+ RaisingkDataSourceClient()
+ self.assertEqual(1, self.xfr.command_handler("retransfer",
+ self.args)['result'][0])
def test_command_handler_refresh(self):
# at this level, refresh is no different than retransfer.
@@ -2650,8 +2770,9 @@ class TestXfrin(unittest.TestCase):
self.xfr.xfrin_started_master_addr)
self.assertEqual(int(TEST_MASTER_PORT),
self.xfr.xfrin_started_master_port)
- # By default we use AXFR (for now)
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+ # By default we use IXFR (with AXFR fallback)
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_notify(self):
# at this level, refresh is no different than retransfer.
@@ -2734,12 +2855,19 @@ class TestXfrin(unittest.TestCase):
Name(zone_config['tsig_key']).to_text())
else:
self.assertIsNone(zone_info.tsig_key_name)
- if 'use_ixfr' in zone_config and\
- zone_config.get('use_ixfr'):
- self.assertTrue(zone_info.use_ixfr)
- else:
- # if not set, should default to False
- self.assertFalse(zone_info.use_ixfr)
+ if ('request_ixfr' in zone_config and
+ zone_config.get('request_ixfr')):
+ cfg_val = zone_config.get('request_ixfr')
+ val = zone_info.request_ixfr
+ if cfg_val == 'yes':
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST, val)
+ elif cfg_val == 'no':
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED, val)
+ elif cfg_val == 'only':
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_ONLY, val)
+ else: # check the default
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_FIRST,
+ zone_info.request_ixfr)
def test_config_handler_zones(self):
# This test passes a number of good and bad configs, and checks whether
@@ -2751,7 +2879,7 @@ class TestXfrin(unittest.TestCase):
{ 'name': 'test.example.',
'master_addr': '192.0.2.1',
'master_port': 53,
- 'use_ixfr': False
+ 'request_ixfr': 'yes'
}
]}
self.assertEqual(self.xfr.config_handler(config1)['result'][0], 0)
@@ -2763,12 +2891,24 @@ class TestXfrin(unittest.TestCase):
'master_addr': '192.0.2.2',
'master_port': 53,
'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g==",
- 'use_ixfr': True
+ 'request_ixfr': 'no'
}
]}
self.assertEqual(self.xfr.config_handler(config2)['result'][0], 0)
self._check_zones_config(config2)
+ config3 = {'transfers_in': 4,
+ 'zones': [
+ { 'name': 'test.example.',
+ 'master_addr': '192.0.2.2',
+ 'master_port': 53,
+ 'tsig_key': "example.com:SFuWd/q99SzF8Yzd1QbB9g==",
+ 'request_ixfr': 'only'
+ }
+ ]}
+ self.assertEqual(self.xfr.config_handler(config3)['result'][0], 0)
+ self._check_zones_config(config3)
+
# test that configuring the zone multiple times fails
zones = { 'transfers_in': 5,
'zones': [
@@ -2783,7 +2923,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': 'test.example.',
@@ -2793,7 +2933,7 @@ class TestXfrin(unittest.TestCase):
}
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'master_addr': '192.0.2.4',
@@ -2802,7 +2942,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': 'bad..zone.',
@@ -2812,7 +2952,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': '',
@@ -2822,7 +2962,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': 'test.example',
@@ -2832,7 +2972,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': 'test.example',
@@ -2842,7 +2982,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
zones = { 'zones': [
{ 'name': 'test.example',
@@ -2854,7 +2994,7 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
# let's also add a zone that is correct too, and make sure
# that the new config is not partially taken
@@ -2871,209 +3011,120 @@ class TestXfrin(unittest.TestCase):
]}
self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
# since this has failed, we should still have the previous config
- self._check_zones_config(config2)
+ self._check_zones_config(config3)
+
+ # invalid request_ixfr value
+ zones = { 'zones': [
+ { 'name': 'test.example',
+ 'master_addr': '192.0.2.7',
+ 'request_ixfr': 'bad value'
+ }
+ ]}
+ self.assertEqual(self.xfr.config_handler(zones)['result'][0], 1)
+ # since this has failed, we should still have the previous config
+ self._check_zones_config(config3)
def test_config_handler_zones_default(self):
# Checking it some default config values apply. Using a separate
# test case for a fresh xfr object.
config = { 'zones': [
{ 'name': 'test.example.',
- 'master_addr': '192.0.2.1',
- 'master_port': 53,
+ 'master_addr': '192.0.2.1',
+ 'master_port': 53,
}
]}
self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
self._check_zones_config(config)
- def common_ixfr_setup(self, xfr_mode, use_ixfr, tsig_key_str = None):
+ def test_config_handler_use_ixfr(self):
+ # use_ixfr was deprecated and explicitly rejected for now.
+ config = { 'zones': [
+ { 'name': 'test.example.',
+ 'master_addr': '192.0.2.1',
+ 'master_port': 53,
+ 'use_ixfr': True
+ }
+ ]}
+ self.assertEqual(self.xfr.config_handler(config)['result'][0], 1)
+
+ def common_ixfr_setup(self, xfr_mode, request_ixfr, tsig_key_str=None):
# This helper method explicitly sets up a zone configuration with
- # use_ixfr, and invokes either retransfer or refresh.
+ # request_ixfr, and invokes either retransfer or refresh.
# Shared by some of the following test cases.
config = {'zones': [
{'name': 'example.com.',
'master_addr': '192.0.2.1',
'tsig_key': tsig_key_str,
- 'use_ixfr': use_ixfr}]}
+ 'request_ixfr': request_ixfr}]}
self.assertEqual(self.xfr.config_handler(config)['result'][0], 0)
self.assertEqual(self.xfr.command_handler(xfr_mode,
self.args)['result'][0], 0)
def test_command_handler_retransfer_ixfr_enabled(self):
- # If IXFR is explicitly enabled in config, IXFR will be used
- self.common_ixfr_setup('retransfer', True)
- self.assertEqual(RRType.IXFR, self.xfr.xfrin_started_request_type)
+ # retransfer always uses AXFR (disabling IXFR), regardless of
+ # request_ixfr value
+ self.common_ixfr_setup('retransfer', 'yes')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_refresh_ixfr_enabled(self):
- # Same for refresh
- self.common_ixfr_setup('refresh', True)
- self.assertEqual(RRType.IXFR, self.xfr.xfrin_started_request_type)
+ # for refresh, it honors zone configuration if defined (the default
+ # case is covered in test_command_handler_refresh
+ self.common_ixfr_setup('refresh', 'no')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_retransfer_with_tsig(self):
- self.common_ixfr_setup('retransfer', False, 'example.com.key')
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+ self.common_ixfr_setup('retransfer', 'no', 'example.com.key')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_retransfer_with_tsig_bad_key(self):
# bad keys should not reach xfrin, but should they somehow,
# they are ignored (and result in 'key not found' + error log).
self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
- 'retransfer', False, 'bad.key')
+ 'retransfer', 'no', 'bad.key')
def test_command_handler_retransfer_with_tsig_unknown_key(self):
self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
- 'retransfer', False, 'no.such.key')
+ 'retransfer', 'no', 'no.such.key')
def test_command_handler_refresh_with_tsig(self):
- self.common_ixfr_setup('refresh', False, 'example.com.key')
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+ self.common_ixfr_setup('refresh', 'no', 'example.com.key')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_refresh_with_tsig_bad_key(self):
# bad keys should not reach xfrin, but should they somehow,
# they are ignored (and result in 'key not found' + error log).
self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
- 'refresh', False, 'bad.key')
+ 'refresh', 'no', 'bad.key')
def test_command_handler_refresh_with_tsig_unknown_key(self):
self.assertRaises(XfrinZoneInfoException, self.common_ixfr_setup,
- 'refresh', False, 'no.such.key')
+ 'refresh', 'no', 'no.such.key')
def test_command_handler_retransfer_ixfr_disabled(self):
# Similar to the previous case, but explicitly disabled. AXFR should
# be used.
- self.common_ixfr_setup('retransfer', False)
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
+ self.common_ixfr_setup('retransfer', 'no')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
def test_command_handler_refresh_ixfr_disabled(self):
# Same for refresh
- self.common_ixfr_setup('refresh', False)
- self.assertEqual(RRType.AXFR, self.xfr.xfrin_started_request_type)
-
-class TestXfrinMemoryZones(unittest.TestCase):
- def setUp(self):
- self.xfr = MockXfrin()
- # Configuration snippet containing 2 memory datasources,
- # one for IN and one for CH. Both contain a zone 'example.com'
- # the IN ds also contains a zone example2.com, and a zone example3.com,
- # which is of file type 'text' (and hence, should be ignored)
- self.config = { 'datasources': [
- { 'type': 'memory',
- 'class': 'IN',
- 'zones': [
- { 'origin': 'example.com',
- 'filetype': 'sqlite3' },
- { 'origin': 'EXAMPLE2.com.',
- 'filetype': 'sqlite3' },
- { 'origin': 'example3.com',
- 'filetype': 'text' }
- ]
- },
- { 'type': 'memory',
- 'class': 'ch',
- 'zones': [
- { 'origin': 'example.com',
- 'filetype': 'sqlite3' }
- ]
- }
- ] }
-
- def test_updates(self):
- self.assertFalse(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- # add them all
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
- # Remove the CH data source from the self.config snippet, and update
- del self.config['datasources'][1]
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- # Remove example2.com from the datasource, and update
- del self.config['datasources'][0]['zones'][1]
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- # If 'datasources' is not in the self.config update list (i.e. its
- # self.config has not changed), no difference should be found
- self.xfr._set_memory_zones({}, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- # If datasources list becomes empty, everything should be removed
- self.config['datasources'][0]['zones'] = []
- self.xfr._set_memory_zones(self.config, None)
- self.assertFalse(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- def test_normalization(self):
- self.xfr._set_memory_zones(self.config, None)
- # make sure it is case insensitive, root-dot-insensitive,
- # and supports CLASSXXX notation
- self.assertTrue(self.xfr._is_memory_zone("EXAMPLE.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "in"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com.", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "CLASS3"))
-
- def test_bad_name(self):
- # First set it to some config
- self.xfr._set_memory_zones(self.config, None)
-
- # Error checking; bad owner name should result in no changes
- self.config['datasources'][1]['zones'][0]['origin'] = ".."
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
- def test_bad_class(self):
- # First set it to some config
- self.xfr._set_memory_zones(self.config, None)
-
- # Error checking; bad owner name should result in no changes
- self.config['datasources'][1]['class'] = "Foo"
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
-
- def test_no_filetype(self):
- # omitting the filetype should leave that zone out, but not
- # the rest
- del self.config['datasources'][1]['zones'][0]['filetype']
- self.xfr._set_memory_zones(self.config, None)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example.com", "CH"))
-
- def test_class_filetype(self):
- # omitting the class should have it default to what is in the
- # specfile for Auth.
- AuthConfigData = isc.config.config_data.ConfigData(
- isc.config.module_spec_from_file(xfrin.AUTH_SPECFILE_LOCATION))
- del self.config['datasources'][0]['class']
- self.xfr._set_memory_zones(self.config, AuthConfigData)
- self.assertTrue(self.xfr._is_memory_zone("example.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example2.com", "IN"))
- self.assertFalse(self.xfr._is_memory_zone("example3.com", "IN"))
- self.assertTrue(self.xfr._is_memory_zone("example.com", "CH"))
+ self.common_ixfr_setup('refresh', 'no')
+ self.assertEqual(ZoneInfo.REQUEST_IXFR_DISABLED,
+ self.xfr.xfrin_started_request_ixfr)
+
+ def test_datasrc_config_handler(self):
+ """Check datasrc config handler works expectedly."""
+ # This is a simple wrapper of DataSrcClientsMgr.reconfigure(), so
+ # we just check it's called as expected, and the only possible
+ # exception doesn't cause disruption.
+ self.xfr._datasrc_config_handler(True, False)
+ self.assertEqual([(True, False)],
+ self.xfr._datasrc_clients_mgr.reconfigure_param)
def raise_interrupt():
raise KeyboardInterrupt()
@@ -3157,6 +3208,13 @@ class TestXfrinProcess(unittest.TestCase):
self.__published = []
# How many connections were created.
self.__created_connections = 0
+ # prepare for possible replacement
+ self.__orig_get_zone_soa = xfrin._get_zone_soa
+ xfrin._get_zone_soa = lambda x, y, z: begin_soa_rdata
+
+ def tearDown(self):
+ # restore original value
+ xfrin._get_zone_soa = self.__orig_get_zone_soa
def __get_connection(self, *args):
"""
@@ -3212,7 +3270,8 @@ class TestXfrinProcess(unittest.TestCase):
"""
pass
- def __do_test(self, rets, transfers, request_type):
+ def __do_test(self, rets, transfers, request_ixfr,
+ zone_soa=begin_soa_rrset):
"""
Do the actual test. The request type, prepared sucesses/failures
and expected sequence of transfers is passed to specify what test
@@ -3221,8 +3280,11 @@ class TestXfrinProcess(unittest.TestCase):
self.__rets = rets
published = rets[-1]
xfrin.process_xfrin(self, XfrinRecorder(), Name("example.org."),
- RRClass.IN, None, None, None, True, None,
- request_type, self.__get_connection)
+ RRClass.IN, None, zone_soa, None,
+ TEST_MASTER_IPV4_ADDRINFO, True, None,
+ request_ixfr,
+ xfrin.Counters(xfrin.SPECFILE_LOCATION),
+ self.__get_connection)
self.assertEqual([], self.__rets)
self.assertEqual(transfers, self.__transfers)
# Create a connection for each attempt
@@ -3233,7 +3295,7 @@ class TestXfrinProcess(unittest.TestCase):
"""
Everything OK the first time, over IXFR.
"""
- self.__do_test([XFRIN_OK], [RRType.IXFR], RRType.IXFR)
+ self.__do_test([XFRIN_OK], [RRType.IXFR], ZoneInfo.REQUEST_IXFR_FIRST)
# Check there was loadzone command
self.assertTrue(self._send_cc_session.send_called)
self.assertTrue(self._send_cc_session.send_called_correctly)
@@ -3244,23 +3306,27 @@ class TestXfrinProcess(unittest.TestCase):
"""
Everything OK the first time, over AXFR.
"""
- self.__do_test([XFRIN_OK], [RRType.AXFR], RRType.AXFR)
+ self.__do_test([XFRIN_OK], [RRType.AXFR],
+ ZoneInfo.REQUEST_IXFR_DISABLED)
def test_axfr_fail(self):
"""
The transfer failed over AXFR. Should not be retried (we don't expect
- to fail on AXFR, but succeed on IXFR and we didn't use IXFR in the first
- place for some reason.
+ to fail on AXFR, but succeed on IXFR and we didn't use IXFR in the
+ first place for some reason.
+
"""
- self.__do_test([XFRIN_FAIL], [RRType.AXFR], RRType.AXFR)
+ self.__do_test([XFRIN_FAIL], [RRType.AXFR],
+ ZoneInfo.REQUEST_IXFR_DISABLED)
def test_ixfr_fallback(self):
"""
- The transfer fails over IXFR, but suceeds over AXFR. It should fall back
- to it and say everything is OK.
+ The transfer fails over IXFR, but suceeds over AXFR. It should fall
+ back to it and say everything is OK.
+
"""
self.__do_test([XFRIN_FAIL, XFRIN_OK], [RRType.IXFR, RRType.AXFR],
- RRType.IXFR)
+ ZoneInfo.REQUEST_IXFR_FIRST)
def test_ixfr_fail(self):
"""
@@ -3268,18 +3334,52 @@ class TestXfrinProcess(unittest.TestCase):
(only once) and should try both before giving up.
"""
self.__do_test([XFRIN_FAIL, XFRIN_FAIL],
- [RRType.IXFR, RRType.AXFR], RRType.IXFR)
+ [RRType.IXFR, RRType.AXFR], ZoneInfo.REQUEST_IXFR_FIRST)
+
+ def test_ixfr_only(self):
+ """
+ The transfer fails and IXFR_ONLY is specified. It shouldn't fall
+ back to AXFR and should report failure.
+ """
+ self.__do_test([XFRIN_FAIL], [RRType.IXFR], ZoneInfo.REQUEST_IXFR_ONLY)
def test_send_loadzone(self):
"""
Check the loadzone command is sent after successful transfer.
"""
- self.__do_test([XFRIN_OK], [RRType.IXFR], RRType.IXFR)
+ self.__do_test([XFRIN_OK], [RRType.IXFR],
+ ZoneInfo.REQUEST_IXFR_FIRST)
self.assertTrue(self._send_cc_session.send_called)
self.assertTrue(self._send_cc_session.send_called_correctly)
self.assertTrue(self._send_cc_session.recv_called)
self.assertTrue(self._send_cc_session.recv_called_correctly)
+ def test_initial_request_type(self):
+ """Check initial xfr reuqest type (AXFR or IXFR).
+
+ Varying the policy of use of IXFR and availability of current
+ zone SOA. We are only interested in the initial request type,
+ so won't check the xfr results.
+
+ """
+ for soa in [begin_soa_rdata, None]:
+ for request_ixfr in [ZoneInfo.REQUEST_IXFR_FIRST,
+ ZoneInfo.REQUEST_IXFR_ONLY,
+ ZoneInfo.REQUEST_IXFR_DISABLED]:
+ # Clear all counters
+ self.__transfers = []
+ self.__published = []
+ self.__created_connections = 0
+
+ # Determine the expected type
+ expected_type = RRType.IXFR
+ if (soa is None or
+ request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED):
+ expected_type = RRType.AXFR
+
+ # perform the test
+ self.__do_test([XFRIN_OK], [expected_type], request_ixfr, soa)
+
class TestFormatting(unittest.TestCase):
# If the formatting functions are moved to a more general library
# (ticket #1379), these tests should be moved with them.
@@ -3395,6 +3495,125 @@ class TestXfrinTransferStats(unittest.TestCase):
zbps = self.stats.get_bytes_per_second()
self.assertEqual(0, zbps)
+class TestXfrinConnectionSocketCounter(unittest.TestCase):
+
+ @property
+ def _master_addrinfo(self):
+ return TEST_MASTER_IPV4_ADDRINFO
+ @property
+ def _ipver(self):
+ return 'ipv4'
+
+ def setUp(self):
+ self.conn = XfrinConnection(
+ None, TEST_ZONE_NAME, None, MockDataSourceClient(), None,
+ self._master_addrinfo, None,
+ xfrin.Counters(xfrin.SPECFILE_LOCATION))
+ self.expception = socket.error
+
+ def raise_expception(self, *args):
+ raise self.expception
+
+ def test_open(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'open')
+ self.conn.create_socket(self._master_addrinfo[0],
+ self._master_addrinfo[1])
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'open'))
+
+ def test_openfail(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'openfail')
+ orig_create_socket = xfrin.asyncore.dispatcher.create_socket
+ xfrin.asyncore.dispatcher.create_socket = self.raise_expception
+ try:
+ self.assertRaises(self.expception, self.conn.create_socket,
+ self._master_addrinfo[0],
+ self._master_addrinfo[1])
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'openfail'))
+ finally:
+ xfrin.asyncore.dispatcher.create_socket = orig_create_socket
+
+ def test_close(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'close')
+ orig_socket_close = xfrin.asyncore.dispatcher.close
+ xfrin.asyncore.dispatcher.close = lambda x: None
+ try:
+ self.conn.close()
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'close'))
+ finally:
+ xfrin.asyncore.dispatcher.close = orig_socket_close
+
+ def test_conn(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'conn')
+ orig_socket_connect = xfrin.asyncore.dispatcher.connect
+ xfrin.asyncore.dispatcher.connect = lambda *a: None
+ try:
+ self.conn.connect(self._master_addrinfo[2])
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'conn'))
+ finally:
+ xfrin.asyncore.dispatcher.connect = orig_socket_connect
+
+ def test_connfail(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'connfail')
+ orig_socket_connect = xfrin.asyncore.dispatcher.connect
+ xfrin.asyncore.dispatcher.connect = self.raise_expception
+ try:
+ self.assertRaises(self.expception, self.conn.connect,
+ self._master_addrinfo[2])
+ self.assertFalse(self.conn.connect_to_master())
+ self.assertEqual(2, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'connfail'))
+ finally:
+ xfrin.asyncore.dispatcher.connect = orig_socket_connect
+
+ def test_senderr(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'senderr')
+ orig_socket_send = xfrin.asyncore.dispatcher.send
+ xfrin.asyncore.dispatcher.send = self.raise_expception
+ try:
+ self.assertRaises(self.expception, self.conn.send, None)
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'senderr'))
+ finally:
+ xfrin.asyncore.dispatcher.send = orig_socket_send
+
+ def test_recverr(self):
+ self.assertRaises(isc.cc.data.DataNotFoundError,
+ self.conn._counters.get,
+ 'socket', self._ipver, 'tcp', 'recverr')
+ orig_socket_recv = xfrin.asyncore.dispatcher.recv
+ xfrin.asyncore.dispatcher.recv = self.raise_expception
+ try:
+ self.assertRaises(self.expception, self.conn.recv, None)
+ self.assertEqual(1, self.conn._counters.get(
+ 'socket', self._ipver, 'tcp', 'recverr'))
+ finally:
+ xfrin.asyncore.dispatcher.recv = orig_socket_recv
+
+class TestXfrinConnectionSocketCounterV6(TestXfrinConnectionSocketCounter):
+
+ @property
+ def _master_addrinfo(self):
+ return TEST_MASTER_IPV6_ADDRINFO
+ @property
+ def _ipver(self):
+ return 'ipv6'
+
if __name__== "__main__":
try:
isc.log.resetUnitTestRootLogger()
diff --git a/src/bin/xfrin/xfrin.py.in b/src/bin/xfrin/xfrin.py.in
index 2066404..a894d55 100755
--- a/src/bin/xfrin/xfrin.py.in
+++ b/src/bin/xfrin/xfrin.py.in
@@ -28,14 +28,17 @@ import time
from functools import reduce
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
-from isc.statistics import Counters
+from isc.statistics.dns import Counters
from isc.notify import notify_out
import isc.util.process
+import isc.util.traceback_handler
+from isc.util.address_formatter import AddressFormatter
from isc.datasrc import DataSourceClient, ZoneFinder
import isc.net.parse
from isc.xfrin.diff import Diff
from isc.server_common.auth_command import auth_loadzone_command
from isc.server_common.tsig_keyring import init_keyring, get_keyring
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr, ConfigError
from isc.log_messages.xfrin_messages import *
from isc.dns import *
@@ -55,13 +58,9 @@ isc.util.process.rename()
SPECFILE_PATH = "@datadir@/@PACKAGE@"\
.replace("${datarootdir}", "@datarootdir@")\
.replace("${prefix}", "@prefix@")
-AUTH_SPECFILE_PATH = SPECFILE_PATH
if "B10_FROM_SOURCE" in os.environ:
SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/xfrin"
-if "B10_FROM_BUILD" in os.environ:
- AUTH_SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/auth"
SPECFILE_LOCATION = SPECFILE_PATH + "/xfrin.spec"
-AUTH_SPECFILE_LOCATION = AUTH_SPECFILE_PATH + "/auth.spec"
AUTH_MODULE_NAME = 'Auth'
XFROUT_MODULE_NAME = 'Xfrout'
@@ -565,18 +564,26 @@ class XfrinConnection(asyncore.dispatcher):
def __init__(self,
sock_map, zone_name, rrclass, datasrc_client,
- shutdown_event, master_addrinfo, db_file, tsig_key=None,
- idle_timeout=60):
- '''Constructor of the XfirnConnection class.
+ shutdown_event, master_addrinfo, zone_soa, counters,
+ tsig_key=None, idle_timeout=60):
+ """Constructor of the XfirnConnection class.
+
+ Parameters:
+ sock_map: empty dict, used with asyncore.
+ zone_name (dns.Name): Zone name.
+ rrclass (dns.RRClass): Zone RR class.
+ datasrc_client (DataSourceClient): the data source client object
+ used for the XFR session.
+ shutdown_event (threading.Event): used for synchronization with
+ parent thread.
+ master_addrinfo (tuple: (sock family, sock type, sockaddr)):
+ address and port of the master server.
+ zone_soa (RRset or None): SOA RRset of zone's current SOA or None
+ if it's not available.
+ counters (Counters): used for statistics counters
+ idle_timeout (int): max idle time for read data from socket.
- db_file: SQLite3 DB file. Unforutnately we still need this for
- temporary workaround in _get_zone_soa(). This should be
- removed when we eliminate the need for the workaround.
- idle_timeout: max idle time for read data from socket.
- datasrc_client: the data source client object used for the XFR session.
- This will eventually replace db_file completely.
-
- '''
+ """
asyncore.dispatcher.__init__(self, map=sock_map)
@@ -595,9 +602,8 @@ class XfrinConnection(asyncore.dispatcher):
self._rrclass = rrclass
# Data source handler
- self._db_file = db_file
self._datasrc_client = datasrc_client
- self._zone_soa = self._get_zone_soa()
+ self._zone_soa = zone_soa
self._sock_map = sock_map
self._soa_rr_count = 0
@@ -605,6 +611,11 @@ class XfrinConnection(asyncore.dispatcher):
self._shutdown_event = shutdown_event
self._master_addrinfo = master_addrinfo
self._tsig_key = tsig_key
+ # self.tsig_key_name is used for outputting an error massage in
+ # connect_to_master().
+ self.tsig_key_name = None
+ if tsig_key:
+ self.tsig_key_name = self._tsig_key.get_key_name()
self._tsig_ctx = None
# tsig_ctx_creator is introduced to allow tests to use a mock class for
# easier tests (in normal case we always use the default)
@@ -613,7 +624,75 @@ class XfrinConnection(asyncore.dispatcher):
# keep a record of this specific transfer to log on success
# (time, rr/s, etc)
self._transfer_stats = XfrinTransferStats()
- self._counters = Counters(SPECFILE_LOCATION)
+ self._counters = counters
+
+ def create_socket(self, family, type):
+ """create_socket() overridden from the super class for
+ statistics counter open and openfail"""
+ try:
+ ret = super().create_socket(family, type)
+ # count open
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'open')
+ return ret
+ except:
+ # count openfail
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'openfail')
+ raise
+
+ def close(self):
+ """close() overridden from the super class for
+ statistics counter close"""
+ ret = super().close()
+ # count close
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'close')
+ return ret
+
+ def connect(self, address):
+ """connect() overridden from the super class for
+ statistics counter conn and connfail"""
+ try:
+ ret = super().connect(address)
+ # count conn
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'conn')
+ return ret
+ except:
+ # count connfail
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'connfail')
+ raise
+
+ def send(self, data):
+ """send() overridden from the super class for
+ statistics counter senderr"""
+ try:
+ return super().send(data)
+ except:
+ # count senderr
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'senderr')
+ raise
+
+ def recv(self, buffer_size):
+ """recv() overridden from the super class for
+ statistics counter senderr"""
+ try:
+ return super().recv(buffer_size)
+ except:
+ # count recverr
+ self._counters.inc('socket',
+ 'ip' + self._get_ipver_str(),
+ 'tcp', 'recverr')
+ raise
def init_socket(self):
'''Initialize the underlyig socket.
@@ -626,54 +705,6 @@ class XfrinConnection(asyncore.dispatcher):
self.create_socket(self._master_addrinfo[0], self._master_addrinfo[1])
self.socket.setblocking(1)
- def _get_zone_soa(self):
- '''Retrieve the current SOA RR of the zone to be transferred.
-
- It will be used for various purposes in subsequent xfr protocol
- processing. It is validly possible that the zone is currently
- empty and therefore doesn't have an SOA, so this method doesn't
- consider it an error and returns None in such a case. It may or
- may not result in failure in the actual processing depending on
- how the SOA is used.
-
- When the zone has an SOA RR, this method makes sure that it's
- valid, i.e., it has exactly one RDATA; if it is not the case
- this method returns None.
-
- If the underlying data source doesn't even know the zone, this method
- tries to provide backward compatible behavior where xfrin is
- responsible for creating zone in the corresponding DB table.
- For a longer term we should deprecate this behavior by introducing
- more generic zone management framework, but at the moment we try
- to not surprise existing users. (Note also that the part of
- providing the compatible behavior uses the old data source API.
- We'll deprecate this API in a near future, too).
-
- '''
- # get the zone finder. this must be SUCCESS (not even
- # PARTIALMATCH) because we are specifying the zone origin name.
- result, finder = self._datasrc_client.find_zone(self._zone_name)
- if result != DataSourceClient.SUCCESS:
- # The data source doesn't know the zone. For now, we provide
- # backward compatibility and creates a new one ourselves.
- isc.datasrc.sqlite3_ds.load(self._db_file,
- self._zone_name.to_text(),
- lambda : [])
- logger.warn(XFRIN_ZONE_CREATED, self.zone_str())
- # try again
- result, finder = self._datasrc_client.find_zone(self._zone_name)
- if result != DataSourceClient.SUCCESS:
- return None
- result, soa_rrset, _ = finder.find(self._zone_name, RRType.SOA)
- if result != ZoneFinder.SUCCESS:
- logger.info(XFRIN_ZONE_NO_SOA, self.zone_str())
- return None
- if soa_rrset.get_rdata_count() != 1:
- logger.warn(XFRIN_ZONE_MULTIPLE_SOA, self.zone_str(),
- soa_rrset.get_rdata_count())
- return None
- return soa_rrset
-
def __set_xfrstate(self, new_state):
self.__state = new_state
@@ -696,7 +727,8 @@ class XfrinConnection(asyncore.dispatcher):
self.connect(self._master_addrinfo[2])
return True
except socket.error as e:
- logger.error(XFRIN_CONNECT_MASTER, self._master_addrinfo[2],
+ logger.error(XFRIN_CONNECT_MASTER, self.tsig_key_name,
+ self._master_addrinfo[2],
str(e))
return False
@@ -746,8 +778,9 @@ class XfrinConnection(asyncore.dispatcher):
msg = self._create_query(query_type)
render = MessageRenderer()
- # XXX Currently, python wrapper doesn't accept 'None' parameter in this case,
- # we should remove the if statement and use a universal interface later.
+ # XXX Currently, python wrapper doesn't accept 'None' parameter in this
+ # case, we should remove the if statement and use a universal
+ # interface later.
if self._tsig_key is not None:
self._tsig_ctx = self._tsig_ctx_creator(self._tsig_key)
msg.to_wire(render, self._tsig_ctx)
@@ -810,14 +843,15 @@ class XfrinConnection(asyncore.dispatcher):
'''
Used as error callback below.
'''
- logger.error(XFRIN_ZONE_INVALID, self._zone_name, self._rrclass,
- reason)
+ logger.error(XFRIN_ZONE_INVALID, self._zone_name,
+ self._rrclass, reason)
def __validate_warning(self, reason):
'''
Used as warning callback below.
'''
- logger.warn(XFRIN_ZONE_WARN, self._zone_name, self._rrclass, reason)
+ logger.warn(XFRIN_ZONE_WARN, self._zone_name,
+ self._rrclass, reason)
def finish_transfer(self):
"""
@@ -900,9 +934,9 @@ class XfrinConnection(asyncore.dispatcher):
It raises a ValueError exception on other address families.
"""
- if self.socket.family == socket.AF_INET:
+ if self._master_addrinfo[0] == socket.AF_INET:
return 'v4'
- elif self.socket.family == socket.AF_INET6:
+ elif self._master_addrinfo[0] == socket.AF_INET6:
return 'v6'
raise ValueError("Invalid address family. "
"This is supported only for IP sockets")
@@ -916,36 +950,44 @@ class XfrinConnection(asyncore.dispatcher):
'''
- self._send_query(RRType.SOA)
- # count soaoutv4 or soaoutv6 requests
- self._counters.inc('zones', self._zone_name.to_text(),
- 'soaout' + self._get_ipver_str())
- data_len = self._get_request_response(2)
- msg_len = socket.htons(struct.unpack('H', data_len)[0])
- soa_response = self._get_request_response(msg_len)
- msg = Message(Message.PARSE)
- msg.from_wire(soa_response, Message.PRESERVE_ORDER)
-
- # Validate/parse the rest of the response, and extract the SOA
- # from the answer section
- soa = self.__parse_soa_response(msg, soa_response)
-
- # Compare the two serials. If ours is 'new', abort with ZoneUptodate.
- primary_serial = get_soa_serial(soa.get_rdata()[0])
- if self._request_serial is not None and \
- self._request_serial >= primary_serial:
- if self._request_serial != primary_serial:
- logger.info(XFRIN_ZONE_SERIAL_AHEAD, primary_serial,
- self.zone_str(),
- format_addrinfo(self._master_addrinfo),
- self._request_serial)
- raise XfrinZoneUptodate
-
- return XFRIN_OK
+ # increment SOA query in progress
+ self._counters.inc('soa_in_progress')
+ try:
+ self._send_query(RRType.SOA)
+ # count soaoutv4 or soaoutv6 requests
+ self._counters.inc('zones', self._rrclass.to_text(),
+ self._zone_name.to_text(), 'soaout' +
+ self._get_ipver_str())
+ data_len = self._get_request_response(2)
+ msg_len = socket.htons(struct.unpack('H', data_len)[0])
+ soa_response = self._get_request_response(msg_len)
+ msg = Message(Message.PARSE)
+ msg.from_wire(soa_response, Message.PRESERVE_ORDER)
+
+ # Validate/parse the rest of the response, and extract the SOA
+ # from the answer section
+ soa = self.__parse_soa_response(msg, soa_response)
+
+ # Compare the two serials. If ours is 'new', abort with ZoneUptodate.
+ primary_serial = get_soa_serial(soa.get_rdata()[0])
+ if self._request_serial is not None and \
+ self._request_serial >= primary_serial:
+ if self._request_serial != primary_serial:
+ logger.info(XFRIN_ZONE_SERIAL_AHEAD, primary_serial,
+ self.zone_str(),
+ format_addrinfo(self._master_addrinfo),
+ self._request_serial)
+ raise XfrinZoneUptodate
+
+ return XFRIN_OK
+ finally:
+ # decrement SOA query in progress
+ self._counters.dec('soa_in_progress')
def do_xfrin(self, check_soa, request_type=RRType.AXFR):
'''Do an xfr session by sending xfr request and parsing responses.'''
+ xfer_started = False # Don't set True until xfer is started
try:
ret = XFRIN_OK
self._request_type = request_type
@@ -957,16 +999,24 @@ class XfrinConnection(asyncore.dispatcher):
if not self.connect_to_master():
raise XfrinException('Unable to reconnect to master')
+ xfer_started = True
+ # increment xfer running
+ self._counters.inc(req_str.lower() + '_running')
# start statistics timer
# Note: If the timer for the zone is already started but
# not yet stopped due to some error, the last start time
# is overwritten at this point.
- self._counters.start_timer('zones', self._zone_name.to_text(),
- 'last_' + req_str.lower() + '_duration')
+ self._counters.start_timer('zones',
+ self._rrclass.to_text(),
+ self._zone_name.to_text(),
+ 'last_' + req_str.lower() +
+ '_duration')
logger.info(XFRIN_XFR_TRANSFER_STARTED, req_str, self.zone_str())
# An AXFR or IXFR is being requested.
- self._counters.inc('zones', self._zone_name.to_text(),
- req_str.lower() + 'req' + self._get_ipver_str())
+ self._counters.inc('zones', self._rrclass.to_text(),
+ self._zone_name.to_text(),
+ req_str.lower() + 'req' +
+ self._get_ipver_str())
self._send_query(self._request_type)
self.__state = XfrinInitialSOA()
self._handle_xfrin_responses()
@@ -1002,17 +1052,18 @@ class XfrinConnection(asyncore.dispatcher):
# The log message doesn't contain the exception text, since there's
# only one place where the exception is thrown now and it'd be the
# same generic message every time.
- logger.error(XFRIN_INVALID_ZONE_DATA, self.zone_str(),
+ logger.error(XFRIN_INVALID_ZONE_DATA,
+ self.zone_str(),
format_addrinfo(self._master_addrinfo))
ret = XFRIN_FAIL
except XfrinProtocolError as e:
- logger.info(XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION, req_str,
- self.zone_str(),
+ logger.info(XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION,
+ req_str, self.zone_str(),
format_addrinfo(self._master_addrinfo), str(e))
ret = XFRIN_FAIL
except XfrinException as e:
- logger.error(XFRIN_XFR_TRANSFER_FAILURE, req_str,
- self.zone_str(),
+ logger.error(XFRIN_XFR_TRANSFER_FAILURE,
+ req_str, self.zone_str(),
format_addrinfo(self._master_addrinfo), str(e))
ret = XFRIN_FAIL
except Exception as e:
@@ -1031,14 +1082,19 @@ class XfrinConnection(asyncore.dispatcher):
# A xfrsuccess or xfrfail counter is incremented depending on
# the result.
result = {XFRIN_OK: 'xfrsuccess', XFRIN_FAIL: 'xfrfail'}[ret]
- self._counters.inc('zones', self._zone_name.to_text(), result)
+ self._counters.inc('zones', self._rrclass.to_text(),
+ self._zone_name.to_text(), result)
# The started statistics timer is finally stopped only in
# a successful case.
if ret == XFRIN_OK:
self._counters.stop_timer('zones',
+ self._rrclass.to_text(),
self._zone_name.to_text(),
'last_' + req_str.lower() +
'_duration')
+ # decrement xfer running only if started
+ if xfer_started:
+ self._counters.dec(req_str.lower() + '_running')
# Make sure any remaining transaction in the diff is closed
# (if not yet - possible in case of xfr-level exception) as soon
# as possible
@@ -1114,55 +1170,77 @@ class XfrinConnection(asyncore.dispatcher):
return False
-def __process_xfrin(server, zone_name, rrclass, db_file,
+def __get_initial_xfr_type(zone_soa, request_ixfr, zname, zclass, master_addr):
+ """Determine the initial xfr request type.
+
+ This is a dedicated subroutine of __process_xfrin.
+ """
+ if zone_soa is None:
+ # This is a kind of special case, so we log it at info level.
+ logger.info(XFRIN_INITIAL_AXFR, format_zone_str(zname, zclass),
+ AddressFormatter(master_addr))
+ return RRType.AXFR
+ if request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED:
+ logger.debug(DBG_XFRIN_TRACE, XFRIN_INITIAL_IXFR_DISABLED,
+ format_zone_str(zname, zclass),
+ AddressFormatter(master_addr))
+ return RRType.AXFR
+
+ assert(request_ixfr == ZoneInfo.REQUEST_IXFR_FIRST or
+ request_ixfr == ZoneInfo.REQUEST_IXFR_ONLY)
+ logger.debug(DBG_XFRIN_TRACE, XFRIN_INITIAL_IXFR,
+ format_zone_str(zname, zclass),
+ AddressFormatter(master_addr))
+ return RRType.IXFR
+
+def __process_xfrin(server, zone_name, rrclass, datasrc_client, zone_soa,
shutdown_event, master_addrinfo, check_soa, tsig_key,
- request_type, conn_class):
+ request_ixfr, counters, conn_class):
conn = None
exception = None
ret = XFRIN_FAIL
try:
- # Create a data source client used in this XFR session. Right now we
- # still assume an sqlite3-based data source, and use both the old and new
- # data source APIs. We also need to use a mock client for tests.
- # For a temporary workaround to deal with these situations, we skip the
- # creation when the given file is none (the test case). Eventually
- # this code will be much cleaner.
- datasrc_client = None
- if db_file is not None:
- # temporary hardcoded sqlite initialization. Once we decide on
- # the config specification, we need to update this (TODO)
- # this may depend on #1207, or any follow-up ticket created for #1207
- datasrc_type = "sqlite3"
- datasrc_config = "{ \"database_file\": \"" + db_file + "\"}"
- datasrc_client = DataSourceClient(datasrc_type, datasrc_config)
-
- # Create a TCP connection for the XFR session and perform the operation.
+ # Determine the initialreuqest type: AXFR or IXFR.
+ request_type = __get_initial_xfr_type(zone_soa, request_ixfr,
+ zone_name, rrclass,
+ master_addrinfo[2])
+
+ # Create a TCP connection for the XFR session and perform the
+ # operation.
sock_map = {}
- # In case we were asked to do IXFR and that one fails, we try again with
- # AXFR. But only if we could actually connect to the server.
+ # In case we were asked to do IXFR and that one fails, we try again
+ # with AXFR. But only if we could actually connect to the server.
#
- # So we start with retry as True, which is set to false on each attempt.
- # In the case of connected but failed IXFR, we set it to true once again.
+ # So we start with retry as True, which is set to false on each
+ # attempt. In the case of connected but failed IXFR, we set it to true
+ # once again.
retry = True
while retry:
retry = False
conn = conn_class(sock_map, zone_name, rrclass, datasrc_client,
- shutdown_event, master_addrinfo, db_file,
- tsig_key)
+ shutdown_event, master_addrinfo, zone_soa,
+ counters, tsig_key)
conn.init_socket()
ret = XFRIN_FAIL
if conn.connect_to_master():
ret = conn.do_xfrin(check_soa, request_type)
if ret == XFRIN_FAIL and request_type == RRType.IXFR:
- # IXFR failed for some reason. It might mean the server can't
- # handle it, or we don't have the zone or we are out of sync or
- # whatever else. So we retry with with AXFR, as it may succeed
- # in many such cases.
- retry = True
- request_type = RRType.AXFR
- logger.warn(XFRIN_XFR_TRANSFER_FALLBACK, conn.zone_str())
- conn.close()
- conn = None
+ # IXFR failed for some reason. It might mean the server
+ # can't handle it, or we don't have the zone or we are out
+ # of sync or whatever else. So we retry with with AXFR, as
+ # it may succeed in many such cases; if "IXFR only" is
+ # specified in request_ixfr, however, we suppress the
+ # fallback.
+ if request_ixfr == ZoneInfo.REQUEST_IXFR_ONLY:
+ logger.warn(XFRIN_XFR_TRANSFER_FALLBACK_DISABLED,
+ tsig_key, conn.zone_str())
+ else:
+ retry = True
+ request_type = RRType.AXFR
+ logger.warn(XFRIN_XFR_TRANSFER_FALLBACK,
+ tsig_key, conn.zone_str())
+ conn.close()
+ conn = None
except Exception as ex:
# If exception happens, just remember it here so that we can re-raise
@@ -1186,9 +1264,9 @@ def __process_xfrin(server, zone_name, rrclass, db_file,
if exception is not None:
raise exception
-def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
- shutdown_event, master_addrinfo, check_soa, tsig_key,
- request_type, conn_class=XfrinConnection):
+def process_xfrin(server, xfrin_recorder, zone_name, rrclass, datasrc_client,
+ zone_soa, shutdown_event, master_addrinfo, check_soa,
+ tsig_key, request_ixfr, counters, conn_class=XfrinConnection):
# Even if it should be rare, the main process of xfrin session can
# raise an exception. In order to make sure the lock in xfrin_recorder
# is released in any cases, we delegate the main part to the helper
@@ -1196,16 +1274,19 @@ def process_xfrin(server, xfrin_recorder, zone_name, rrclass, db_file,
xfrin_recorder.increment(zone_name)
exception = None
try:
- __process_xfrin(server, zone_name, rrclass, db_file,
+ __process_xfrin(server, zone_name, rrclass, datasrc_client, zone_soa,
shutdown_event, master_addrinfo, check_soa, tsig_key,
- request_type, conn_class)
+ request_ixfr, counters, conn_class)
except Exception as ex:
# don't log it until we complete decrement().
exception = ex
xfrin_recorder.decrement(zone_name)
if exception is not None:
- typestr = "AXFR" if request_type == RRType.AXFR else "IXFR"
+ if request_ixfr == ZoneInfo.REQUEST_IXFR_DISABLED:
+ typestr = "AXFR"
+ else:
+ typestr = "IXFR"
logger.error(XFRIN_XFR_PROCESS_FAILURE, typestr, zone_name.to_text(),
str(rrclass), str(exception))
@@ -1238,10 +1319,26 @@ class XfrinRecorder:
return ret
class ZoneInfo:
+ # Internal values corresponding to request_ixfr
+ REQUEST_IXFR_FIRST = 0 # request_ixfr=yes, use IXFR 1st then AXFR
+ REQUEST_IXFR_ONLY = 1 # request_ixfr=only, use IXFR only
+ REQUEST_IXFR_DISABLED = 2 # request_ixfr=no, AXFR-only
+
+ # Map from configuration values for request_ixfr to internal values
+ # This is a constant; don't modify.
+ REQUEST_IXFR_CFG_TO_VAL = { 'yes': REQUEST_IXFR_FIRST,
+ 'only': REQUEST_IXFR_ONLY,
+ 'no': REQUEST_IXFR_DISABLED }
+
def __init__(self, config_data, module_cc):
"""Creates a zone_info with the config data element as
specified by the 'zones' list in xfrin.spec. Module_cc is
needed to get the defaults from the specification"""
+ # Handle deprecated config parameter explicitly for the moment.
+ if config_data.get('use_ixfr') is not None:
+ raise XfrinZoneInfoException('"use_ixfr" was deprecated, ' +
+ 'use "request_ixfr"')
+
self._module_cc = module_cc
self.set_name(config_data.get('name'))
self.set_master_addr(config_data.get('master_addr'))
@@ -1249,7 +1346,17 @@ class ZoneInfo:
self.set_master_port(config_data.get('master_port'))
self.set_zone_class(config_data.get('class'))
self.set_tsig_key_name(config_data.get('tsig_key'))
- self.set_use_ixfr(config_data.get('use_ixfr'))
+ self.set_request_ixfr(config_data.get('request_ixfr'))
+
+ @property
+ def request_ixfr(self):
+ """Policy on the use of IXFR.
+
+ Possible values are REQUEST_IXFR_xxx, internally stored in
+ __request_ixfr, read-only outside of the class.
+
+ """
+ return self.__request_ixfr
def set_name(self, name_str):
"""Set the name for this zone given a name string.
@@ -1336,16 +1443,15 @@ class ZoneInfo:
else:
return key
- def set_use_ixfr(self, use_ixfr):
- """Set use_ixfr. If set to True, it will use
- IXFR for incoming transfers. If set to False, it will use AXFR.
- At this moment there is no automatic fallback"""
- # TODO: http://bind10.isc.org/ticket/1279
- if use_ixfr is None:
- self.use_ixfr = \
- self._module_cc.get_default_value("zones/use_ixfr")
- else:
- self.use_ixfr = use_ixfr
+ def set_request_ixfr(self, request_ixfr):
+ if request_ixfr is None:
+ request_ixfr = \
+ self._module_cc.get_default_value("zones/request_ixfr")
+ try:
+ self.__request_ixfr = self.REQUEST_IXFR_CFG_TO_VAL[request_ixfr]
+ except KeyError:
+ raise XfrinZoneInfoException('invalid value for request_ixfr: ' +
+ request_ixfr)
def get_master_addr_info(self):
return (self.master_addr.family, socket.SOCK_STREAM,
@@ -1365,15 +1471,22 @@ class Xfrin:
def __init__(self):
self._max_transfers_in = 10
self._zones = {}
- # This is a set of (zone/class) tuples (both as strings),
- # representing the in-memory zones maintaned by Xfrin. It
- # is used to trigger Auth/in-memory so that it reloads
- # zones when they have been transfered in
- self._memory_zones = set()
- self._cc_setup()
self.recorder = XfrinRecorder()
self._shutdown_event = threading.Event()
self._counters = Counters(SPECFILE_LOCATION)
+ # This is essentially private, but we allow tests to customize it.
+ self._datasrc_clients_mgr = DataSrcClientsMgr()
+
+ # Initial configuration
+ self._cc_setup()
+ config_data = self._module_cc.get_full_config()
+ self.config_handler(config_data)
+ # data_sources configuration should be ready with cfgmgr, so this
+ # shouldn't fail; if it ever does we simply propagate the exception
+ # to terminate the program.
+ self._module_cc.add_remote_config_by_name('data_sources',
+ self._datasrc_config_handler)
+ init_keyring(self._module_cc)
def _cc_setup(self):
'''This method is used only as part of initialization, but is
@@ -1384,14 +1497,9 @@ class Xfrin:
# listening session will block the send operation.
self._send_cc_session = isc.cc.Session()
self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler)
+ self.config_handler,
+ self.command_handler)
self._module_cc.start()
- config_data = self._module_cc.get_full_config()
- self.config_handler(config_data)
- self._module_cc.add_remote_config(AUTH_SPECFILE_LOCATION,
- self._auth_config_handler)
- init_keyring(self._module_cc)
def _cc_check_command(self):
'''This is a straightforward wrapper for cc.check_command,
@@ -1423,7 +1531,8 @@ class Xfrin:
old_max_transfers_in = self._max_transfers_in
old_zones = self._zones
- self._max_transfers_in = new_config.get("transfers_in") or self._max_transfers_in
+ self._max_transfers_in = \
+ new_config.get("transfers_in") or self._max_transfers_in
if 'zones' in new_config:
self._clear_zone_info()
@@ -1438,78 +1547,25 @@ class Xfrin:
return create_answer(0)
- def _auth_config_handler(self, new_config, config_data):
- # Config handler for changes in Auth configuration
- self._set_db_file()
- self._set_memory_zones(new_config, config_data)
-
- def _clear_memory_zones(self):
- """Clears the memory_zones set; called before processing the
- changed list of memory datasource zones that have file type
- sqlite3"""
- self._memory_zones.clear()
-
- def _is_memory_zone(self, zone_name_str, zone_class_str):
- """Returns true if the given zone/class combination is configured
- in the in-memory datasource of the Auth process with file type
- 'sqlite3'.
- Note: this method is not thread-safe. We are considering
- changing the threaded model here, but if we do not, take
- care in accessing and updating the memory zone set (or add
- locks)
- """
- # Normalize them first, if either conversion fails, return false
- # (they won't be in the set anyway)
- try:
- zone_name_str = Name(zone_name_str).to_text().lower()
- zone_class_str = RRClass(zone_class_str).to_text()
- except Exception:
- return False
- return (zone_name_str, zone_class_str) in self._memory_zones
-
- def _set_memory_zones(self, new_config, config_data):
- """Part of the _auth_config_handler function, keeps an internal set
- of zones in the datasources config subset that have 'sqlite3' as
- their file type.
- Note: this method is not thread-safe. We are considering
- changing the threaded model here, but if we do not, take
- care in accessing and updating the memory zone set (or add
- locks)
+ def _datasrc_config_handler(self, new_config, config_data):
+ """Configuration handler of the 'data_sources' module.
+
+ The actual handling is deletegated to the DataSrcClientsMgr class;
+ this method is a simple wrapper.
+
+ This is essentially private, but implemented as 'protected' so tests
+ can refer to it; other external use is prohibited.
+
"""
- # walk through the data and collect the memory zones
- # If this causes any exception, assume we were passed bad data
- # and keep the original set
- new_memory_zones = set()
try:
- if "datasources" in new_config:
- for datasource in new_config["datasources"]:
- if "class" in datasource:
- ds_class = RRClass(datasource["class"])
- else:
- # Get the default
- ds_class = RRClass(config_data.get_default_value(
- "datasources/class"))
- if datasource["type"] == "memory":
- for zone in datasource["zones"]:
- if "filetype" in zone and \
- zone["filetype"] == "sqlite3":
- zone_name = Name(zone["origin"])
- zone_name_str = zone_name.to_text().lower()
- new_memory_zones.add((zone_name_str,
- ds_class.to_text()))
- # Ok, we can use the data, update our list
- self._memory_zones = new_memory_zones
- except Exception:
- # Something is wrong with the data. If this data even reached us,
- # we cannot do more than assume the real module has logged and
- # reported an error. Keep the old set.
- return
+ self._datasrc_clients_mgr.reconfigure(new_config, config_data)
+ except isc.server_common.datasrc_clients_mgr.ConfigError as ex:
+ logger.error(XFRIN_DATASRC_CONFIG_ERROR, ex)
def shutdown(self):
''' shutdown the xfrin process. the thread which is doing xfrin should be
terminated.
'''
- self._module_cc.remove_remote_config(AUTH_SPECFILE_LOCATION)
self._module_cc.send_stopping()
self._shutdown_event.set()
main_thread = threading.currentThread()
@@ -1518,6 +1574,85 @@ class Xfrin:
continue
th.join()
+ def __validate_notify_addr(self, notify_addr, zone_str, zone_info):
+ """Validate notify source as a destination for xfr source.
+
+ This is called from __handle_xfr_command in case xfr is triggered
+ by ZoneMgr either due to incoming Notify or periodic refresh event.
+
+ """
+ if zone_info is None:
+ # TODO what to do? no info known about zone. defaults?
+ errmsg = "Got notification to retransfer unknown zone " + zone_str
+ logger.info(XFRIN_RETRANSFER_UNKNOWN_ZONE, zone_str)
+ return create_answer(1, errmsg)
+ else:
+ master_addr = zone_info.get_master_addr_info()
+ if (notify_addr[0] != master_addr[0] or
+ notify_addr[2] != master_addr[2]):
+ notify_addr_str = format_addrinfo(notify_addr)
+ master_addr_str = format_addrinfo(master_addr)
+ errmsg = "Got notification for " + zone_str\
+ + "from unknown address: " + notify_addr_str;
+ logger.info(XFRIN_NOTIFY_UNKNOWN_MASTER, zone_str,
+ notify_addr_str, master_addr_str)
+ return create_answer(1, errmsg)
+
+ # Notified address is okay
+ return None
+
+ def __get_running_request_ixfr(self, arg_request_ixfr, zone_info):
+ """Determine the request_ixfr policy for a specific transfer.
+
+ This is a dedicated subroutine of __handle_xfr_command.
+
+ """
+ # If explicitly specified, use it.
+ if arg_request_ixfr is not None:
+ return arg_request_ixfr
+ # Otherwise, if zone info is known, use its value.
+ if zone_info is not None:
+ return zone_info.request_ixfr
+ # Otherwise, use the default value for ZoneInfo
+ request_ixfr_def = \
+ self._module_cc.get_default_value("zones/request_ixfr")
+ return ZoneInfo.REQUEST_IXFR_CFG_TO_VAL[request_ixfr_def]
+
+ def __handle_xfr_command(self, args, check_soa, addr_validator,
+ request_ixfr):
+ """Common subroutine for handling transfer commands.
+
+ This helper method unifies both cases of transfer command from
+ ZoneMgr or from a user. Depending on who invokes the transfer,
+ details of validation and parameter selection slightly vary.
+ These conditions are passed through parameters and handled in the
+ unified code of this method accordingly.
+
+ If this is from the ZoneMgr due to incoming notify, zone transfer
+ should start from the notify's source address as long as it's
+ configured as a master address, according to RFC1996. The current
+ implementation conforms to it in a limited way: we can only set one
+ master address. Once we add the ability to have multiple master
+ addresses, we should check if it matches one of them, and then use it.
+
+ In case of transfer command from the user, if the command specifies
+ the master address, use that one; otherwise try to use a configured
+ master address for the zone.
+
+ """
+ (zone_name, rrclass) = self._parse_zone_name_and_class(args)
+ master_addr = self._parse_master_and_port(args, zone_name, rrclass)
+ zone_info = self._get_zone_info(zone_name, rrclass)
+ tsig_key = None if zone_info is None else zone_info.get_tsig_key()
+ zone_str = format_zone_str(zone_name, rrclass) # for logging
+ answer = addr_validator(master_addr, zone_str, zone_info)
+ if answer is not None:
+ return answer
+ request_ixfr = self.__get_running_request_ixfr(request_ixfr, zone_info)
+ ret = self.xfrin_start(zone_name, rrclass, master_addr, tsig_key,
+ request_ixfr, check_soa)
+ return create_answer(ret[0], ret[1])
+
def command_handler(self, command, args):
logger.debug(DBG_XFRIN_TRACE, XFRIN_RECEIVED_COMMAND, command)
answer = create_answer(0)
@@ -1525,69 +1660,24 @@ class Xfrin:
if command == 'shutdown':
self._shutdown_event.set()
elif command == 'notify' or command == REFRESH_FROM_ZONEMGR:
- # Xfrin receives the refresh/notify command from zone manager.
- # notify command maybe has the parameters which
- # specify the notifyfrom address and port, according the RFC1996, zone
- # transfer should starts first from the notifyfrom, but now, let 'TODO' it.
- # (using the value now, while we can only set one master address, would be
- # a security hole. Once we add the ability to have multiple master addresses,
- # we should check if it matches one of them, and then use it.)
- (zone_name, rrclass) = self._parse_zone_name_and_class(args)
- zone_str = format_zone_str(zone_name, rrclass)
- zone_info = self._get_zone_info(zone_name, rrclass)
- notify_addr = self._parse_master_and_port(args, zone_name,
- rrclass)
- if zone_info is None:
- # TODO what to do? no info known about zone. defaults?
- errmsg = "Got notification to retransfer unknown zone " + zone_str
- logger.info(XFRIN_RETRANSFER_UNKNOWN_ZONE, zone_str)
- answer = create_answer(1, errmsg)
- else:
- request_type = RRType.AXFR
- if zone_info.use_ixfr:
- request_type = RRType.IXFR
- master_addr = zone_info.get_master_addr_info()
- if notify_addr[0] == master_addr[0] and\
- notify_addr[2] == master_addr[2]:
- ret = self.xfrin_start(zone_name,
- rrclass,
- self._get_db_file(),
- master_addr,
- zone_info.get_tsig_key(), request_type,
- True)
- answer = create_answer(ret[0], ret[1])
- else:
- notify_addr_str = format_addrinfo(notify_addr)
- master_addr_str = format_addrinfo(master_addr)
- errmsg = "Got notification for " + zone_str\
- + "from unknown address: " + notify_addr_str;
- logger.info(XFRIN_NOTIFY_UNKNOWN_MASTER, zone_str,
- notify_addr_str, master_addr_str)
- answer = create_answer(1, errmsg)
-
- elif command == 'retransfer' or command == 'refresh':
- # Xfrin receives the retransfer/refresh from cmdctl(sent by bindctl).
- # If the command has specified master address, do transfer from the
- # master address, or else do transfer from the configured masters.
- (zone_name, rrclass) = self._parse_zone_name_and_class(args)
- master_addr = self._parse_master_and_port(args, zone_name,
- rrclass)
- zone_info = self._get_zone_info(zone_name, rrclass)
- tsig_key = None
- request_type = RRType.AXFR
- if zone_info:
- tsig_key = zone_info.get_tsig_key()
- if zone_info.use_ixfr:
- request_type = RRType.IXFR
- db_file = args.get('db_file') or self._get_db_file()
- ret = self.xfrin_start(zone_name,
- rrclass,
- db_file,
- master_addr,
- tsig_key, request_type,
- (False if command == 'retransfer' else True))
- answer = create_answer(ret[0], ret[1])
-
+ # refresh/notify command from zone manager.
+ # The address has to be validated and always perform SOA check.
+ addr_validator = \
+ lambda x, y, z: self.__validate_notify_addr(x, y, z)
+ answer = self.__handle_xfr_command(args, True, addr_validator,
+ None)
+ elif command == 'retransfer':
+ # retransfer from cmdctl (sent by bindctl).
+ # No need for address validation, skip SOA check, and always
+ # use AXFR.
+ answer = self.__handle_xfr_command(
+ args, False, lambda x, y, z: None,
+ ZoneInfo.REQUEST_IXFR_DISABLED)
+ elif command == 'refresh':
+ # retransfer from cmdctl (sent by bindctl). similar to
+ # retransfer, but do SOA check, and honor request_ixfr config.
+ answer = self.__handle_xfr_command(
+ args, True, lambda x, y, z: None, None)
# return statistics data to the stats daemon
elif command == "getstats":
# The log level is here set to debug in order to avoid
@@ -1608,7 +1698,8 @@ class Xfrin:
if zone_name_str is None:
raise XfrinException('zone name should be provided')
- return (_check_zone_name(zone_name_str), _check_zone_class(args.get('zone_class')))
+ return (_check_zone_name(zone_name_str),
+ _check_zone_class(args.get('zone_class')))
def _parse_master_and_port(self, args, zone_name, zone_class):
"""
@@ -1650,21 +1741,6 @@ class Xfrin:
return (addr.family, socket.SOCK_STREAM, (str(addr), port))
- def _get_db_file(self):
- return self._db_file
-
- def _set_db_file(self):
- db_file, is_default =\
- self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
- if is_default and "B10_FROM_BUILD" in os.environ:
- # override the local database setting if it is default and we
- # are running from the source tree
- # This should be hidden inside the data source library and/or
- # done as a configuration, and this special case should be gone).
- db_file = os.environ["B10_FROM_BUILD"] + os.sep +\
- "bind10_zones.sqlite3"
- self._db_file = db_file
-
def publish_xfrin_news(self, zone_name, zone_class, xfr_result):
'''Send command to xfrout/zone manager module.
If xfrin has finished successfully for one zone, tell the good
@@ -1703,7 +1779,8 @@ class Xfrin:
except isc.cc.session.SessionTimeout:
pass # for now we just ignore the failure
except socket.error as err:
- logger.error(XFRIN_MSGQ_SEND_ERROR, XFROUT_MODULE_NAME, ZONE_MANAGER_MODULE_NAME)
+ logger.error(XFRIN_MSGQ_SEND_ERROR, self.tsig_key_name,
+ XFROUT_MODULE_NAME, ZONE_MANAGER_MODULE_NAME)
else:
msg = create_command(notify_out.ZONE_XFRIN_FAILED, param)
@@ -1717,18 +1794,16 @@ class Xfrin:
except isc.cc.session.SessionTimeout:
pass # for now we just ignore the failure
except socket.error as err:
- logger.error(XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER, ZONE_MANAGER_MODULE_NAME)
+ logger.error(XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER, self.tsig_key_name,
+ ZONE_MANAGER_MODULE_NAME)
def startup(self):
logger.debug(DBG_PROCESS, XFRIN_STARTED)
while not self._shutdown_event.is_set():
self._cc_check_command()
- def xfrin_start(self, zone_name, rrclass, db_file, master_addrinfo,
- tsig_key, request_type, check_soa=True):
- if "pydnspp" not in sys.modules:
- return (1, "xfrin failed, can't load dns message python library: 'pydnspp'")
-
+ def xfrin_start(self, zone_name, rrclass, master_addrinfo, tsig_key,
+ request_ixfr, check_soa=True):
# check max_transfer_in, else return quota error
if self.recorder.count() >= self._max_transfers_in:
return (1, 'xfrin quota error')
@@ -1736,19 +1811,83 @@ class Xfrin:
if self.recorder.xfrin_in_progress(zone_name):
return (1, 'zone xfrin is in progress')
- xfrin_thread = threading.Thread(target = process_xfrin,
- args = (self,
- self.recorder,
- zone_name,
- rrclass,
- db_file,
- self._shutdown_event,
- master_addrinfo, check_soa,
- tsig_key, request_type))
+ # Identify the data source to which the zone content is transferred,
+ # and get the current zone SOA from the data source (if available).
+ # Note that we do this before spawning the xfrin session thread.
+ # find() on the client list and use of ZoneFinder (in _get_zone_soa())
+ # should be completed within the same single thread.
+ datasrc_client = None
+ clist = self._datasrc_clients_mgr.get_client_list(rrclass)
+ if clist is None:
+ return (1, 'no data source is configured for class %s' % rrclass)
+
+ try:
+ datasrc_client = clist.find(zone_name, True, False)[0]
+ if datasrc_client is None: # can happen, so log it separately.
+ logger.error(XFRIN_DATASRC_UNKNOWN,
+ format_zone_str(zone_name, rrclass))
+ return (1, 'data source to transfer %s to is unknown' %
+ format_zone_str(zone_name, rrclass))
+ zone_soa = _get_zone_soa(datasrc_client, zone_name, rrclass)
+ except isc.datasrc.Error as ex:
+ # rare case error. re-raise as XfrinException so it'll be logged
+ # in command_handler().
+ raise XfrinException('unexpected failure in datasrc module: ' +
+ str(ex))
+
+ xfrin_thread = threading.Thread(target=process_xfrin,
+ args=(self, self.recorder,
+ zone_name, rrclass,
+ datasrc_client, zone_soa,
+ self._shutdown_event,
+ master_addrinfo, check_soa,
+ tsig_key, request_ixfr,
+ self._counters))
xfrin_thread.start()
return (0, 'zone xfrin is started')
+def _get_zone_soa(datasrc_client, zone_name, zone_class):
+ """Retrieve the current SOA RR of the zone to be transferred.
+
+ This function is essentially private to the module, but will also
+ be called (or tweaked) from tests; no one else should use this
+ function directly.
+
+ The specified zone is expected to exist in the data source referenced
+ by the given datasrc_client at the point of the call to this function.
+ If this is not met XfrinException exception will be raised.
+
+ It will be used for various purposes in subsequent xfr protocol
+ processing. It is validly possible that the zone is currently
+ empty and therefore doesn't have an SOA, so this method doesn't
+ consider it an error and returns None in such a case. It may or
+ may not result in failure in the actual processing depending on
+ how the SOA is used.
+
+ When the zone has an SOA RR, this method makes sure that it's
+ valid, i.e., it has exactly one RDATA; if it is not the case
+ this method returns None.
+
+ """
+ # get the zone finder. this must be SUCCESS (not even
+ # PARTIALMATCH) because we are specifying the zone origin name.
+ result, finder = datasrc_client.find_zone(zone_name)
+ if result != DataSourceClient.SUCCESS:
+ # The data source doesn't know the zone. In the context of this
+ # function is called, this shouldn't happen.
+ raise XfrinException("unexpected result: zone %s doesn't exist" %
+ format_zone_str(zone_name, zone_class))
+ result, soa_rrset, _ = finder.find(zone_name, RRType.SOA)
+ if result != ZoneFinder.SUCCESS:
+ logger.info(XFRIN_ZONE_NO_SOA, format_zone_str(zone_name, zone_class))
+ return None
+ if soa_rrset.get_rdata_count() != 1:
+ logger.warn(XFRIN_ZONE_MULTIPLE_SOA,
+ format_zone_str(zone_name, zone_class),
+ soa_rrset.get_rdata_count())
+ return None
+ return soa_rrset
xfrind = None
@@ -1797,4 +1936,4 @@ def main(xfrin_class, use_signal=True):
logger.info(XFRIN_EXITING)
if __name__ == '__main__':
- main(Xfrin)
+ isc.util.traceback_handler.traceback_handler(lambda: main(Xfrin))
diff --git a/src/bin/xfrin/xfrin.spec b/src/bin/xfrin/xfrin.spec
index dc993f7..a662f75 100644
--- a/src/bin/xfrin/xfrin.spec
+++ b/src/bin/xfrin/xfrin.spec
@@ -48,6 +48,11 @@
"item_type": "boolean",
"item_optional": false,
"item_default": false
+ },
+ { "item_name": "request_ixfr",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": "yes"
}
]
}
@@ -56,7 +61,36 @@
"commands": [
{
"command_name": "retransfer",
- "command_description": "retransfer a single zone without checking zone serial number",
+ "command_description": "retransfer a single zone without checking zone serial number, always using AXFR",
+ "command_args": [ {
+ "item_name": "zone_name",
+ "item_type": "string",
+ "item_optional": false,
+ "item_default": ""
+ },
+ {
+ "item_name": "zone_class",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": "IN"
+ },
+ {
+ "item_name": "master",
+ "item_type": "string",
+ "item_optional": true,
+ "item_default": ""
+ },
+ {
+ "item_name": "port",
+ "item_type": "integer",
+ "item_optional": true,
+ "item_default": 53
+ }
+ ]
+ },
+ {
+ "command_name": "refresh",
+ "command_description": "transfer a single zone with checking zone serial number and honoring the request_ixfr policy",
"command_args": [ {
"item_name": "zone_name",
"item_type": "string",
@@ -101,111 +135,366 @@
"item_type": "named_set",
"item_optional": false,
"item_default": {
- "_SERVER_" : {
- "soaoutv4": 0,
- "soaoutv6": 0,
- "axfrreqv4": 0,
- "axfrreqv6": 0,
- "ixfrreqv4": 0,
- "ixfrreqv6": 0,
- "xfrsuccess": 0,
- "xfrfail": 0,
- "last_ixfr_duration": 0.0,
- "last_axfr_duration": 0.0
+ "IN" : {
+ "_SERVER_" : {
+ "soaoutv4": 0,
+ "soaoutv6": 0,
+ "axfrreqv4": 0,
+ "axfrreqv6": 0,
+ "ixfrreqv4": 0,
+ "ixfrreqv6": 0,
+ "xfrsuccess": 0,
+ "xfrfail": 0,
+ "last_ixfr_duration": 0.0,
+ "last_axfr_duration": 0.0
+ }
}
},
"item_title": "Zone names",
"item_description": "A directory name of per-zone statistics",
"named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
+ "item_name": "classname",
+ "item_type": "named_set",
"item_optional": false,
"item_default": {},
- "item_title": "Zone name",
- "item_description": "An actual zone name or special zone name _SERVER_ representing the entire server. Zone classes (e.g. IN, CH, and HS) are mixed and counted so far. But these will be distinguished in future release.",
- "map_item_spec": [
- {
- "item_name": "soaoutv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "SOAOutv4",
- "item_description": "Number of IPv4 SOA queries sent from Xfrin"
- },
- {
- "item_name": "soaoutv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "SOAOutv6",
- "item_description": "Number of IPv6 SOA queries sent from Xfrin"
- },
- {
- "item_name": "axfrreqv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "AXFRReqv4",
- "item_description": "Number of IPv4 AXFR requests sent from Xfrin"
- },
- {
- "item_name": "axfrreqv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "AXFRReqv6",
- "item_description": "Number of IPv6 AXFR requests sent from Xfrin"
- },
- {
- "item_name": "ixfrreqv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IXFRReqv4",
- "item_description": "Number of IPv4 IXFR requests sent from Xfrin"
- },
- {
- "item_name": "ixfrreqv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IXFRReqv6",
- "item_description": "Number of IPv6 IXFR requests sent from Xfrin"
- },
- {
- "item_name": "xfrsuccess",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XfrSuccess",
- "item_description": "Number of zone transfer requests succeeded. These include the case where the zone turns out to be the latest as a result of an initial SOA query (and there is actually no AXFR or IXFR transaction)."
- },
- {
- "item_name": "xfrfail",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XfrFail",
- "item_description": "Number of zone transfer requests failed"
+ "item_title": "RR class name",
+ "item_description": "An actual RR class name of the zone, e.g. IN, CH, and HS",
+ "named_set_item_spec": {
+ "item_name": "zonename",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "Zone name",
+ "item_description": "An actual zone name or special zone name _SERVER_ representing the entire server",
+ "map_item_spec": [
+ {
+ "item_name": "soaoutv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "SOAOutv4",
+ "item_description": "Number of IPv4 SOA queries sent from Xfrin"
+ },
+ {
+ "item_name": "soaoutv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "SOAOutv6",
+ "item_description": "Number of IPv6 SOA queries sent from Xfrin"
+ },
+ {
+ "item_name": "axfrreqv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "AXFRReqv4",
+ "item_description": "Number of IPv4 AXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "axfrreqv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "AXFRReqv6",
+ "item_description": "Number of IPv6 AXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "ixfrreqv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IXFRReqv4",
+ "item_description": "Number of IPv4 IXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "ixfrreqv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IXFRReqv6",
+ "item_description": "Number of IPv6 IXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "xfrsuccess",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XfrSuccess",
+ "item_description": "Number of zone transfer requests succeeded. These include the case where the zone turns out to be the latest as a result of an initial SOA query (and there is actually no AXFR or IXFR transaction)."
+ },
+ {
+ "item_name": "xfrfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XfrFail",
+ "item_description": "Number of zone transfer requests failed"
+ },
+ {
+ "item_name": "last_axfr_duration",
+ "item_type": "real",
+ "item_optional": false,
+ "item_default": 0.0,
+ "item_title": "Last AXFR duration",
+ "item_description": "Duration in seconds of the last successful AXFR. 0.0 means no successful AXFR done or means a successful AXFR done in less than a microsecond. If an AXFR is aborted due to some failure, this duration won't be updated."
+ },
+ {
+ "item_name": "last_ixfr_duration",
+ "item_type": "real",
+ "item_optional": false,
+ "item_default": 0.0,
+ "item_title": "Last IXFR duration",
+ "item_description": "Duration in seconds of the last successful IXFR. 0.0 means no successful IXFR done or means a successful IXFR done in less than a microsecond. If an IXFR is aborted due to some failure, this duration won't be updated."
+ }
+ ]
+ }
+ }
+ },
+ {
+ "item_name": "ixfr_running",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IXFRs running",
+ "item_description": "Number of IXFRs in progress"
+ },
+ {
+ "item_name": "axfr_running",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "AXFRs running",
+ "item_description": "Number of AXFRs in progress"
+ },
+ {
+ "item_name": "soa_in_progress",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "SOA queries",
+ "item_description": "Number of SOA queries in progress"
+ },
+ {
+ "item_name": "socket",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {
+ "ipv4": {
+ "tcp": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ }
+ },
+ "ipv6": {
+ "tcp": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ }
+ }
+ },
+ "item_title": "Socket",
+ "item_description": "A directory name of socket statistics",
+ "map_item_spec": [
+ {
+ "item_name": "ipv4",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {
+ "tcp": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ }
},
- {
- "item_name": "last_axfr_duration",
- "item_type": "real",
- "item_optional": false,
- "item_default": 0.0,
- "item_title": "Last AXFR duration",
- "item_description": "Duration in seconds of the last successful AXFR. 0.0 means no successful AXFR done or means a successful AXFR done in less than a microsecond. If an AXFR is aborted due to some failure, this duration won't be updated."
+ "item_title": "IPv4",
+ "item_description": "A directory name of IPv4",
+ "map_item_spec": [
+ {
+ "item_name": "tcp",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ },
+ "item_title": "TCP",
+ "item_description": "A directory name of TCP statistics",
+ "map_item_spec": [
+ {
+ "item_name": "open",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Open",
+ "item_description": "IPv4 TCP sockets opened successfully"
+ },
+ {
+ "item_name": "openfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Open failures",
+ "item_description": "IPv4 TCP sockets open failures"
+ },
+ {
+ "item_name": "close",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Close",
+ "item_description": "IPv4 TCP sockets closed"
+ },
+ {
+ "item_name": "connfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Connect failures",
+ "item_description": "IPv4 TCP sockets connection failures"
+ },
+ {
+ "item_name": "conn",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Connect",
+ "item_description": "IPv4 TCP connections established successfully"
+ },
+ {
+ "item_name": "senderr",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Send errors",
+ "item_description": "IPv4 TCP sockets send errors"
+ },
+ {
+ "item_name": "recverr",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Receive errors",
+ "item_description": "IPv4 TCP sockets receive errors"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "item_name": "ipv6",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {
+ "tcp": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ }
},
- {
- "item_name": "last_ixfr_duration",
- "item_type": "real",
- "item_optional": false,
- "item_default": 0.0,
- "item_title": "Last IXFR duration",
- "item_description": "Duration in seconds of the last successful IXFR. 0.0 means no successful IXFR done or means a successful IXFR done in less than a microsecond. If an IXFR is aborted due to some failure, this duration won't be updated."
- }
- ]
- }
+ "item_title": "IPv6",
+ "item_description": "A directory name of IPv6",
+ "map_item_spec": [
+ {
+ "item_name": "tcp",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {
+ "open": 0,
+ "openfail": 0,
+ "close": 0,
+ "connfail": 0,
+ "conn": 0,
+ "senderr": 0,
+ "recverr": 0
+ },
+ "item_title": "TCP",
+ "item_description": "A directory name of TCP statistics",
+ "map_item_spec": [
+ {
+ "item_name": "open",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Open",
+ "item_description": "IPv6 TCP sockets opened successfully"
+ },
+ {
+ "item_name": "openfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Open failures",
+ "item_description": "IPv6 TCP sockets open failures"
+ },
+ {
+ "item_name": "close",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Close",
+ "item_description": "IPv6 TCP sockets closed"
+ },
+ {
+ "item_name": "connfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Connect failures",
+ "item_description": "IPv6 TCP sockets connection failures"
+ },
+ {
+ "item_name": "conn",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Connect",
+ "item_description": "IPv6 TCP connections established successfully"
+ },
+ {
+ "item_name": "senderr",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Send errors",
+ "item_description": "IPv6 TCP sockets send errors"
+ },
+ {
+ "item_name": "recverr",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Receive errors",
+ "item_description": "IPv6 TCP sockets receive errors"
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
]
}
diff --git a/src/bin/xfrin/xfrin_messages.mes b/src/bin/xfrin/xfrin_messages.mes
index eeddee9..4383324 100644
--- a/src/bin/xfrin/xfrin_messages.mes
+++ b/src/bin/xfrin/xfrin_messages.mes
@@ -56,10 +56,31 @@ most likely cause is that xfrin the msgq daemon is not running.
There was an error while the given command was being processed. The
error is given in the log message.
-% XFRIN_CONNECT_MASTER error connecting to master at %1: %2
+% XFRIN_CONNECT_MASTER (with TSIG %1) error connecting to master at %2: %3
There was an error opening a connection to the master. The error is
shown in the log message.
+% XFRIN_DATASRC_CONFIG_ERROR failed to update data source configuration: %1
+Configuration for the global data sources is updated, but the update
+cannot be applied to xfrin. The xfrin module will still keep running
+with the previous configuration, but the cause of the failure and
+other log messages must be carefully examined because if only xfrin
+rejects the new configuration then the entire BIND 10 system will have
+inconsistent state among different modules. If other modules accept
+the update but xfrin produces this error, the xfrin module should
+probably be restarted.
+
+% XFRIN_DATASRC_UNKNOWN data source to transfer %1 to is unknown
+The xfrin daemon received a command that would trigger a transfer,
+but could not find a data source where the specified zone belongs.
+There can be several reasons for this error: it may be a simple
+misspelling in the xfrin or zonemgr configuration, or in the user
+supplied parameter if it is triggered by an external command (such as
+from bindctl). Another possibility is that this is the initial transfer
+for a newly setup secondary zone. In this case at least an initial empty zone
+must be created in one of configured data sources. This can be done by
+the -e option of b10-loadzone.
+
% XFRIN_EXITING exiting
The xfrin daemon is exiting.
@@ -80,6 +101,24 @@ is not equal to the requested SOA serial.
There was an error importing the python DNS module pydnspp. The most
likely cause is a PYTHONPATH problem.
+% XFRIN_INITIAL_AXFR no SOA available for %1 yet, requesting AXFR of initial version from %2
+On starting the zone transfer, it's detected that there is no SOA
+record available for the zone. This is always the case for the very
+first transfer or if the administrator has removed the locally copied
+data by hand for some reason. In this case trying IXFR does not make
+sense for the obvious reason, so AXFR will be used from the beginning,
+regardless of the request_ixfr configuration (even if "only" is
+specified).
+
+% XFRIN_INITIAL_IXFR requesting IXFR for %1 from %2
+IXFR will be used for the initial request type for the specified zone
+transfer. It will fall back to AXFR if the initial request fails
+(and unless specified not to do so by configuration).
+
+% XFRIN_INITIAL_IXFR_DISABLED IXFR disabled for %1, requesting AXFR from %2
+The use of IXFR is disabled by configuration for the specified zone,
+so only AXFR will be tried.
+
% XFRIN_INVALID_ZONE_DATA zone %1 received from %2 is broken and unusable
The zone was received, but it failed sanity validation. The previous version
of zone (if any is available) will be used. Look for previous
@@ -120,12 +159,12 @@ the primary server between the SOA and IXFR queries. The client
implementation confirms the whole response is this single SOA, and
aborts the transfer just like a successful case.
-% XFRIN_MSGQ_SEND_ERROR error while contacting %1 and %2
+% XFRIN_MSGQ_SEND_ERROR (with TSIG %1) error while contacting %2 and %3
There was a problem sending a message to the xfrout module or the
zone manager. This most likely means that the msgq daemon has quit or
was killed.
-% XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER error while contacting %1
+% XFRIN_MSGQ_SEND_ERROR_ZONE_MANAGER (with TSIG %1) error while contacting %2
There was a problem sending a message to the zone manager. This most
likely means that the msgq daemon has quit or was killed.
@@ -206,12 +245,23 @@ often.
The XFR transfer for the given zone has failed due to an internal error.
The error is shown in the log message.
-% XFRIN_XFR_TRANSFER_FALLBACK falling back from IXFR to AXFR for %1
+% XFRIN_XFR_TRANSFER_FALLBACK (with TSIG %1) falling back from IXFR to AXFR for %2
The IXFR transfer of the given zone failed. This might happen in many cases,
such that the remote server doesn't support IXFR, we don't have the SOA record
(or the zone at all), we are out of sync, etc. In many of these situations,
AXFR could still work. Therefore we try that one in case it helps.
+% XFRIN_XFR_TRANSFER_FALLBACK_DISABLED (with TSIG %1) suppressing fallback from IXFR to AXFR for %2
+An IXFR transfer of the given zone failed. By default AXFR will be
+tried next, but this fallback is disabled by configuration, so the
+whole transfer attempt failed at that point. If the reason for the
+failure (which should be logged separately) is temporary, this is
+probably harmless or even desired as another IXFR will take place some
+time later (without falling back to the possibly expensive AXFR). If
+this is a permanent error (e.g., some change at the master server
+completely disables IXFR), the secondary zone will eventually expire,
+so the configuration should be changed to allow AXFR.
+
% XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION %1 transfer of zone %2 with %3 failed: %4
The XFR transfer for the given zone has failed due to a protocol
error, such as an unexpected response from the primary server. The
@@ -226,19 +276,6 @@ is recommended to check the primary server configuration.
A connection to the master server has been made, the serial value in
the SOA record has been checked, and a zone transfer has been started.
-% XFRIN_ZONE_CREATED Zone %1 not found in the given data source, newly created
-On starting an xfrin session, it is identified that the zone to be
-transferred is not found in the data source. This can happen if a
-secondary DNS server first tries to perform AXFR from a primary server
-without creating the zone image beforehand (e.g. by b10-loadzone). As
-of this writing the xfrin process provides backward compatible
-behavior to previous versions: creating a new one in the data source
-not to surprise existing users too much. This is probably not a good
-idea, however, in terms of who should be responsible for managing
-zones at a higher level. In future it is more likely that a separate
-zone management framework is provided, and the situation where the
-given zone isn't found in xfrout will be treated as an error.
-
% XFRIN_ZONE_INVALID Newly received zone %1/%2 fails validation: %3
The zone was received successfully, but it failed validation. The problem
is severe enough that the new version of zone is discarded and the old version,
@@ -250,9 +287,9 @@ On starting an xfrin session, it is identified that the zone to be
transferred has multiple SOA RRs. Such a zone is broken, but could be
accidentally configured especially in a data source using "non
captive" backend database. The implementation ignores entire SOA RRs
-and tries to continue processing as if the zone were empty. This
-means subsequent AXFR can succeed and possibly replace the zone with
-valid content, but an IXFR attempt will fail.
+and tries to continue processing as if the zone were empty. This also
+means AXFR will be used unconditionally, regardless of the configured value
+for request_ixfr of the zone.
% XFRIN_ZONE_NO_SOA Zone %1 does not have SOA
On starting an xfrin session, it is identified that the zone to be
diff --git a/src/bin/xfrout/b10-xfrout.xml b/src/bin/xfrout/b10-xfrout.xml
index 5c71e05..468c6af 100644
--- a/src/bin/xfrout/b10-xfrout.xml
+++ b/src/bin/xfrout/b10-xfrout.xml
@@ -171,44 +171,56 @@
<variablelist>
<varlistentry>
- <term><replaceable>zonename</replaceable></term>
+ <term><replaceable>classname</replaceable></term>
<listitem><simpara>
- A actual zone name or special zone name <quote>_SERVER_</quote>
- representing an entire server
+ An actual RR class name of the zone, e.g. IN, CH, and HS
</simpara>
<variablelist>
<varlistentry>
- <term>notifyoutv4</term>
+ <term><replaceable>zonename</replaceable></term>
<listitem><simpara>
- Number of IPv4 notifies per zone name sent out from Xfrout
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>notifyoutv6</term>
- <listitem><simpara>
- Number of IPv6 notifies per zone name sent out from Xfrout
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>xfrrej</term>
- <listitem><simpara>
- Number of XFR requests per zone name rejected by Xfrout
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>xfrreqdone</term>
- <listitem><simpara>
- Number of requested zone transfers per zone name completed
- </simpara></listitem>
- </varlistentry>
+ An actual zone name or special zone
+ name <quote>_SERVER_</quote> representing an entire
+ server
+ </simpara>
+ <variablelist>
+
+ <varlistentry>
+ <term>notifyoutv4</term>
+ <listitem><simpara>
+ Number of IPv4 notifies per zone name sent out from Xfrout
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>notifyoutv6</term>
+ <listitem><simpara>
+ Number of IPv6 notifies per zone name sent out from Xfrout
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>xfrrej</term>
+ <listitem><simpara>
+ Number of XFR requests per zone name rejected by Xfrout
+ </simpara></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>xfrreqdone</term>
+ <listitem><simpara>
+ Number of requested zone transfers per zone name completed
+ </simpara></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </listitem>
+ </varlistentry><!-- end of zonename -->
</variablelist>
</listitem>
- </varlistentry><!-- end of zonename -->
+ </varlistentry><!-- end of classname -->
</variablelist>
</listitem>
diff --git a/src/bin/xfrout/tests/xfrout_test.py.in b/src/bin/xfrout/tests/xfrout_test.py.in
index f663e55..f7ed1e1 100644
--- a/src/bin/xfrout/tests/xfrout_test.py.in
+++ b/src/bin/xfrout/tests/xfrout_test.py.in
@@ -1,4 +1,4 @@
-# Copyright (C) 2010-2012 Internet Systems Consortium.
+# Copyright (C) 2010-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -18,6 +18,8 @@
import unittest
import os
+import socket
+import fcntl
from isc.testutils.tsigctx_mock import MockTSIGContext
from isc.testutils.ccsession_mock import MockModuleCCSession
from isc.cc.session import *
@@ -39,6 +41,7 @@ TSIG_KEY = TSIGKey("example.com:SFuWd/q99SzF8Yzd1QbB9g==")
TEST_ZONE_NAME_STR = "example.com."
TEST_ZONE_NAME = Name(TEST_ZONE_NAME_STR)
TEST_RRCLASS = RRClass.IN
+TEST_RRCLASS_STR = TEST_RRCLASS.to_text()
IXFR_OK_VERSION = 2011111802
IXFR_NG_VERSION = 2011111803
SOA_CURRENT_VERSION = 2011112001
@@ -190,6 +193,29 @@ class Dbserver:
def decrease_transfers_counter(self):
self.transfer_counter -= 1
+class TestUtility(unittest.TestCase):
+ """Test some utility functions."""
+
+ def test_make_blockign(self):
+ def is_blocking(fd):
+ return (fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK) == 0
+
+ # socket.socket doesn't support the 'with' statement before Python
+ # 3.2, so we'll close it ourselves (while it's not completely exception
+ # safe, it should be acceptable for a test case)
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM,
+ socket.IPPROTO_TCP)
+ # By default socket is made blocking
+ self.assertTrue(is_blocking(sock.fileno()))
+ # make_blocking(False) makes it non blocking
+ xfrout.make_blocking(sock.fileno(), False)
+ self.assertFalse(is_blocking(sock.fileno()))
+ # make_blocking(True) makes it blocking again
+ xfrout.make_blocking(sock.fileno(), True)
+ self.assertTrue(is_blocking(sock.fileno()))
+
+ sock.close()
+
class TestXfroutSessionBase(unittest.TestCase):
'''Base class for tests related to xfrout sessions
@@ -269,6 +295,12 @@ class TestXfroutSessionBase(unittest.TestCase):
self.xfrsess._request_typestr = 'IXFR'
def setUp(self):
+ # xfrout.make_blocking won't work with faked socket, and we'd like to
+ # examine how it's called, so we replace it with our mock.
+ self.__orig_make_blocking = xfrout.make_blocking
+ self._make_blocking_history = []
+ xfrout.make_blocking = lambda x, y: self.__make_blocking(x, y)
+
self.sock = MySocket(socket.AF_INET,socket.SOCK_STREAM)
self.xfrsess = MyXfroutSession(self.sock, None, Dbserver(),
TSIGKeyRing(),
@@ -277,7 +309,8 @@ class TestXfroutSessionBase(unittest.TestCase):
# When not testing ACLs, simply accept
isc.acl.dns.REQUEST_LOADER.load(
[{"action": "ACCEPT"}]),
- {})
+ {},
+ xfrout.Counters(xfrout.SPECFILE_LOCATION))
self.set_request_type(RRType.AXFR) # test AXFR by default
self.mdata = self.create_request_data()
self.soa_rrset = create_soa(SOA_CURRENT_VERSION)
@@ -285,7 +318,11 @@ class TestXfroutSessionBase(unittest.TestCase):
# original is used elsewhere.
self.orig_get_rrset_len = xfrout.get_rrset_len
+ def __make_blocking(self, fd, on):
+ self._make_blocking_history.append((fd, on))
+
def tearDown(self):
+ xfrout.make_blocking = self.__orig_make_blocking
xfrout.get_rrset_len = self.orig_get_rrset_len
# transfer_counter must be always be reset no matter happens within
# the XfroutSession object. We check the condition here.
@@ -306,7 +343,12 @@ class TestXfroutSession(TestXfroutSessionBase):
def test_quota_ok(self):
'''The default case in terms of the xfrout quota.
+ It also confirms that the socket is made blocking.
+
'''
+ # make_blocking shouldn't be called yet.
+ self.assertEqual([], self._make_blocking_history)
+
# set up a bogus request, which should result in FORMERR. (it only
# has to be something that is different from the previous case)
self.xfrsess._request_data = \
@@ -316,6 +358,26 @@ class TestXfroutSession(TestXfroutSessionBase):
XfroutSession._handle(self.xfrsess)
self.assertEqual(self.sock.read_msg().get_rcode(), Rcode.FORMERR)
+ # make_blocking should have been called in _handle(). Note that
+ # in the test fixture we handle fileno as a faked socket object, not
+ # as integer.
+ self.assertEqual([(self.sock, True)], self._make_blocking_history)
+
+ def test_make_blocking_fail(self):
+ """Check what if make_blocking() raises an exception."""
+
+ # make_blocking() can fail due to OSError. It shouldn't cause
+ # disruption, and xfrout_start shouldn't be called.
+
+ def raise_exception():
+ raise OSError('faked error')
+ xfrout.make_blocking = lambda x, y: raise_exception()
+ self.start_arg = []
+ self.xfrsess.dns_xfrout_start = \
+ lambda s, x, y: self.start_arg.append((x, y))
+ XfroutSession._handle(self.xfrsess)
+ self.assertEqual([], self.start_arg)
+
def test_exception_from_session(self):
'''Test the case where the main processing raises an exception.
@@ -381,7 +443,8 @@ class TestXfroutSession(TestXfroutSessionBase):
# check the 'xfrrej' counter initially
self.assertRaises(isc.cc.data.DataNotFoundError,
self.xfrsess._counters.get, 'zones',
- TEST_ZONE_NAME_STR, 'xfrrej')
+ TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej')
# Localhost (the default in this test) is accepted
rcode, msg = self.xfrsess._parse_query_message(self.mdata)
self.assertEqual(rcode.to_text(), "NOERROR")
@@ -397,7 +460,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 1)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 1)
# TSIG signed request
request_data = self.create_request_data(with_tsig=True)
@@ -428,7 +492,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 2)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 2)
# ACL using TSIG: no TSIG; should be rejected
acl_setter(isc.acl.dns.REQUEST_LOADER.load([
@@ -438,7 +503,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 3)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 3)
#
# ACL using IP + TSIG: both should match
@@ -460,7 +526,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 4)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 4)
# Address matches, but TSIG doesn't (not included)
self.xfrsess._remote = (socket.AF_INET, socket.SOCK_STREAM,
('192.0.2.1', 12345))
@@ -468,7 +535,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 5)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 5)
# Neither address nor TSIG matches
self.xfrsess._remote = (socket.AF_INET, socket.SOCK_STREAM,
('192.0.2.2', 12345))
@@ -476,7 +544,8 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertEqual(rcode.to_text(), "REFUSED")
# check the 'xfrrej' counter after incrementing
self.assertEqual(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrrej'), 6)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrrej'), 6)
def test_transfer_acl(self):
# ACL checks only with the default ACL
@@ -876,12 +945,14 @@ class TestXfroutSession(TestXfroutSessionBase):
self.assertRaises(isc.cc.data.DataNotFoundError,
self.xfrsess._counters.get,
- 'zones', TEST_ZONE_NAME_STR, 'xfrreqdone')
+ 'zones', TEST_RRCLASS_STR,
+ TEST_ZONE_NAME_STR, 'xfrreqdone')
self.xfrsess._reply_xfrout_query = myreply
self.xfrsess.dns_xfrout_start(self.sock, self.mdata)
self.assertEqual(self.sock.readsent(), b"success")
self.assertGreater(self.xfrsess._counters.get(
- 'zones', TEST_ZONE_NAME_STR, 'xfrreqdone'), 0)
+ 'zones', TEST_RRCLASS_STR, TEST_ZONE_NAME_STR,
+ 'xfrreqdone'), 0)
def test_reply_xfrout_query_axfr(self):
self.xfrsess._soa = self.soa_rrset
@@ -1253,7 +1324,8 @@ class TestUnixSockServer(unittest.TestCase):
# This would be the handler class, but we just check it is passed
# the right parametes, so function is enough for that.
keys = isc.server_common.tsig_keyring.get_keyring()
- def handler(sock, data, server, keyring, address, acl, config):
+ def handler(sock, data, server, keyring, address, acl, config,
+ counters):
self.assertEqual("sock", sock)
self.assertEqual("data", data)
self.assertEqual(self.unix, server)
@@ -1261,6 +1333,7 @@ class TestUnixSockServer(unittest.TestCase):
self.assertEqual("Address", address)
self.assertEqual("acl", acl)
self.assertEqual("Zone config", config)
+ self.assertIs(self.unix._counters, counters)
self.unix.RequestHandlerClass = handler
self.unix.finish_request("sock", "data")
finally:
@@ -1559,7 +1632,9 @@ class TestUnixSockServerForCounter(unittest.TestCase):
xfrout.ThreadingUnixStreamServer = DummySocketserver
xfrout.super = lambda : DummySocketserver()
xfrout.select.select = lambda x,y,z: ([None],[None],[None])
- self.unix = UnixSockServer(None, None, threading.Event(), None, None)
+ self._counters = xfrout.Counters(xfrout.SPECFILE_LOCATION)
+ self.unix = UnixSockServer(None, None, threading.Event(), None, None,
+ self._counters)
def tearDown(self):
( UnixSockServer._remove_unused_sock_file,
@@ -1589,7 +1664,8 @@ class TestUnixSockServerForCounter(unittest.TestCase):
'socket', 'unixdomain', 'openfail')
xfrout.ThreadingUnixStreamServer = DummySocketserverException
try:
- self.unix = UnixSockServer(None, None, None, None, None)
+ self.unix = UnixSockServer(None, None, None, None, None,
+ self._counters)
except Exception:
pass
else:
@@ -1630,7 +1706,7 @@ class TestUnixSockServerForCounter(unittest.TestCase):
self.unix._counters.get,
'socket', 'unixdomain', 'acceptfail')
xfrout.super = lambda : DummyClassException()
- self.unix = UnixSockServer(None, None, None, None, None)
+ self.unix = UnixSockServer(None, None, None, None, None, self._counters)
self.assertRaises(Exception, self.unix.get_request)
self.assertEqual(
self.unix._counters.get('socket', 'unixdomain', 'acceptfail'), 1)
diff --git a/src/bin/xfrout/xfrout.py.in b/src/bin/xfrout/xfrout.py.in
index 1863ad0..aa84587 100755
--- a/src/bin/xfrout/xfrout.py.in
+++ b/src/bin/xfrout/xfrout.py.in
@@ -27,9 +27,11 @@ from socketserver import *
import os
from isc.config.ccsession import *
from isc.cc import SessionError, SessionTimeout
-from isc.statistics import Counters
+from isc.statistics.dns import Counters
from isc.notify import notify_out
import isc.util.process
+import isc.util.traceback_handler
+import fcntl
import socket
import select
import errno
@@ -152,9 +154,31 @@ def get_soa_serial(soa_rdata):
'''
return Serial(int(soa_rdata.to_text().split()[2]))
+def make_blocking(filenum, on):
+ """A helper function to change blocking mode of the given socket.
+
+ It sets the mode of blocking I/O for the socket associated with filenum
+ (descriptor of the socket) according to parameter 'on': if it's True the
+ file will be made blocking; otherwise it will be made non-blocking.
+
+ The given filenum must be a descriptor of a socket (not an ordinary file
+ etc), but this function doesn't check that condition.
+
+ filenum(int): file number (descriptor) of the socket to update.
+ on(bool): whether enable (True) or disable (False) blocking I/O.
+
+ """
+ flags = fcntl.fcntl(filenum, fcntl.F_GETFL)
+ if on: # make it blocking
+ flags &= ~os.O_NONBLOCK
+ else: # make it non blocking
+ flags |= os.O_NONBLOCK
+ fcntl.fcntl(filenum, fcntl.F_SETFL, flags)
+
class XfroutSession():
def __init__(self, sock_fd, request_data, server, tsig_key_ring, remote,
- default_acl, zone_config, client_class=DataSourceClient):
+ default_acl, zone_config, counters,
+ client_class=DataSourceClient):
self._sock_fd = sock_fd
self._request_data = request_data
self._server = server
@@ -171,7 +195,7 @@ class XfroutSession():
self._jnl_reader = None # will be set to a reader for IXFR
# Creation of self.counters should be done before of
# invoking self._handle()
- self._counters = Counters(SPECFILE_LOCATION)
+ self._counters = counters
self._handle()
def create_tsig_ctx(self, tsig_record, tsig_key_ring):
@@ -190,6 +214,10 @@ class XfroutSession():
quota_ok = self._server.increase_transfers_counter()
ex = None
try:
+ # Before start, make sure the socket uses blocking I/O because
+ # responses will be sent in the blocking mode; otherwise it could
+ # result in EWOULDBLOCK and disrupt the session.
+ make_blocking(self._sock_fd, True)
self.dns_xfrout_start(self._sock_fd, self._request_data, quota_ok)
except Exception as e:
# To avoid resource leak we need catch all possible exceptions
@@ -275,7 +303,8 @@ class XfroutSession():
return None, None
elif acl_result == REJECT:
# count rejected Xfr request by each zone name
- self._counters.inc('zones', zone_name.to_text(), 'xfrrej')
+ self._counters.inc('zones', zone_class.to_text(),
+ zone_name.to_text(), 'xfrrej')
logger.debug(DBG_XFROUT_TRACE, XFROUT_QUERY_REJECTED,
self._request_type, format_addrinfo(self._remote),
format_zone_str(zone_name, zone_class))
@@ -545,7 +574,8 @@ class XfroutSession():
else:
self._counters.dec('ixfr_running')
# count done Xfr requests by each zone name
- self._counters.inc('zones', zone_name.to_text(), 'xfrreqdone')
+ self._counters.inc('zones', zone_class.to_text(),
+ zone_name.to_text(), 'xfrreqdone')
logger.info(XFROUT_XFR_TRANSFER_DONE, self._request_typestr,
format_addrinfo(self._remote), zone_str)
@@ -655,11 +685,11 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
'''The unix domain socket server which accept xfr query sent from auth server.'''
def __init__(self, sock_file, handle_class, shutdown_event, config_data,
- cc):
+ cc, counters):
self._remove_unused_sock_file(sock_file)
self._sock_file = sock_file
socketserver_mixin.NoPollMixIn.__init__(self)
- self._counters = Counters(SPECFILE_LOCATION)
+ self._counters = counters
try:
ThreadingUnixStreamServer.__init__(self, sock_file, \
handle_class)
@@ -858,7 +888,8 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
self._lock.release()
self.RequestHandlerClass(sock_fd, request_data, self,
isc.server_common.tsig_keyring.get_keyring(),
- self._guess_remote(sock_fd), acl, zone_config)
+ self._guess_remote(sock_fd), acl, zone_config,
+ self._counters)
def _remove_unused_sock_file(self, sock_file):
'''Try to remove the socket file. If the file is being used
@@ -990,17 +1021,19 @@ class UnixSockServer(socketserver_mixin.NoPollMixIn,
class XfroutServer:
def __init__(self):
+ self._default_notify_address = ''
+ self._default_notify_port = 53
self._unix_socket_server = None
self._listen_sock_file = UNIX_SOCKET_FILE
self._shutdown_event = threading.Event()
self._cc = isc.config.ModuleCCSession(SPECFILE_LOCATION, self.config_handler, self.command_handler)
self._config_data = self._cc.get_full_config()
+ self._counters = Counters(SPECFILE_LOCATION)
self._cc.start()
self._cc.add_remote_config(AUTH_SPECFILE_LOCATION)
isc.server_common.tsig_keyring.init_keyring(self._cc)
self._start_xfr_query_listener()
self._start_notifier()
- self._counters = Counters(SPECFILE_LOCATION)
def _start_xfr_query_listener(self):
'''Start a new thread to accept xfr query. '''
@@ -1009,16 +1042,22 @@ class XfroutServer:
XfroutSession,
self._shutdown_event,
self._config_data,
- self._cc)
+ self._cc, self._counters)
listener = threading.Thread(target=self._unix_socket_server.serve_forever)
listener.start()
def _start_notifier(self):
datasrc = self._unix_socket_server.get_db_file()
- self._notifier = notify_out.NotifyOut(datasrc)
+ self._notifier = notify_out.NotifyOut(datasrc, counters=self._counters)
if 'also_notify' in self._config_data:
for slave in self._config_data['also_notify']:
- self._notifier.add_slave(slave['address'], slave['port'])
+ address = self._default_notify_address
+ if 'address' in slave:
+ address = slave['address']
+ port = self._default_notify_port
+ if 'port' in slave:
+ port = slave['port']
+ self._notifier.add_slave(address, port)
self._notifier.dispatcher()
def send_notify(self, zone_name, zone_class):
@@ -1128,7 +1167,7 @@ def set_cmd_options(parser):
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
-if '__main__' == __name__:
+def main():
try:
parser = OptionParser()
set_cmd_options(parser)
@@ -1153,3 +1192,6 @@ if '__main__' == __name__:
xfrout_server.shutdown()
logger.info(XFROUT_EXITING)
+
+if '__main__' == __name__:
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/xfrout/xfrout.spec.pre.in b/src/bin/xfrout/xfrout.spec.pre.in
index 4277c3b..dd90fe4 100644
--- a/src/bin/xfrout/xfrout.spec.pre.in
+++ b/src/bin/xfrout/xfrout.spec.pre.in
@@ -43,7 +43,7 @@
"item_name": "port",
"item_type": "integer",
"item_optional": false,
- "item_default": 0
+ "item_default": 53
}
]
}
@@ -121,56 +121,66 @@
"item_type": "named_set",
"item_optional": false,
"item_default": {
- "_SERVER_" : {
- "notifyoutv4" : 0,
- "notifyoutv6" : 0,
- "xfrrej" : 0,
- "xfrreqdone" : 0
+ "IN" : {
+ "_SERVER_" : {
+ "notifyoutv4" : 0,
+ "notifyoutv6" : 0,
+ "xfrrej" : 0,
+ "xfrreqdone" : 0
+ }
}
},
"item_title": "Zone names",
"item_description": "A directory name of per-zone statistics",
"named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
+ "item_name": "classname",
+ "item_type": "named_set",
"item_optional": false,
"item_default": {},
- "item_title": "Zone name",
- "item_description": "A actual zone name or special zone name _SERVER_ representing an entire server",
- "map_item_spec": [
- {
- "item_name": "notifyoutv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IPv4 notifies",
- "item_description": "Number of IPv4 notifies per zone name sent out from Xfrout"
- },
- {
- "item_name": "notifyoutv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IPv6 notifies",
- "item_description": "Number of IPv6 notifies per zone name sent out from Xfrout"
- },
- {
- "item_name": "xfrrej",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XFR rejected requests",
- "item_description": "Number of XFR requests per zone name rejected by Xfrout"
- },
- {
- "item_name": "xfrreqdone",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Requested zone transfers",
- "item_description": "Number of requested zone transfers completed per zone name"
- }
- ]
+ "item_title": "RR class name",
+ "item_description": "An actual RR class name of the zone, e.g. IN, CH, and HS",
+ "named_set_item_spec": {
+ "item_name": "zonename",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "Zone name",
+ "item_description": "An actual zone name or special zone name _SERVER_ representing an entire server",
+ "map_item_spec": [
+ {
+ "item_name": "notifyoutv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv4 notifies",
+ "item_description": "Number of IPv4 notifies per zone name sent out from Xfrout"
+ },
+ {
+ "item_name": "notifyoutv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv6 notifies",
+ "item_description": "Number of IPv6 notifies per zone name sent out from Xfrout"
+ },
+ {
+ "item_name": "xfrrej",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XFR rejected requests",
+ "item_description": "Number of XFR requests per zone name rejected by Xfrout"
+ },
+ {
+ "item_name": "xfrreqdone",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Requested zone transfers",
+ "item_description": "Number of requested zone transfers completed per zone name"
+ }
+ ]
+ }
}
},
{
diff --git a/src/bin/zonemgr/tests/zonemgr_test.py b/src/bin/zonemgr/tests/zonemgr_test.py
index 111d650..5a17476 100644
--- a/src/bin/zonemgr/tests/zonemgr_test.py
+++ b/src/bin/zonemgr/tests/zonemgr_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2010 Internet Systems Consortium.
+# Copyright (C) 2010-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -22,6 +22,7 @@ import tempfile
from zonemgr import *
from isc.testutils.ccsession_mock import MockModuleCCSession
from isc.notify import notify_out
+from isc.datasrc import ZoneFinder
ZONE_NAME_CLASS1_IN = ("example.net.", "IN")
ZONE_NAME_CLASS1_CH = ("example.net.", "CH")
@@ -36,8 +37,6 @@ LOWERBOUND_RETRY = 5
REFRESH_JITTER = 0.10
RELOAD_JITTER = 0.75
-TEST_SQLITE3_DBFILE = os.getenv("TESTDATAOBJDIR") + '/initdb.file'
-
class ZonemgrTestException(Exception):
pass
@@ -46,16 +45,53 @@ class FakeCCSession(isc.config.ConfigData, MockModuleCCSession):
module_spec = isc.config.module_spec_from_file(SPECFILE_LOCATION)
ConfigData.__init__(self, module_spec)
MockModuleCCSession.__init__(self)
+ # For inspection
+ self.added_remote_modules = []
+
+ def add_remote_config_by_name(self, name, callback):
+ self.added_remote_modules.append((name, callback))
def rpc_call(self, command, module, instance="*", to="*", params=None):
if module not in ("Auth", "Xfrin"):
raise ZonemgrTestException("module name not exist")
- def get_remote_config_value(self, module_name, identifier):
- if module_name == "Auth" and identifier == "database_file":
- return TEST_SQLITE3_DBFILE, False
+class MockDataSourceClient():
+ '''A simple mock data source client.'''
+ def __init__(self):
+ self.rdata_net = 'a.example.net. root.example.net. 2009073106 ' + \
+ '7200 3600 2419200 21600'
+ self.rdata_org = 'a.example.org. root.example.org. 2009073112 ' + \
+ '7200 3600 2419200 21600'
+
+ def find_zone(self, zone_name):
+ '''Mock version of DataSourceClient.find_zone().'''
+ return (isc.datasrc.DataSourceClient.SUCCESS, self)
+
+ def find(self, name, rrtype, options=ZoneFinder.FIND_DEFAULT):
+ '''Mock version of ZoneFinder.find().'''
+ if name == Name('example.net'):
+ rdata = Rdata(RRType.SOA, RRClass.IN, self.rdata_net)
+ elif name == 'example.org.':
+ rdata = Rdata(RRType.SOA, RRClass.IN, self.rdata_org)
else:
- return "unknown", False
+ return (ZoneFinder.NXDOMAIN, None, 0)
+ rrset = RRset(name, RRClass.IN, RRType.SOA, RRTTL(3600))
+ rrset.add_rdata(rdata)
+ return (ZoneFinder.SUCCESS, rrset, 0)
+
+class MockDataSrcClientsMgr():
+ '''A simple mock data source client manager.'''
+ def __init__(self):
+ self.datasrc_client = MockDataSourceClient()
+
+ def get_client_list(self, rrclass):
+ return self
+
+ def find(self, zone_name, want_exact_match, want_finder):
+ """Pretending find method on the object returned by get_client_list"""
+ if issubclass(type(self.datasrc_client), Exception):
+ raise self.datasrc_client
+ return self.datasrc_client, None, None
class MyZonemgrRefresh(ZonemgrRefresh):
def __init__(self):
@@ -66,19 +102,8 @@ class MyZonemgrRefresh(ZonemgrRefresh):
self._reload_jitter = 0.75
self._refresh_jitter = 0.25
- def get_zone_soa(zone_name, db_file):
- if zone_name == 'example.net.':
- return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
- 'a.example.net. root.example.net. 2009073106 7200 3600 2419200 21600')
- elif zone_name == 'example.org.':
- return (1, 2, 'example.org.', 'example.org.sd.', 21600, 'SOA', None,
- 'a.example.org. root.example.org. 2009073112 7200 3600 2419200 21600')
- else:
- return None
- sqlite3_ds.get_zone_soa = get_zone_soa
-
- ZonemgrRefresh.__init__(self, TEST_SQLITE3_DBFILE, self._slave_socket,
- FakeCCSession())
+ ZonemgrRefresh.__init__(self, self._slave_socket, FakeCCSession())
+ self._datasrc_clients_mgr = MockDataSrcClientsMgr()
current_time = time.time()
self._zonemgr_refresh_info = {
('example.net.', 'IN'): {
@@ -95,19 +120,23 @@ class MyZonemgrRefresh(ZonemgrRefresh):
class TestZonemgrRefresh(unittest.TestCase):
def setUp(self):
- if os.path.exists(TEST_SQLITE3_DBFILE):
- os.unlink(TEST_SQLITE3_DBFILE)
self.stderr_backup = sys.stderr
sys.stderr = open(os.devnull, 'w')
self.zone_refresh = MyZonemgrRefresh()
self.cc_session = FakeCCSession()
def tearDown(self):
- if os.path.exists(TEST_SQLITE3_DBFILE):
- os.unlink(TEST_SQLITE3_DBFILE)
sys.stderr.close()
sys.stderr = self.stderr_backup
+ def test_init(self):
+ """Check some initial configuration after construction"""
+ # data source "module" should have been registrered as a necessary
+ # remote config
+ self.assertEqual([('data_sources',
+ self.zone_refresh._datasrc_config_handler)],
+ self.zone_refresh._module_cc.added_remote_modules)
+
def test_random_jitter(self):
max = 100025.120
jitter = 0
@@ -195,16 +224,9 @@ class TestZonemgrRefresh(unittest.TestCase):
def test_zonemgr_reload_zone(self):
soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
- # We need to restore this not to harm other tests
- old_get_zone_soa = sqlite3_ds.get_zone_soa
- def get_zone_soa(zone_name, db_file):
- return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
- 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
- sqlite3_ds.get_zone_soa = get_zone_soa
-
+ self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
self.zone_refresh.zonemgr_reload_zone(ZONE_NAME_CLASS1_IN)
self.assertEqual(soa_rdata, self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_soa_rdata"])
- sqlite3_ds.get_zone_soa = old_get_zone_soa
def test_get_zone_notifier_master(self):
notify_master = "192.168.1.1"
@@ -275,24 +297,10 @@ class TestZonemgrRefresh(unittest.TestCase):
def test_send_command(self):
self.assertRaises(ZonemgrTestException, self.zone_refresh._send_command, "Unknown", "Notify", None)
- def test_zone_mgr_is_empty(self):
- self.assertFalse(self.zone_refresh._zone_mgr_is_empty())
- self.zone_refresh._zonemgr_refresh_info = {}
- self.assertTrue(self.zone_refresh._zone_mgr_is_empty())
-
def test_zonemgr_add_zone(self):
soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
- # This needs to be restored. The following test actually failed if we left
- # this unclean
- old_get_zone_soa = sqlite3_ds.get_zone_soa
+ self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
time1 = time.time()
-
- def get_zone_soa(zone_name, db_file):
- return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
- 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
-
- sqlite3_ds.get_zone_soa = get_zone_soa
-
self.zone_refresh._zonemgr_refresh_info = {}
self.zone_refresh.zonemgr_add_zone(ZONE_NAME_CLASS1_IN)
self.assertEqual(1, len(self.zone_refresh._zonemgr_refresh_info))
@@ -306,12 +314,13 @@ class TestZonemgrRefresh(unittest.TestCase):
self.assertTrue((time1 + 900 * (1 - self.zone_refresh._reload_jitter)) <= zone_timeout)
self.assertTrue(zone_timeout <= time2 + 900)
- def get_zone_soa2(zone_name, db_file):
+ old_get_zone_soa = self.zone_refresh._get_zone_soa
+ def get_zone_soa2(zone_name_class):
return None
- sqlite3_ds.get_zone_soa = get_zone_soa2
+ self.zone_refresh._get_zone_soa = get_zone_soa2
self.zone_refresh.zonemgr_add_zone(ZONE_NAME_CLASS2_IN)
self.assertTrue(self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS2_IN]["zone_soa_rdata"] is None)
- sqlite3_ds.get_zone_soa = old_get_zone_soa
+ self.zone_refresh._get_zone_soa = old_get_zone_soa
def test_zone_handle_notify(self):
self.assertTrue(self.zone_refresh.zone_handle_notify(
@@ -333,10 +342,7 @@ class TestZonemgrRefresh(unittest.TestCase):
def test_zone_refresh_success(self):
soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
- def get_zone_soa(zone_name, db_file):
- return (1, 2, 'example.net.', 'example.net.sd.', 21600, 'SOA', None,
- 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600')
- sqlite3_ds.get_zone_soa = get_zone_soa
+ self.zone_refresh._datasrc_clients_mgr.datasrc_client.rdata_net = soa_rdata
time1 = time.time()
self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_state"] = ZONE_REFRESHING
self.zone_refresh.zone_refresh_success(ZONE_NAME_CLASS1_IN)
@@ -373,14 +379,14 @@ class TestZonemgrRefresh(unittest.TestCase):
self.assertRaises(ZonemgrException, self.zone_refresh.zone_refresh_fail, ZONE_NAME_CLASS3_CH)
self.assertRaises(ZonemgrException, self.zone_refresh.zone_refresh_fail, ZONE_NAME_CLASS3_IN)
- old_get_zone_soa = sqlite3_ds.get_zone_soa
- def get_zone_soa(zone_name, db_file):
+ old_get_zone_soa = self.zone_refresh._get_zone_soa
+ def get_zone_soa(zone_name_class):
return None
- sqlite3_ds.get_zone_soa = get_zone_soa
+ self.zone_refresh._get_zone_soa = get_zone_soa
self.zone_refresh.zone_refresh_fail(ZONE_NAME_CLASS1_IN)
self.assertEqual(self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["zone_state"],
ZONE_EXPIRED)
- sqlite3_ds.get_zone_soa = old_get_zone_soa
+ self.zone_refresh._get_zone_soa = old_get_zone_soa
def test_find_need_do_refresh_zone(self):
time1 = time.time()
@@ -628,7 +634,6 @@ class MyZonemgr(Zonemgr):
def __exit__(self, type, value, traceback): pass
def __init__(self):
- self._db_file = TEST_SQLITE3_DBFILE
self._zone_refresh = None
self._shutdown_event = threading.Event()
self._module_cc = FakeCCSession()
@@ -649,14 +654,8 @@ class MyZonemgr(Zonemgr):
class TestZonemgr(unittest.TestCase):
def setUp(self):
- if os.path.exists(TEST_SQLITE3_DBFILE):
- os.unlink(TEST_SQLITE3_DBFILE)
self.zonemgr = MyZonemgr()
- def tearDown(self):
- if os.path.exists(TEST_SQLITE3_DBFILE):
- os.unlink(TEST_SQLITE3_DBFILE)
-
def test_config_handler(self):
config_data1 = {
"lowerbound_refresh" : 60,
@@ -676,9 +675,8 @@ class TestZonemgr(unittest.TestCase):
config_data3 = {"refresh_jitter" : 0.7}
self.zonemgr.config_handler(config_data3)
self.assertEqual(0.5, self.zonemgr._config_data.get("refresh_jitter"))
- # The zone doesn't exist in database, simply skip loading soa for it and log an warning
- self.zonemgr._zone_refresh = ZonemgrRefresh(TEST_SQLITE3_DBFILE, None,
- FakeCCSession())
+ # The zone doesn't exist in database, simply skip loading soa for it and log a warning
+ self.zonemgr._zone_refresh = ZonemgrRefresh(None, FakeCCSession())
config_data1["secondary_zones"] = [{"name": "nonexistent.example",
"class": "IN"}]
self.assertEqual(self.zonemgr.config_handler(config_data1),
@@ -689,9 +687,6 @@ class TestZonemgr(unittest.TestCase):
is None)
self.assertEqual(0.1, self.zonemgr._config_data.get("refresh_jitter"))
- def test_get_db_file(self):
- self.assertEqual(TEST_SQLITE3_DBFILE, self.zonemgr.get_db_file())
-
def test_parse_cmd_params(self):
params1 = {"zone_name" : "example.com.", "zone_class" : "CH",
"master" : "127.0.0.1"}
diff --git a/src/bin/zonemgr/zonemgr.py.in b/src/bin/zonemgr/zonemgr.py.in
index fcb929a..622336d 100755
--- a/src/bin/zonemgr/zonemgr.py.in
+++ b/src/bin/zonemgr/zonemgr.py.in
@@ -1,6 +1,6 @@
#!@PYTHON@
-# Copyright (C) 2010 Internet Systems Consortium.
+# Copyright (C) 2010-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -34,12 +34,15 @@ import threading
import select
import socket
import errno
-from isc.datasrc import sqlite3_ds
from optparse import OptionParser, OptionValueError
from isc.config.ccsession import *
import isc.util.process
+import isc.util.traceback_handler
from isc.log_messages.zonemgr_messages import *
from isc.notify import notify_out
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr, ConfigError
+from isc.datasrc import DataSourceClient, ZoneFinder
+from isc.dns import *
# Initialize logging for called modules.
isc.log.init("b10-zonemgr", buffer=True)
@@ -66,7 +69,9 @@ if "B10_FROM_BUILD" in os.environ:
else:
PREFIX = "@prefix@"
DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
+ SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}",
+ DATAROOTDIR).replace("${prefix}",
+ PREFIX)
AUTH_SPECFILE_PATH = SPECFILE_PATH
SPECFILE_LOCATION = SPECFILE_PATH + "/zonemgr.spec"
@@ -76,7 +81,6 @@ __version__ = "BIND10"
# define module name
XFRIN_MODULE_NAME = 'Xfrin'
-AUTH_MODULE_NAME = 'Auth'
# define command name
ZONE_REFRESH_COMMAND = 'refresh_from_zonemgr'
@@ -103,18 +107,23 @@ class ZonemgrRefresh:
can be stopped by calling shutdown() in another thread.
"""
- def __init__(self, db_file, slave_socket, module_cc_session):
- self._mccs = module_cc_session
+ def __init__(self, slave_socket, module_cc):
+ self._module_cc = module_cc
self._check_sock = slave_socket
- self._db_file = db_file
self._zonemgr_refresh_info = {}
self._lowerbound_refresh = None
self._lowerbound_retry = None
self._max_transfer_timeout = None
self._refresh_jitter = None
self._reload_jitter = None
- self.update_config_data(module_cc_session.get_full_config(),
- module_cc_session)
+ # This is essentially private, but we allow tests to customize it.
+ self._datasrc_clients_mgr = DataSrcClientsMgr()
+ # data_sources configuration should be ready with cfgmgr, so this
+ # shouldn't fail; if it ever does we simply propagate the exception
+ # to terminate the program.
+ self._module_cc.add_remote_config_by_name('data_sources',
+ self._datasrc_config_handler)
+ self.update_config_data(module_cc.get_full_config(), module_cc)
self._running = False
def _random_jitter(self, max, jitter):
@@ -133,27 +142,32 @@ class ZonemgrRefresh:
def _set_zone_timer(self, zone_name_class, max, jitter):
"""Set zone next refresh time.
jitter should not be bigger than half the original value."""
- self._set_zone_next_refresh_time(zone_name_class, self._get_current_time() + \
+ self._set_zone_next_refresh_time(zone_name_class,
+ self._get_current_time() +
self._random_jitter(max, jitter))
def _set_zone_refresh_timer(self, zone_name_class):
"""Set zone next refresh time after zone refresh success.
now + refresh - refresh_jitter <= next_refresh_time <= now + refresh
"""
- zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[REFRESH_OFFSET])
+ zone_refresh_time = float(self._get_zone_soa_rdata(zone_name_class).
+ split(" ")[REFRESH_OFFSET])
zone_refresh_time = max(self._lowerbound_refresh, zone_refresh_time)
- self._set_zone_timer(zone_name_class, zone_refresh_time, self._refresh_jitter * zone_refresh_time)
+ self._set_zone_timer(zone_name_class, zone_refresh_time,
+ self._refresh_jitter * zone_refresh_time)
def _set_zone_retry_timer(self, zone_name_class):
"""Set zone next refresh time after zone refresh fail.
now + retry - retry_jitter <= next_refresh_time <= now + retry
"""
- if (self._get_zone_soa_rdata(zone_name_class) is not None):
- zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[RETRY_OFFSET])
+ if self._get_zone_soa_rdata(zone_name_class) is not None:
+ zone_retry_time = float(self._get_zone_soa_rdata(zone_name_class).
+ split(" ")[RETRY_OFFSET])
else:
zone_retry_time = 0.0
zone_retry_time = max(self._lowerbound_retry, zone_retry_time)
- self._set_zone_timer(zone_name_class, zone_retry_time, self._refresh_jitter * zone_retry_time)
+ self._set_zone_timer(zone_name_class, zone_retry_time,
+ self._refresh_jitter * zone_retry_time)
def _set_zone_notify_timer(self, zone_name_class):
"""Set zone next refresh time after receiving notify
@@ -167,19 +181,22 @@ class ZonemgrRefresh:
def zone_refresh_success(self, zone_name_class):
"""Update zone info after zone refresh success"""
- if (self._zone_not_exist(zone_name_class)):
- logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0], zone_name_class[1])
+ if self._zone_not_exist(zone_name_class):
+ logger.error(ZONEMGR_UNKNOWN_ZONE_SUCCESS, zone_name_class[0],
+ zone_name_class[1])
raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
"belong to zonemgr" % zone_name_class)
self.zonemgr_reload_zone(zone_name_class)
self._set_zone_refresh_timer(zone_name_class)
self._set_zone_state(zone_name_class, ZONE_OK)
- self._set_zone_last_refresh_time(zone_name_class, self._get_current_time())
+ self._set_zone_last_refresh_time(zone_name_class,
+ self._get_current_time())
def zone_refresh_fail(self, zone_name_class):
"""Update zone info after zone refresh fail"""
- if (self._zone_not_exist(zone_name_class)):
- logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0], zone_name_class[1])
+ if self._zone_not_exist(zone_name_class):
+ logger.error(ZONEMGR_UNKNOWN_ZONE_FAIL, zone_name_class[0],
+ zone_name_class[1])
raise ZonemgrException("[b10-zonemgr] Zone (%s, %s) doesn't "
"belong to zonemgr" % zone_name_class)
# Is zone expired?
@@ -199,11 +216,6 @@ class ZonemgrRefresh:
zone; the Auth module should have rejected the case where it's not
even authoritative for the zone.
- Note: to be more robust and less independent from other module's
- behavior, it's probably safer to check the authority condition here,
- too. But right now it uses SQLite3 specific API (to be deprecated),
- so we rather rely on Auth.
-
Parameters:
zone_name_class (Name, RRClass): the notified zone name and class.
master (str): textual address of the NOTIFY sender.
@@ -219,35 +231,86 @@ class ZonemgrRefresh:
def zonemgr_reload_zone(self, zone_name_class):
""" Reload a zone."""
- zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
- self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = zone_soa[7]
+ self._zonemgr_refresh_info[zone_name_class]["zone_soa_rdata"] = \
+ self._get_zone_soa(zone_name_class)
def zonemgr_add_zone(self, zone_name_class):
""" Add a zone into zone manager."""
-
- logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0], zone_name_class[1])
+ logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_LOAD_ZONE, zone_name_class[0],
+ zone_name_class[1])
zone_info = {}
- zone_soa = sqlite3_ds.get_zone_soa(str(zone_name_class[0]), self._db_file)
+ zone_soa = self._get_zone_soa(zone_name_class)
if zone_soa is None:
logger.warn(ZONEMGR_NO_SOA, zone_name_class[0], zone_name_class[1])
zone_info["zone_soa_rdata"] = None
zone_reload_time = 0.0
else:
- zone_info["zone_soa_rdata"] = zone_soa[7]
- zone_reload_time = float(zone_soa[7].split(" ")[RETRY_OFFSET])
+ zone_info["zone_soa_rdata"] = zone_soa
+ zone_reload_time = float(zone_soa.split(" ")[RETRY_OFFSET])
zone_info["zone_state"] = ZONE_OK
zone_info["last_refresh_time"] = self._get_current_time()
self._zonemgr_refresh_info[zone_name_class] = zone_info
- # Imposes some random jitters to avoid many zones need to do refresh at the same time.
+ # Imposes some random jitters to avoid many zones need to do refresh
+ # at the same time.
zone_reload_time = max(self._lowerbound_retry, zone_reload_time)
- self._set_zone_timer(zone_name_class, zone_reload_time, self._reload_jitter * zone_reload_time)
+ self._set_zone_timer(zone_name_class, zone_reload_time,
+ self._reload_jitter * zone_reload_time)
+
+ def _get_zone_soa(self, zone_name_class):
+ """Retrieve the current SOA RR of the zone to be transferred."""
+
+ def get_zone_soa_rrset(datasrc_client, zone_name, zone_class):
+ """Retrieve the current SOA RR of the zone to be transferred."""
+ # get the zone finder. this must be SUCCESS (not even
+ # PARTIALMATCH) because we are specifying the zone origin name.
+ result, finder = datasrc_client.find_zone(zone_name)
+ if result != DataSourceClient.SUCCESS:
+ # The data source doesn't know the zone. In the context in
+ # which this function is called, this shouldn't happen.
+ raise ZonemgrException(
+ "unexpected result: zone %s/%s doesn't exist" %
+ (zone_name.to_text(True), str(zone_class)))
+ result, soa_rrset, _ = finder.find(zone_name, RRType.SOA)
+ if result != ZoneFinder.SUCCESS:
+ logger.warn(ZONEMGR_NO_SOA,
+ zone_name.to_text(True), str(zone_class))
+ return None
+ return soa_rrset
+
+ # Identify the data source to which the zone content is transferred,
+ # and get the current zone SOA from the data source (if available).
+ datasrc_client = None
+ clist = self._datasrc_clients_mgr.get_client_list(zone_name_class[1])
+ if clist is None:
+ return None
+ try:
+ datasrc_client = clist.find(zone_name_class[0], True, False)[0]
+ if datasrc_client is None: # can happen, so log it separately.
+ logger.error(ZONEMGR_DATASRC_UNKNOWN,
+ zone_name_class[0] + '/' + zone_name_class[1])
+ return None
+ zone_soa = get_zone_soa_rrset(datasrc_client,
+ Name(zone_name_class[0]),
+ RRClass(zone_name_class[1]))
+ if zone_soa == None:
+ return None
+ else:
+ return zone_soa.get_rdata()[0].to_text()
+ except isc.datasrc.Error as ex:
+ # rare case error. re-raise as ZonemgrException so it'll be logged
+ # in command_handler().
+ raise ZonemgrException('unexpected failure in datasrc module: ' +
+ str(ex))
def _zone_is_expired(self, zone_name_class):
"""Judge whether a zone is expired or not."""
- zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).split(" ")[EXPIRED_OFFSET])
- zone_last_refresh_time = self._get_zone_last_refresh_time(zone_name_class)
+ zone_expired_time = float(self._get_zone_soa_rdata(zone_name_class).
+ split(" ")[EXPIRED_OFFSET])
+ zone_last_refresh_time = \
+ self._get_zone_last_refresh_time(zone_name_class)
if (ZONE_EXPIRED == self._get_zone_state(zone_name_class) or
- zone_last_refresh_time + zone_expired_time <= self._get_current_time()):
+ zone_last_refresh_time + zone_expired_time <=
+ self._get_current_time()):
return True
return False
@@ -262,16 +325,19 @@ class ZonemgrRefresh:
self._zonemgr_refresh_info[zone_name_class]["last_refresh_time"] = time
def _get_zone_notifier_master(self, zone_name_class):
- if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
+ if ("notify_master" in
+ self._zonemgr_refresh_info[zone_name_class].keys()):
return self._zonemgr_refresh_info[zone_name_class]["notify_master"]
return None
def _set_zone_notifier_master(self, zone_name_class, master_addr):
- self._zonemgr_refresh_info[zone_name_class]["notify_master"] = master_addr
+ self._zonemgr_refresh_info[zone_name_class]["notify_master"] = \
+ master_addr
def _clear_zone_notifier_master(self, zone_name_class):
- if ("notify_master" in self._zonemgr_refresh_info[zone_name_class].keys()):
+ if ("notify_master" in
+ self._zonemgr_refresh_info[zone_name_class].keys()):
del self._zonemgr_refresh_info[zone_name_class]["notify_master"]
def _get_zone_state(self, zone_name_class):
@@ -295,7 +361,7 @@ class ZonemgrRefresh:
def _send_command(self, module_name, command_name, params):
"""Send command between modules."""
try:
- self._mccs.rpc_call(command_name, module_name, params=params)
+ self._module_cc.rpc_call(command_name, module_name, params=params)
except socket.error:
# FIXME: WTF? Where does socket.error come from? And how do we ever
# dare ignore such serious error? It can only be broken link to
@@ -314,7 +380,8 @@ class ZonemgrRefresh:
# If hasn't received refresh response but are within refresh
# timeout, skip the zone
if (ZONE_REFRESHING == zone_state and
- (self._get_zone_refresh_timeout(zone_name_class) > self._get_current_time())):
+ (self._get_zone_refresh_timeout(zone_name_class) >
+ self._get_current_time())):
continue
# Get the zone with minimum next_refresh_time
@@ -324,7 +391,8 @@ class ZonemgrRefresh:
zone_need_refresh = zone_name_class
# Find the zone need do refresh
- if (self._get_zone_next_refresh_time(zone_need_refresh) < self._get_current_time()):
+ if (self._get_zone_next_refresh_time(zone_need_refresh) <
+ self._get_current_time()):
break
return zone_need_refresh
@@ -332,9 +400,12 @@ class ZonemgrRefresh:
def _do_refresh(self, zone_name_class):
"""Do zone refresh."""
- logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE, zone_name_class[0], zone_name_class[1])
+ logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_REFRESH_ZONE,
+ zone_name_class[0], zone_name_class[1])
self._set_zone_state(zone_name_class, ZONE_REFRESHING)
- self._set_zone_refresh_timeout(zone_name_class, self._get_current_time() + self._max_transfer_timeout)
+ self._set_zone_refresh_timeout(zone_name_class,
+ self._get_current_time() +
+ self._max_transfer_timeout)
notify_master = self._get_zone_notifier_master(zone_name_class)
# If the zone has notify master, send notify command to xfrin module
if notify_master:
@@ -351,13 +422,6 @@ class ZonemgrRefresh:
}
self._send_command(XFRIN_MODULE_NAME, ZONE_REFRESH_COMMAND, param)
- def _zone_mgr_is_empty(self):
- """Does zone manager has no zone?"""
- if not len(self._zonemgr_refresh_info):
- return True
-
- return False
-
def _run_timer(self, start_event):
while self._running:
# Notify run_timer that we already started and are inside the loop.
@@ -367,24 +431,29 @@ class ZonemgrRefresh:
if start_event:
start_event.set()
start_event = None
- # If zonemgr has no zone, set timer timeout to self._lowerbound_retry.
- if self._zone_mgr_is_empty():
+ # If zonemgr has no zone, set timeout to minimum
+ if not self._zonemgr_refresh_info:
timeout = self._lowerbound_retry
else:
zone_need_refresh = self._find_need_do_refresh_zone()
- # If don't get zone with minimum next refresh time, set timer timeout to self._lowerbound_retry.
+ # If don't get zone with minimum next refresh time, set
+ # timeout to minimum
if not zone_need_refresh:
timeout = self._lowerbound_retry
else:
- timeout = self._get_zone_next_refresh_time(zone_need_refresh) - self._get_current_time()
- if (timeout < 0):
+ timeout = \
+ self._get_zone_next_refresh_time(zone_need_refresh) - \
+ self._get_current_time()
+ if timeout < 0:
self._do_refresh(zone_need_refresh)
continue
""" Wait for the socket notification for a maximum time of timeout
in seconds (as float)."""
try:
- rlist, wlist, xlist = select.select([self._check_sock, self._read_sock], [], [], timeout)
+ rlist, wlist, xlist = \
+ select.select([self._check_sock, self._read_sock],
+ [], [], timeout)
except select.error as e:
if e.args[0] == errno.EINTR:
(rlist, wlist, xlist) = ([], [], [])
@@ -403,8 +472,8 @@ class ZonemgrRefresh:
def run_timer(self, daemon=False):
"""
- Keep track of zone timers. Spawns and starts a thread. The thread object
- is returned.
+ Keep track of zone timers. Spawns and starts a thread. The thread
+ object is returned.
You can stop it by calling shutdown().
"""
@@ -429,6 +498,20 @@ class ZonemgrRefresh:
# Return the thread to anyone interested
return self._thread
+ def _datasrc_config_handler(self, new_config, config_data):
+ """Configuration handler of the 'data_sources' module.
+
+ The actual handling is delegated to the DataSrcClientsMgr class;
+ this method is a simple wrapper.
+
+ This is essentially private, but implemented as 'protected' so tests
+ can refer to it; other external use is prohibited.
+ """
+ try:
+ self._datasrc_clients_mgr.reconfigure(new_config, config_data)
+ except isc.server_common.datasrc_clients_mgr.ConfigError as ex:
+ logger.error(ZONEMGR_DATASRC_CONFIG_ERROR, ex)
+
def shutdown(self):
"""
Stop the run_timer() thread. Block until it finished. This must be
@@ -450,7 +533,7 @@ class ZonemgrRefresh:
self._read_sock = None
self._write_sock = None
- def update_config_data(self, new_config, module_cc_session):
+ def update_config_data(self, new_config, module_cc):
""" update ZonemgrRefresh config """
# Get a new value, but only if it is defined (commonly used below)
# We don't use "value or default", because if value would be
@@ -499,7 +582,7 @@ class ZonemgrRefresh:
# Currently we use an explicit get_default_value call
# in case the class hasn't been set. Alternatively, we
# could use
- # module_cc_session.get_value('secondary_zones[INDEX]/class')
+ # module_cc.get_value('secondary_zones[INDEX]/class')
# To get either the value that was set, or the default if
# it wasn't set.
# But the real solution would be to make new_config a type
@@ -509,7 +592,7 @@ class ZonemgrRefresh:
if 'class' in secondary_zone:
rr_class = secondary_zone['class']
else:
- rr_class = module_cc_session.get_default_value(
+ rr_class = module_cc.get_default_value(
'secondary_zones/class')
# Convert rr_class to and from RRClass to check its value
try:
@@ -521,10 +604,12 @@ class ZonemgrRefresh:
required[name_class] = True
# Add it only if it isn't there already
if not name_class in self._zonemgr_refresh_info:
- # If we are not able to find it in database, log an warning
+ # If we are not able to find it in database, log an
+ # warning
self.zonemgr_add_zone(name_class)
# Drop the zones that are no longer there
- # Do it in two phases, python doesn't like deleting while iterating
+ # Do it in two phases, python doesn't like deleting while
+ # iterating
to_drop = []
for old_zone in self._zonemgr_refresh_info:
if not old_zone in required:
@@ -539,10 +624,11 @@ class Zonemgr:
def __init__(self):
self._zone_refresh = None
self._setup_session()
- self._db_file = self.get_db_file()
- # Create socket pair for communicating between main thread and zonemgr timer thread
- self._master_socket, self._slave_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
- self._zone_refresh = ZonemgrRefresh(self._db_file, self._slave_socket, self._module_cc)
+ # Create socket pair for communicating between main thread and zonemgr
+ # timer thread
+ self._master_socket, self._slave_socket = \
+ socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._zone_refresh = ZonemgrRefresh(self._slave_socket, self._module_cc)
self._zone_refresh.run_timer()
self._lock = threading.Lock()
@@ -550,9 +636,10 @@ class Zonemgr:
self.running = False
def _setup_session(self):
- """Setup two sessions for zonemgr, one(self._module_cc) is used for receiving
- commands and config data sent from other modules, another one (self._cc)
- is used to send commands to proper modules."""
+ """Setup two sessions for zonemgr, one(self._module_cc) is used for
+ receiving commands and config data sent from other modules, another
+ one (self._cc) is used to send commands to proper modules.
+ """
self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
self.config_handler,
self.command_handler)
@@ -561,18 +648,9 @@ class Zonemgr:
self._config_data_check(self._config_data)
self._module_cc.start()
- def get_db_file(self):
- db_file, is_default = self._module_cc.get_remote_config_value(AUTH_MODULE_NAME, "database_file")
- # this too should be unnecessary, but currently the
- # 'from build' override isn't stored in the config
- # (and we don't have indirect python access to datasources yet)
- if is_default and "B10_FROM_BUILD" in os.environ:
- db_file = os.environ["B10_FROM_BUILD"] + "/bind10_zones.sqlite3"
- return db_file
-
def shutdown(self):
"""Shutdown the zonemgr process. The thread which is keeping track of
- zone timers should be terminated.
+ zone timers should be terminated.
"""
self._zone_refresh.shutdown()
@@ -596,7 +674,8 @@ class Zonemgr:
self._config_data_check(complete)
if self._zone_refresh is not None:
try:
- self._zone_refresh.update_config_data(complete, self._module_cc)
+ self._zone_refresh.update_config_data(complete,
+ self._module_cc)
except Exception as e:
answer = create_answer(1, str(e))
ok = False
@@ -608,7 +687,8 @@ class Zonemgr:
def _config_data_check(self, config_data):
"""Check whether the new config data is valid or
not. It contains only basic logic, not full check against
- database."""
+ database.
+ """
# jitter should not be bigger than half of the original value
if config_data.get('refresh_jitter') > 0.5:
config_data['refresh_jitter'] = 0.5
@@ -625,7 +705,7 @@ class Zonemgr:
logger.error(ZONEMGR_NO_ZONE_CLASS)
raise ZonemgrException("zone class should be provided")
- if (command != ZONE_NOTIFY_COMMAND):
+ if command != ZONE_NOTIFY_COMMAND:
return (zone_name, zone_class)
master_str = args.get("master")
@@ -641,7 +721,8 @@ class Zonemgr:
ZONE_NOTIFY_COMMAND is issued by Auth process;
ZONE_NEW_DATA_READY_CMD and ZONE_XFRIN_FAILED are issued by
Xfrin process;
- shutdown is issued by a user or Init process. """
+ shutdown is issued by a user or Init process.
+ """
answer = create_answer(0)
if command == ZONE_NOTIFY_COMMAND:
""" Handle Auth notify command"""
@@ -691,6 +772,7 @@ class Zonemgr:
try:
while not self._shutdown_event.is_set():
fileno = self._module_cc.get_socket().fileno()
+ reads = []
# Wait with select() until there is something to read,
# and then read it using a non-blocking read
# This may or may not be relevant data for this loop,
@@ -721,7 +803,7 @@ def set_cmd_options(parser):
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="display more about what is going on")
-if '__main__' == __name__:
+def main():
try:
logger.debug(DBG_START_SHUT, ZONEMGR_STARTING)
parser = OptionParser()
@@ -749,3 +831,6 @@ if '__main__' == __name__:
zonemgrd.shutdown()
logger.info(ZONEMGR_SHUTDOWN)
+
+if '__main__' == __name__:
+ isc.util.traceback_handler.traceback_handler(main)
diff --git a/src/bin/zonemgr/zonemgr_messages.mes b/src/bin/zonemgr/zonemgr_messages.mes
index 0334858..e749e3b 100644
--- a/src/bin/zonemgr/zonemgr_messages.mes
+++ b/src/bin/zonemgr/zonemgr_messages.mes
@@ -1,4 +1,4 @@
-# Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -19,6 +19,16 @@
An error was encountered on the command channel. The message indicates
the nature of the error.
+% ZONEMGR_DATASRC_CONFIG_ERROR failed to update data source configuration: %1
+Configuration for the global data sources is updated, but the update
+cannot be applied to zonemgr. The zonemgr module will still keep running
+with the previous configuration, but the cause of the failure and
+other log messages must be carefully examined because if only zonemgr
+rejects the new configuration then the entire BIND 10 system will have
+inconsistent state among different modules. If other modules accept
+the update but zonemgr produces this error, the zonemgr module should
+probably be restarted.
+
% ZONEMGR_JITTER_TOO_BIG refresh_jitter is too big, setting to 0.5
The value specified in the configuration for the refresh jitter is too large
so its value has been set to the maximum of 0.5.
diff --git a/src/hooks/Makefile.am b/src/hooks/Makefile.am
new file mode 100644
index 0000000..815beed
--- /dev/null
+++ b/src/hooks/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = dhcp
diff --git a/src/hooks/dhcp/Makefile.am b/src/hooks/dhcp/Makefile.am
new file mode 100644
index 0000000..6785617
--- /dev/null
+++ b/src/hooks/dhcp/Makefile.am
@@ -0,0 +1 @@
+SUBDIRS = user_chk
diff --git a/src/hooks/dhcp/user_chk/Makefile.am b/src/hooks/dhcp/user_chk/Makefile.am
new file mode 100644
index 0000000..6b68f38
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/Makefile.am
@@ -0,0 +1,68 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+# Until logging in dynamically loaded libraries is fixed,
+# Define rule to build logging source files from message file
+# user_chk_messages.h user_chk_messages.cc: s-messages
+
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+# s-messages: user_chk_messages.mes
+# $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/hooks/dhcp/user_chk/user_chk_messages.mes
+# touch $@
+
+# Tell automake that the message files are built as part of the build process
+# (so that they are built before the main library is built).
+# BUILT_SOURCES = user_chk_messages.h user_chk_messages.cc
+BUILT_SOURCES =
+
+# Ensure that the message file is included in the distribution
+EXTRA_DIST = libdhcp_user_chk.dox
+
+# Get rid of generated message files on a clean
+#CLEANFILES = *.gcno *.gcda user_chk_messages.h user_chk_messages.cc s-messages
+CLEANFILES = *.gcno *.gcda
+
+lib_LTLIBRARIES = libdhcp_user_chk.la
+libdhcp_user_chk_la_SOURCES =
+libdhcp_user_chk_la_SOURCES += load_unload.cc
+libdhcp_user_chk_la_SOURCES += pkt_receive_co.cc
+libdhcp_user_chk_la_SOURCES += pkt_send_co.cc
+libdhcp_user_chk_la_SOURCES += subnet_select_co.cc
+libdhcp_user_chk_la_SOURCES += user.cc user.h
+libdhcp_user_chk_la_SOURCES += user_chk.h
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#libdhcp_user_chk_la_SOURCES += user_chk_log.cc user_chk_log.h
+libdhcp_user_chk_la_SOURCES += user_data_source.h
+libdhcp_user_chk_la_SOURCES += user_file.cc user_file.h
+libdhcp_user_chk_la_SOURCES += user_registry.cc user_registry.h
+libdhcp_user_chk_la_SOURCES += version.cc
+
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#nodist_libdhcp_user_chk_la_SOURCES = user_chk_messages.cc user_chk_messages.h
+
+libdhcp_user_chk_la_CXXFLAGS = $(AM_CXXFLAGS)
+libdhcp_user_chk_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libdhcp_user_chk_la_LDFLAGS = $(AM_LDFLAGS)
+libdhcp_user_chk_la_LDFLAGS += -avoid-version -export-dynamic -module
+libdhcp_user_chk_la_LIBADD =
+libdhcp_user_chk_la_LIBADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+libdhcp_user_chk_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
+libdhcp_user_chk_la_LIBADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcp_user_chk_la_LIBADD += $(top_builddir)/src/lib/util/libb10-util.la
+libdhcp_user_chk_la_LIBADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
+
+
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+libdhcp_user_chk_la_CXXFLAGS += -Wno-unused-parameter
+endif
diff --git a/src/hooks/dhcp/user_chk/libdhcp_user_chk.dox b/src/hooks/dhcp/user_chk/libdhcp_user_chk.dox
new file mode 100644
index 0000000..09d0e48
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/libdhcp_user_chk.dox
@@ -0,0 +1,207 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ at page libdhcp_user_chk DHCP User Check Hooks Library
+
+ at section libdhcp_user_chkIntro Libdhcp_user_chk: An Example Hooks Library
+## Introduction
+libdhcp_user_chk is an example hooks library which customizes the DHCP query
+processing provided by BIND X DHCP server modules (b10-dhcp4 and b10-dhcp6).
+Specifically it allows subnet selection and DHCP response option customization
+based upon a registry of DHCP clients. Note that the words "client" and "user" are used interchangeably herein. The intent of the custom behavior is three
+fold:
+
+1. To assign "new" or "unregistered" users to a restricted subnet, while "known"
+or "registered" users are assigned to unrestricted subnets.
+
+2. To allow DHCP response options or vendor option values to be customized
+based upon user identity.
+
+3. To provide a real time record of the user registration activity which can be sampled by an external consumer.
+
+## User Registry Classes
+At the heart of the library is a class hierarchy centered around the class,
+user_chk::UserRegistry. This class represents a maintainable, searchable
+registry of "known" users and their attributes. It provides services to load,
+clear, and refresh the registry as well as to add, find, and remove users.
+
+Each entry in the registry is an instance of the class, user_chk::User. Users
+are uniquely identified by their user_chk::UserId. UserIds are comprised of
+data taken from the DHCP request. IPv4 users have a type of "HW_ADDR" and
+their id is the hardware address from the request. IPv6 users have a type of
+"DUID" and their id is the DUID from the request.
+
+The registry may be manually populated or loaded from a source of data which
+implements the UserDataSource interface. Currently, a single implementation has
+been implemented, user_chk::UserFile. UserFile supports reading the registry
+from a text file in which each line is a user entry in JSON format. Each entry
+contains the id type and user id along with optional attributes. Attributes are
+essentially name/value pairs whose significance is left up to the calling layer.
+UserFile does not enforce any specific content beyond id type and id.
+(See user_file.h for more details on file content).
+
+## Callout Processing
+The library implements callouts for packet receive, subnet select, and packet
+send for both IPv4 and IPv6. Regardless of the protocol type, the process
+flow upon receipt of an inbound request is the same and is as follows:
+
+-# pkt_receive callout is invoked
+ -# Refresh the user registry
+ -# Extract user id from DHCP request and store it to context
+ -# Look up user id in registry and store resultant user pointer to context
+
+ Note that each time a packet is received, the user registry is refreshed.
+ This ensures that the registry content always has the latest external
+ updates. The primary goal at this stage is check the registry for the
+ user and push the result to the context making it available to subsequent
+ callouts.
+
+-# subnet_select callout is invoked
+ -# Retrieve the user pointer from context
+ -# If pointer is null (i.e. user is not registered), replace subnet
+ selection with restricted subnet
+
+ By convention, the last subnet in the collection of subnets available is
+ assumed to be the "restricted access" subnet. A more sophisticated mechanism is likely to be needed.
+
+-# pkt_send callout is invoked:
+ -# Retrieve the user id and user pointer from context
+ -# If user is not null add the options based on user's attributes,
+ otherwise use default user's attributes
+ -# Generate user check outcome
+
+ This final step is what produces the real time record, referred to as the
+ "user check outcome" file.
+
+## Using the library
+Two steps are required in order to use the library:
+-# The user registry file must be created and deployed
+-# The BIND10 DHCP module(s) must be configured to load the library
+
+### Creating the Registry File
+Currently, the library uses a hard coded pathname for the user registry defined
+in load_unload.cc:
+
+ const char* registry_fname = "/tmp/user_chk_registry.txt";
+
+Each line in the file is a self-contained JSON snippet which must have the
+following two entries:
+
+ - "type" whose value is "HW_ADDR" for IPv4 users or "DUID" for IPv6 users
+ - "id" whose value is either the hardware address or the DUID from the
+ request formatted as a string of hex digits, with or without ":" delimiters.
+
+and may have the zero or more of the following entries:
+
+ - "bootfile" whose value is the pathname of the desired file
+ - "tftp_server" whose value is the hostname or IP address of the desired
+ server
+
+Sample user registry file is shown below:
+
+ at code
+{ "type" : "HW_ADDR", "id" : "0c:0e:0a:01:ff:04", "bootfile" : "/tmp/v4bootfile" }
+{ "type" : "HW_ADDR", "id" : "0c:0e:0a:01:ff:06", "tftp_server" : "tftp.v4.example.com" }
+{ "type" : "DUID", "id" : "00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04", "bootfile" : "/tmp/v6bootfile" }
+{ "type" : "DUID", "id" : "00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:06", "tftp_server" : "tftp.v6.example.com" }
+ at endcode
+
+It is possible to specify additional attributes. They will be loaded and stored with the user's entry in the registry. This allows the library to be extended to perform additional actions based on these attributes.
+
+Upon start up the library will attempt to load this file. If it does not exist
+the library will unload.
+
+### Configuring the DHCP Modules
+it must be configured as a hook library for the
+desired DHCP server modules. Note that the user_chk library is installed alongside the BIND10 libraries in "<install-dir>/lib" where <install-dir> is determined by the --prefix option of the configure script. It defaults to "/usr/local". Assuming the default value then, configuring b10-dhcp4 to load the user_chk
+library could be done with the following BIND10 configuration commands:
+
+ at code
+config add Dhcp4/hook_libraries
+config set Dhcp4/hook_libraries[0] "/usr/local/lib/libdhcp_user_chk.so"
+config commit
+ at endcode
+
+To configure it for b10-dhcp6, the commands are simply as shown below:
+
+ at code
+config add Dhcp6/hook_libraries
+config set Dhcp6/hook_libraries[0] "/usr/local/lib/libdhcp_user_chk.so"
+config commit
+ at endcode
+
+## User Check Outcome
+Once up and running, the library should begin adding entries to the outcome
+file. Currently, the library uses a hard coded pathname for the user registry defined in load_unload.cc:
+
+ const char* user_chk_output_fname = "/tmp/user_chk_outcome.txt";
+
+If the file cannot be created (or opened), the library will unload.
+
+For each lease granted, the library will add the following information to the
+end of the file: the id type, the user id, the lease or prefix granted, and
+whether or not the user was found in the registry. This information is written
+in the form of "name=value" with one value per line. (See subnet_callout.cc for details.)
+
+A sample outcome file is shown below:
+
+ at code
+id_type=HW_ADDR
+client=hwtype=1 0c:0e:0a:01:ff:04
+addr=175.16.1.100
+registered=yes
+
+id_type=HW_ADDR
+client=hwtype=1 0c:0e:0a:01:ff:05
+addr=152.0.2.10
+registered=no
+
+id_type=HW_ADDR
+client=hwtype=1 0c:0e:0a:01:ff:06
+addr=175.16.1.101
+registered=yes
+
+id_type=HW_ADDR
+client=hwtype=1 0c:0e:0a:01:ff:04
+addr=175.16.1.102
+registered=yes
+
+id_type=DUID
+client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04
+addr=2001:db8:2::1:0:0/96
+registered=yes
+
+id_type=DUID
+client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04
+addr=2001:db8:2::
+registered=yes
+
+id_type=DUID
+client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:05
+addr=5005:778:2::
+registered=no
+
+id_type=DUID
+client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:06
+addr=2001:db8:2::1
+registered=yes
+
+ at endcode
+
+Note the library always opens this file in append mode and does not limit its size.
+
+
+
+*/
diff --git a/src/hooks/dhcp/user_chk/load_unload.cc b/src/hooks/dhcp/user_chk/load_unload.cc
new file mode 100644
index 0000000..dff2622
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/load_unload.cc
@@ -0,0 +1,132 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file load_unload.cc Defines the load and unload hooks library functions.
+
+#include <hooks/hooks.h>
+#include <user_registry.h>
+#include <user_file.h>
+
+#include <iostream>
+#include <fstream>
+#include <errno.h>
+
+using namespace isc::hooks;
+using namespace user_chk;
+
+/// @brief Pointer to the registry instance.
+UserRegistryPtr user_registry;
+
+/// @brief Output filestream for recording user check outcomes.
+std::fstream user_chk_output;
+
+/// @brief User registry input file name.
+/// @todo Hard-coded for now, this should be configurable.
+const char* registry_fname = "/tmp/user_chk_registry.txt";
+
+/// @brief User check outcome file name.
+/// @todo Hard-coded for now, this should be configurable.
+const char* user_chk_output_fname = "/tmp/user_chk_outcome.txt";
+
+/// @brief Text label of user id in the inbound query in callout context
+const char* query_user_id_label = "query_user_id";
+
+/// @brief Text label of registered user pointer in callout context
+const char* registered_user_label = "registered_user";
+
+/// @brief Text id used to identify the default IPv4 user in the registry
+/// The format is a string containing an even number of hex digits. This
+/// value is to look up the default IPv4 user in the user registry for the
+/// the purpose of retrieving default values for user options.
+const char* default_user4_id_str = "00000000";
+
+/// @brief Text id used to identify the default IPv6 user in the registry
+/// The format is a string containing an even number of hex digits. This
+/// value is to look up the default IPv6 user in the user registry for the
+/// the purpose of retrieving default values for user options.
+const char *default_user6_id_str = "00000000";
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief Called by the Hooks library manager when the library is loaded.
+///
+/// Instantiates the UserRegistry and opens the outcome file. Failure in
+/// either results in a failed return code.
+///
+/// @param unused library handle parameter required by Hooks API.
+///
+/// @return Returns 0 upon success, non-zero upon failure.
+int load(LibraryHandle&) {
+ // non-zero indicates an error.
+ int ret_val = 0;
+ try {
+ // Instantiate the registry.
+ user_registry.reset(new UserRegistry());
+
+ // Create the data source.
+ UserDataSourcePtr user_file(new UserFile(registry_fname));
+
+ // Set the registry's data source
+ user_registry->setSource(user_file);
+
+ // Do an initial load of the registry.
+ user_registry->refresh();
+
+ // Open up the output file for user_chk results.
+ user_chk_output.open(user_chk_output_fname,
+ std::fstream::out | std::fstream::app);
+
+ if (!user_chk_output) {
+ // Grab the system error message.
+ const char* errmsg = strerror(errno);
+ isc_throw(isc::Unexpected, "Cannot open output file: "
+ << user_chk_output_fname
+ << " reason: " << errmsg);
+ }
+ }
+ catch (const std::exception& ex) {
+ // Log the error and return failure.
+ std::cout << "DHCP UserCheckHook could not be loaded: "
+ << ex.what() << std::endl;
+ ret_val = 1;
+ }
+
+ return (ret_val);
+}
+
+/// @brief Called by the Hooks library manager when the library is unloaded.
+///
+/// Destroys the UserRegistry and closes the outcome file.
+///
+/// @return Always returns 0.
+int unload() {
+ try {
+ user_registry.reset();
+ if (user_chk_output.is_open()) {
+ user_chk_output.close();
+ }
+ } catch (const std::exception& ex) {
+ // On the off chance something goes awry, catch it and log it.
+ // @todo Not sure if we should return a non-zero result or not.
+ std::cout << "DHCP UserCheckHook could not be unloaded: "
+ << ex.what() << std::endl;
+ }
+
+ return (0);
+}
+
+}
diff --git a/src/hooks/dhcp/user_chk/pkt_receive_co.cc b/src/hooks/dhcp/user_chk/pkt_receive_co.cc
new file mode 100644
index 0000000..bf00922
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/pkt_receive_co.cc
@@ -0,0 +1,144 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file pkt_receive.cc Defines the pkt4_receive and pkt6_receive callout functions.
+
+#include <hooks/hooks.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt6.h>
+#include <user_chk.h>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace user_chk;
+using namespace std;
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief This callout is called at the "pkt4_receive" hook.
+///
+/// This function determines if the DHCP client identified by the inbound
+/// DHCP query packet is in the user registry.
+/// Upon entry, the registry is refreshed. Next the hardware address is
+/// extracted from query and saved to the context as the "query_user_id".
+/// This id is then used to search the user registry. The resultant UserPtr
+/// whether the user is found or not, is saved to the callout context as
+/// "registered_user". This makes the registered user, if not null, available
+/// to subsequent callouts.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int pkt4_receive(CalloutHandle& handle) {
+ if (!user_registry) {
+ std::cout << "DHCP UserCheckHook : pkt4_receive UserRegistry is null"
+ << std::endl;
+ return (1);
+ }
+
+ try {
+ // Refresh the registry.
+ user_registry->refresh();
+
+ // Get the HWAddress to use as the user identifier.
+ Pkt4Ptr query;
+ handle.getArgument("query4", query);
+ HWAddrPtr hwaddr = query->getHWAddr();
+
+ // Store the id we search with so it is available down the road.
+ handle.setContext(query_user_id_label, hwaddr);
+
+ // Look for the user in the registry.
+ UserPtr registered_user = user_registry->findUser(*hwaddr);
+
+ // Store user regardless. Empty user pointer means non-found. It is
+ // cheaper to fetch it and test it, than to use an exception throw.
+ handle.setContext(registered_user_label, registered_user);
+ std::cout << "DHCP UserCheckHook : pkt4_receive user : "
+ << hwaddr->toText() << " is "
+ << (registered_user ? " registered" : " not registered")
+ << std::endl;
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : pkt4_receive unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+/// @brief This callout is called at the "pkt6_receive" hook.
+///
+/// This function determines if the DHCP client identified by the inbound
+/// DHCP query packet is in the user registry.
+/// Upon entry, the registry is refreshed. Next the DUID is extracted from
+/// query and saved to the context as the "query_user_id". This id is then
+/// used to search the user registry. The resultant UserPtr whether the user
+/// is found or not, is saved to the callout context as "registered_user".
+/// This makes the registered user, if not null, available to subsequent
+/// callouts.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int pkt6_receive(CalloutHandle& handle) {
+ if (!user_registry) {
+ std::cout << "DHCP UserCheckHook : pkt6_receive UserRegistry is null"
+ << std::endl;
+ return (1);
+ }
+
+ try {
+ // Refresh the registry.
+ user_registry->refresh();
+
+ // Fetch the inbound packet.
+ Pkt6Ptr query;
+ handle.getArgument("query6", query);
+
+ // Get the DUID to use as the user identifier.
+ OptionPtr opt_duid = query->getOption(D6O_CLIENTID);
+ if (!opt_duid) {
+ std::cout << "DHCP6 query is missing DUID" << std::endl;
+ return (1);
+ }
+ DuidPtr duid = DuidPtr(new DUID(opt_duid->getData()));
+
+ // Store the id we search with so it is available down the road.
+ handle.setContext(query_user_id_label, duid);
+
+ // Look for the user in the registry.
+ UserPtr registered_user = user_registry->findUser(*duid);
+
+ // Store user regardless. Empty user pointer means non-found. It is
+ // cheaper to fetch it and test it, than to use an exception throw.
+ handle.setContext(registered_user_label, registered_user);
+ std::cout << "DHCP UserCheckHook : pkt6_receive user : "
+ << duid->toText() << " is "
+ << (registered_user ? " registered" : " not registered")
+ << std::endl;
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : pkt6_receive unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+} // end extern "C"
diff --git a/src/hooks/dhcp/user_chk/pkt_send_co.cc b/src/hooks/dhcp/user_chk/pkt_send_co.cc
new file mode 100644
index 0000000..805333b
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/pkt_send_co.cc
@@ -0,0 +1,541 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file pkt_send.cc Defines the pkt4_send and pkt6_send callout functions.
+
+#include <asiolink/io_address.h>
+#include <hooks/hooks.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option6_ia.h>
+#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <user_chk.h>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace user_chk;
+using namespace std;
+
+// prototypes for local helper functions
+void generate_output_record(const std::string& id_type_str,
+ const std::string& id_val_str,
+ const std::string& addr_str,
+ const bool& registered);
+std::string getV6AddrStr (Pkt6Ptr response);
+std::string getAddrStrIA_NA(OptionPtr options);
+std::string getAddrStrIA_PD(OptionPtr options);
+bool checkIAStatus(boost::shared_ptr<Option6IA>& ia_opt);
+
+void add4Options(Pkt4Ptr& response, const UserPtr& user);
+void add4Option(Pkt4Ptr& response, uint8_t opt_code, std::string& opt_value);
+void add6Options(Pkt6Ptr& response, const UserPtr& user);
+void add6Option(OptionPtr& vendor, uint8_t opt_code, std::string& opt_value);
+const UserPtr& getDefaultUser4();
+const UserPtr& getDefaultUser6();
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief This callout is called at the "pkt4_send" hook.
+///
+/// This function generates the user check outcome and modifies options
+/// to the IPv4 response packet based on whether the user is registered or not.
+///
+/// It retrieves a pointer to the registered user from the callout context.
+/// This value should have been set upstream. If the registered user pointer
+/// is non-null (i.e the user is registered), then a registered user outcome
+/// is recorded in the outcome output and the vendor properties are altered
+/// based upon this user's properties.
+///
+/// A null value means the user is not registered and a unregistered user
+/// outcome is recorded in the outcome output and the vendor properties
+/// are altered based upon the default IPv4 user in the registry (if defined).
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int pkt4_send(CalloutHandle& handle) {
+ try {
+ Pkt4Ptr response;
+ handle.getArgument("response4", response);
+
+ uint8_t packet_type = response->getType();
+ if (packet_type == DHCPNAK) {
+ std::cout << "DHCP UserCheckHook : pkt4_send"
+ << "skipping packet type: "
+ << static_cast<int>(packet_type) << std::endl;
+ return (0);
+ }
+
+ // Get the user id saved from the query packet.
+ HWAddrPtr hwaddr;
+ handle.getContext(query_user_id_label, hwaddr);
+
+ // Get registered_user pointer.
+ UserPtr registered_user;
+ handle.getContext(registered_user_label, registered_user);
+
+ // Fetch the lease address.
+ isc::asiolink::IOAddress addr = response->getYiaddr();
+
+ if (registered_user) {
+ // add options based on user
+ // then generate registered output record
+ std::cout << "DHCP UserCheckHook : pkt4_send registered_user is: "
+ << registered_user->getUserId() << std::endl;
+
+ // Add the outcome entry to the output file.
+ generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
+ addr.toText(), true);
+ add4Options(response, registered_user);
+ } else {
+ // add default options based
+ // then generate not registered output record
+ std::cout << "DHCP UserCheckHook : pkt4_send no registered_user"
+ << std::endl;
+ // Add the outcome entry to the output file.
+ generate_output_record(UserId::HW_ADDRESS_STR, hwaddr->toText(),
+ addr.toText(), false);
+
+ add4Options(response, getDefaultUser4());
+ }
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : pkt4_send unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+/// @brief This callout is called at the "pkt6_send" hook.
+///
+/// This function generates the user check outcome and modifies options
+/// to the IPv6 response packet based on whether the user is registered or not.
+///
+/// It retrieves a pointer to the registered user from the callout context.
+/// This value should have been set upstream. If the registered user pointer
+/// is non-null (i.e the user is registered), then a registered user outcome
+/// is recorded in the outcome output and the vendor properties are altered
+/// based upon this user's properties.
+///
+/// A null value means the user is not registered and a unregistered user
+/// outcome is recorded in the outcome output and the vendor properties
+/// are altered based upon the default IPv6 user in the registry (if defined).
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int pkt6_send(CalloutHandle& handle) {
+ try {
+ Pkt6Ptr response;
+ handle.getArgument("response6", response);
+
+ // Fetch the lease address as a string
+ std::string addr_str = getV6AddrStr(response);
+ if (addr_str.empty()) {
+ // packet did not contain an address, must be failed.
+ std::cout << "pkt6_send: Skipping packet address is blank"
+ << std::endl;
+ return (0);
+ }
+
+ // Get the user id saved from the query packet.
+ DuidPtr duid;
+ handle.getContext(query_user_id_label, duid);
+
+ // Get registered_user pointer.
+ UserPtr registered_user;
+ handle.getContext(registered_user_label, registered_user);
+
+ if (registered_user) {
+ // add options based on user
+ // then generate registered output record
+ std::cout << "DHCP UserCheckHook : pkt6_send registered_user is: "
+ << registered_user->getUserId() << std::endl;
+ // Add the outcome entry to the output file.
+ generate_output_record(UserId::DUID_STR, duid->toText(),
+ addr_str, true);
+ add6Options(response, registered_user);
+ } else {
+ // add default options based
+ // then generate not registered output record
+ std::cout << "DHCP UserCheckHook : pkt6_send no registered_user"
+ << std::endl;
+ // Add the outcome entry to the output file.
+ generate_output_record(UserId::DUID_STR, duid->toText(),
+ addr_str, false);
+ add6Options(response, getDefaultUser6());
+ }
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : pkt6_send unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+} // extern C
+
+/// @brief Adds IPv4 options to the response packet based on given user
+///
+/// Adds or replaces IPv4 options with values from the given user, if
+/// the user has corresponding properties defined. Currently it supports
+/// the following options:
+///
+/// - DHO_BOOT_FILE_NAME from user property "bootfile"
+/// - DHO_TFTP_SERVER_NAME from user property "tftp_server"
+///
+/// @param response IPv4 response packet
+/// @param user User from whom properties are sourced
+void add4Options(Pkt4Ptr& response, const UserPtr& user) {
+ // If user is null, do nothing.
+ if (!user) {
+ return;
+ }
+
+ // If the user has bootfile property, update it in the response.
+ std::string opt_value = user->getProperty("bootfile");
+ if (!opt_value.empty()) {
+ std::cout << "DHCP UserCheckHook : add4Options "
+ << "adding boot file:" << opt_value << std::endl;
+
+ // Add boot file to packet.
+ add4Option(response, DHO_BOOT_FILE_NAME, opt_value);
+
+ // Boot file also goes in file field.
+ response->setFile((const uint8_t*)(opt_value.c_str()),
+ opt_value.length());
+ }
+
+ // If the user has tftp server property, update it in the response.
+ opt_value = user->getProperty("tftp_server");
+ if (!opt_value.empty()) {
+ std::cout << "DHCP UserCheckHook : add4Options "
+ << "adding TFTP server:" << opt_value << std::endl;
+
+ // Add tftp server option to packet.
+ add4Option(response, DHO_TFTP_SERVER_NAME, opt_value);
+ }
+ // add next option here
+}
+
+/// @brief Adds/updates are specific IPv4 string option in response packet.
+///
+/// @param response IPV4 response packet to update
+/// @param opt_code DHCP standard numeric code of the option
+/// @param opt_value String value of the option
+void add4Option(Pkt4Ptr& response, uint8_t opt_code, std::string& opt_value) {
+ // Remove the option if it exists.
+ OptionPtr opt = response->getOption(opt_code);
+ if (opt) {
+ response->delOption(opt_code);
+ }
+
+ // Now add the option.
+ opt.reset(new OptionString(Option::V4, opt_code, opt_value));
+ response->addOption(opt);
+}
+
+
+/// @brief Adds IPv6 vendor options to the response packet based on given user
+///
+/// Adds or replaces IPv6 vendor options with values from the given user, if
+/// the user has the corresponding properties defined. Currently it supports
+/// the following options:
+///
+/// - DOCSIS3_V6_CONFIG_FILE from user property "bootfile"
+/// - DOCSIS3_V6_TFTP_SERVERS from user property "tftp_server"
+///
+/// @param response IPv5 reponse packet
+/// @param user User from whom properties are sourced
+void add6Options(Pkt6Ptr& response, const UserPtr& user) {
+ if (!user) {
+ return;
+ }
+
+ /// @todo: if packets have no vendor opt... do we need to add it
+ /// if its not there? If so how?
+ OptionPtr vendor = response->getOption(D6O_VENDOR_OPTS);
+ if (!vendor) {
+ std::cout << "DHCP UserCheckHook : add6Options "
+ << "response has no vendor option to update" << std::endl;
+ return;
+ }
+
+ // If the user defines bootfile, set the option in response.
+ std::string opt_value = user->getProperty("bootfile");
+ if (!opt_value.empty()) {
+ std::cout << "DHCP UserCheckHook : add6Options "
+ << "adding boot file:" << opt_value << std::endl;
+ add6Option(vendor, DOCSIS3_V6_CONFIG_FILE, opt_value);
+ }
+
+ // If the user defines tftp server, set the option in response.
+ opt_value = user->getProperty("tftp_server");
+ if (!opt_value.empty()) {
+ std::cout << "DHCP UserCheckHook : add6Options "
+ << "adding tftp server:" << opt_value << std::endl;
+
+ add6Option(vendor, DOCSIS3_V6_TFTP_SERVERS, opt_value);
+ }
+
+ // add next option here
+}
+
+/// @brief Adds/updates a specific IPv6 string vendor option.
+///
+/// @param vendor IPv6 vendor option set to update
+/// @param opt_code DHCP standard numeric code of the option
+/// @param opt_value String value of the option
+void add6Option(OptionPtr& vendor, uint8_t opt_code, std::string& opt_value) {
+ vendor->delOption(opt_code);
+ OptionPtr option(new OptionString(Option::V6, opt_code, opt_value));
+ vendor->addOption(option);
+}
+
+
+/// @brief Adds an entry to the end of the user check outcome file.
+///
+/// @todo This ought to be replaced with an abstract output similar to
+/// UserDataSource to allow greater flexibility.
+///
+/// Each user entry is written in an ini-like format, with one name-value pair
+/// per line as follows:
+///
+/// id_type=<id type>
+/// client=<id str>
+/// subnet=<subnet str>
+/// registered=<is registered>"
+///
+/// where:
+/// <id type> text label of the id type: "HW_ADDR" or "DUID"
+/// <id str> user's id formatted as either isc::dhcp::Hwaddr.toText() or
+/// isc::dhcp::DUID.toText()
+/// <subnet str> selected subnet formatted as isc::dhcp::Subnet4::toText() or
+/// isc::dhcp::Subnet6::toText() as appropriate.
+/// <is registered> "yes" or "no"
+///
+/// Sample IPv4 entry would like this:
+///
+/// @code
+/// id_type=DUID
+/// client=00:01:00:01:19:ef:e6:3b:00:0c:01:02:03:04
+/// subnet=2001:db8:2::/64
+/// registered=yes
+/// id_type=duid
+/// @endcode
+///
+/// Sample IPv4 entry would like this:
+///
+/// @code
+/// id_type=DUID
+/// id_type=HW_ADDR
+/// client=hwtype=1 00:0c:01:02:03:05
+/// subnet=152.77.5.0/24
+/// registered=no
+/// @endcode
+///
+/// @param id_type_str text label identify the id type
+/// @param id_val_str text representation of the user id
+/// @param subnet_str text representation of the selected subnet
+/// @param registered boolean indicating if the user is registered or not
+void generate_output_record(const std::string& id_type_str,
+ const std::string& id_val_str,
+ const std::string& addr_str,
+ const bool& registered)
+{
+ user_chk_output << "id_type=" << id_type_str << std::endl
+ << "client=" << id_val_str << std::endl
+ << "addr=" << addr_str << std::endl
+ << "registered=" << (registered ? "yes" : "no")
+ << std::endl
+ << std::endl; // extra line in between
+
+ // @todo Flush is here to ensure output is immediate for demo purposes.
+ // Performance would generally dictate not using it.
+ flush(user_chk_output);
+}
+
+/// @brief Stringify the lease address or prefix IPv6 response packet
+///
+/// Converts the lease value, either an address or a prefix, into a string
+/// suitable for the user check outcome output. Note that this will use
+/// the first address or prefix in the response for responses with more than
+/// one value.
+///
+/// @param response IPv6 response packet from which to extract the lease value.
+///
+/// @return A string containing the lease value.
+/// @throw isc::BadValue if the response contains neither an IA_NA nor IA_PD
+/// option.
+std::string getV6AddrStr(Pkt6Ptr response) {
+ OptionPtr tmp = response->getOption(D6O_IA_NA);
+ if (tmp) {
+ return(getAddrStrIA_NA(tmp));
+ }
+
+ // IA_NA not there so try IA_PD
+ tmp = response->getOption(D6O_IA_PD);
+ if (!tmp) {
+ isc_throw (isc::BadValue, "Response has neither IA_NA nor IA_PD");
+ }
+
+ return(getAddrStrIA_PD(tmp));
+}
+
+/// @brief Stringify the lease address in an D6O_IA_NA option set
+///
+/// Converts the IA_NA lease address into a string suitable for the user check
+/// outcome output.
+///
+/// @param options pointer to the Option6IA instance from which to extract the
+/// lease address.
+///
+/// @return A string containing the lease address.
+///
+/// @throw isc::BadValue if the lease address cannot be extracted from options.
+std::string getAddrStrIA_NA(OptionPtr options) {
+ boost::shared_ptr<Option6IA> ia =
+ boost::dynamic_pointer_cast<Option6IA>(options);
+
+ if (!ia) {
+ isc_throw (isc::BadValue, "D6O_IA_NA option invalid");
+ }
+
+ // If status indicates a failure return a blank string.
+ if (!checkIAStatus(ia)) {
+ return (std::string(""));
+ }
+
+ options = ia->getOption(D6O_IAADDR);
+ if (!options) {
+ isc_throw (isc::BadValue, "D6O_IAADDR option missing");
+ }
+
+ boost::shared_ptr<Option6IAAddr> addr_option;
+ addr_option = boost::dynamic_pointer_cast<Option6IAAddr>(options);
+ if (!addr_option) {
+ isc_throw (isc::BadValue, "D6O_IAADDR Option6IAAddr missing");
+ }
+
+ isc::asiolink::IOAddress addr = addr_option->getAddress();
+ return (addr.toText());
+}
+
+/// @brief Stringify the lease prefix in an D6O_IA_PD option set
+///
+/// Converts the IA_PD lease prefix into a string suitable for the user check
+/// outcome output.
+///
+/// @param options pointer to the Option6IA instance from which to extract the
+/// lease prefix.
+///
+/// @return A string containing lease prefix
+///
+/// @throw isc::BadValue if the prefix cannot be extracted from options.
+std::string getAddrStrIA_PD(OptionPtr options) {
+ boost::shared_ptr<Option6IA> ia =
+ boost::dynamic_pointer_cast<Option6IA>(options);
+
+ // Make sure we have an IA_PD option.
+ if (!ia) {
+ isc_throw (isc::BadValue, "D6O_IA_PD option invalid");
+ }
+
+ // Check the response for success status. If it isn't a success response
+ // there will not be a lease prefix value which is denoted by returning
+ // an empty string.
+ if (!checkIAStatus(ia)) {
+ return (std::string(""));
+ }
+
+ // Get the prefix option the IA_PD option.
+ options = ia->getOption(D6O_IAPREFIX);
+ if (!options) {
+ isc_throw(isc::BadValue, "D60_IAPREFIX option is missing");
+ }
+
+ boost::shared_ptr<Option6IAPrefix> addr_option;
+ addr_option = boost::dynamic_pointer_cast<Option6IAPrefix>(options);
+ if (!addr_option) {
+ isc_throw (isc::BadValue, "D6O_IA_PD addr option bad");
+ }
+
+ // Get the address and prefix length values.
+ isc::asiolink::IOAddress addr = addr_option->getAddress();
+ uint8_t prefix_len = addr_option->getLength();
+
+ // Build the output string and return it.
+ stringstream buf;
+ buf << addr.toText() << "/" << static_cast<int>(prefix_len);
+ return (buf.str());
+}
+
+/// @brief Tests given IA option set for successful status.
+///
+/// This function is used to determine if the given Option6IA represents
+/// a successful lease operation. If it contains no status option or a status
+/// option of 0 (which is defined to mean success), then the option represents
+/// success and should contain a lease value (address or prefix).
+///
+/// @param ia pointer to the Option6IA to test
+///
+/// @return True if the option represents success, false otherwise.
+bool checkIAStatus(boost::shared_ptr<Option6IA>& ia) {
+ OptionCustomPtr status =
+ boost::dynamic_pointer_cast
+ <OptionCustom>(ia->getOption(D6O_STATUS_CODE));
+
+ // If a non-zero status is present, bail.
+ if (status) {
+ int status_val = status->readInteger<uint16_t>(0);
+ if (status_val != 0) {
+ std::cout << "SKIPPING PACKET STATUS is not success:"
+ << status_val << std::endl;
+ return (false);
+ }
+ }
+
+ return (true);
+}
+
+/// @brief Fetches the default IPv4 user from the registry.
+///
+/// The default user may be used to provide default property values.
+///
+/// @return A pointer to the IPv4 user or null if not defined.
+const UserPtr& getDefaultUser4() {
+ return (user_registry->findUser(UserId(UserId::HW_ADDRESS,
+ default_user4_id_str)));
+}
+
+/// @brief Fetches the default IPv6 user from the registry.
+///
+/// The default user may be used to provide default property values.
+///
+/// @return A pointer to the IPv6 user or null if not defined.
+const UserPtr& getDefaultUser6() {
+ return (user_registry->findUser(UserId(UserId::DUID,
+ default_user6_id_str)));
+}
+
diff --git a/src/hooks/dhcp/user_chk/subnet_select_co.cc b/src/hooks/dhcp/user_chk/subnet_select_co.cc
new file mode 100644
index 0000000..c30f37b
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/subnet_select_co.cc
@@ -0,0 +1,144 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file subnet_select.cc Defines the subnet4_select and subnet6_select callout functions.
+
+#include <hooks/hooks.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt6.h>
+#include <dhcpsrv/subnet.h>
+#include <user_chk.h>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace user_chk;
+using namespace std;
+
+// Functions accessed by the hooks framework use C linkage to avoid the name
+// mangling that accompanies use of the C++ compiler as well as to avoid
+// issues related to namespaces.
+extern "C" {
+
+/// @brief This callout is called at the "subnet4_select" hook.
+///
+/// This function alters the selected subnet based upon whether or not the
+/// requesting DHCP client is a "registered user". It fetches a pointer to
+/// the registered user from the callout context. This value is presumed to
+/// have been set upstream. If it is non-null that it points to the client's
+/// user entry in the UserRegistry. If it is null, the client is not
+/// registered.
+///
+/// If the client is registered, then replace the selected subnet with the
+/// restricted access subnet. By convention, it is assumed that last subnet in
+/// the list of available subnets is the restricted access subnet.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int subnet4_select(CalloutHandle& handle) {
+ if (!user_registry) {
+ std::cout << "DHCP UserCheckHook : subnet4_select UserRegistry is null"
+ << std::endl;
+ return (1);
+ }
+
+ try {
+ // Get subnet collection. If it's empty just bail nothing to do.
+ const isc::dhcp::Subnet4Collection *subnets = NULL;
+ handle.getArgument("subnet4collection", subnets);
+ if (subnets->empty()) {
+ return (0);
+ }
+
+ // Get registered_user pointer.
+ UserPtr registered_user;
+ handle.getContext(registered_user_label, registered_user);
+
+ if (registered_user) {
+ // User is in the registry, so leave the pre-selected subnet alone.
+ Subnet4Ptr subnet;
+ handle.getArgument("subnet4", subnet);
+ } else {
+ // User is not in the registry, so assign them to the last subnet
+ // in the collection. By convention we are assuming this is the
+ // restricted subnet.
+ Subnet4Ptr subnet = subnets->back();
+ handle.setArgument("subnet4", subnet);
+ }
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : subnet6_select unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+/// @brief This callout is called at the "subnet6_select" hook.
+///
+/// This function alters the selected subnet based upon whether or not the
+/// requesting DHCP client is a "registered user". It fetches a pointer to
+/// the registered user from the callout context. This value is presumed to
+/// have been set upstream. If it is non-null that it points to the client's
+/// user entry in the UserRegistry. If it is null, the client is not
+/// registered.
+///
+/// If the client is registered, then replace the selected subnet with the
+/// restricted access subnet. By convention, it is assumed that last subnet in
+/// the list of available subnets is the restricted access subnet.
+///
+/// @param handle CalloutHandle which provides access to context.
+///
+/// @return 0 upon success, non-zero otherwise.
+int subnet6_select(CalloutHandle& handle) {
+ if (!user_registry) {
+ std::cout << "DHCP UserCheckHook : subnet6_select UserRegistry is null"
+ << std::endl;
+ return (1);
+ }
+
+ try {
+ // Get subnet collection. If it's empty just bail nothing to do.
+ const isc::dhcp::Subnet6Collection *subnets = NULL;
+ handle.getArgument("subnet6collection", subnets);
+ if (subnets->empty()) {
+ return (0);
+ }
+
+ // Get registered_user pointer.
+ UserPtr registered_user;
+ handle.getContext(registered_user_label, registered_user);
+
+ if (registered_user) {
+ // User is in the registry, so leave the pre-selected subnet alone.
+ Subnet6Ptr subnet;
+ handle.getArgument("subnet6", subnet);
+ } else {
+ // User is not in the registry, so assign them to the last subnet
+ // in the collection. By convention we are assuming this is the
+ // restricted subnet.
+ Subnet6Ptr subnet = subnets->back();
+ handle.setArgument("subnet6", subnet);
+ }
+ } catch (const std::exception& ex) {
+ std::cout << "DHCP UserCheckHook : subnet6_select unexpected error: "
+ << ex.what() << std::endl;
+ return (1);
+ }
+
+ return (0);
+}
+
+}
diff --git a/src/hooks/dhcp/user_chk/tests/.gitignore b/src/hooks/dhcp/user_chk/tests/.gitignore
new file mode 100644
index 0000000..32b5d22
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/.gitignore
@@ -0,0 +1,2 @@
+/libdhcp_user_chk_unittests
+/test_data_files_config.h
diff --git a/src/hooks/dhcp/user_chk/tests/Makefile.am b/src/hooks/dhcp/user_chk/tests/Makefile.am
new file mode 100644
index 0000000..3a73f6d
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/Makefile.am
@@ -0,0 +1,77 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -I$(top_builddir)/src/hooks/dhcp/user_chk -I$(top_srcdir)/src/hooks/dhcp/user_chk
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(BOTAN_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_srcdir)/src/hooks/dhcp/user_chk/tests\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+USER_CHK_LIB = $(top_builddir)/src/hooks/dhcp/user_chk/libdhcp_user_chk.la
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+# Unit test data files need to get installed.
+EXTRA_DIST = test_users_1.txt test_users_err.txt
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += libdhcp_user_chk_unittests
+
+libdhcp_user_chk_unittests_SOURCES =
+libdhcp_user_chk_unittests_SOURCES += ../load_unload.cc
+libdhcp_user_chk_unittests_SOURCES += ../pkt_receive_co.cc
+libdhcp_user_chk_unittests_SOURCES += ../pkt_send_co.cc
+libdhcp_user_chk_unittests_SOURCES += ../subnet_select_co.cc
+libdhcp_user_chk_unittests_SOURCES += ../version.cc
+libdhcp_user_chk_unittests_SOURCES += ../user.cc ../user.h
+libdhcp_user_chk_unittests_SOURCES += ../user_chk.h
+# Until logging in dynamically loaded libraries is fixed, exclude these.
+#libdhcp_user_chk_unittests_SOURCES += ../user_chk_log.cc ../user_chk_log.h
+#libdhcp_user_chk_unittests_SOURCES += ../user_chk_messages.cc ../user_chk_messages.h
+libdhcp_user_chk_unittests_SOURCES += ../user_data_source.h
+libdhcp_user_chk_unittests_SOURCES += ../user_file.cc ../user_file.h
+libdhcp_user_chk_unittests_SOURCES += ../user_registry.cc ../user_registry.h
+libdhcp_user_chk_unittests_SOURCES += run_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += userid_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_registry_unittests.cc
+libdhcp_user_chk_unittests_SOURCES += user_file_unittests.cc
+
+libdhcp_user_chk_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
+
+libdhcp_user_chk_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+
+libdhcp_user_chk_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+if USE_CLANGPP
+# This is to workaround unused variables tcout and tcerr in
+# log4cplus's streams.h and unused parameters from some of the
+# Boost headers.
+libdhcp_user_chk_unittests_CXXFLAGS += -Wno-unused-parameter
+endif
+
+libdhcp_user_chk_unittests_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libdhcp_user_chk_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+libdhcp_user_chk_unittests_LDADD += ${BOTAN_LIBS} ${BOTAN_RPATH}
+libdhcp_user_chk_unittests_LDADD += $(GTEST_LDADD)
+endif
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/hooks/dhcp/user_chk/tests/run_unittests.cc b/src/hooks/dhcp/user_chk/tests/run_unittests.cc
new file mode 100644
index 0000000..185f888
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/run_unittests.cc
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/logger_support.h>
+
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+ ::testing::InitGoogleTest(&argc, argv);
+ isc::log::initLogger();
+
+ int result = RUN_ALL_TESTS();
+
+ return (result);
+}
diff --git a/src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in b/src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in
new file mode 100644
index 0000000..9abdbc6
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/test_data_files_config.h.in
@@ -0,0 +1,16 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+//
+/// @brief Path to local dir so tests can locate test data files
+#define USER_CHK_TEST_DIR "@abs_top_srcdir@/src/hooks/dhcp/user_chk/tests"
diff --git a/src/hooks/dhcp/user_chk/tests/test_users_1.txt b/src/hooks/dhcp/user_chk/tests/test_users_1.txt
new file mode 100644
index 0000000..20ee232
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/test_users_1.txt
@@ -0,0 +1,4 @@
+{ "type" : "HW_ADDR", "id" : "01AC00F03344", "opt1" : "true" }
+{ "type" : "HW_ADDR", "id" : "01:AC:00:F0:33:45", "opt1" : "true" }
+{ "type" : "DUID", "id" : "225060de0a0b", "opt1" : "true" }
+{ "type" : "DUID", "id" : "22:50:60:de:0a:0c", "opt1" : "true" }
diff --git a/src/hooks/dhcp/user_chk/tests/test_users_err.txt b/src/hooks/dhcp/user_chk/tests/test_users_err.txt
new file mode 100644
index 0000000..3204006
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/test_users_err.txt
@@ -0,0 +1,2 @@
+{ "type" : "DUID", "id" : "777777777777", "opt1" : "true" }
+{ "type" : "BOGUS", "id" : "01AC00F03344", "opt1" : "true" }
diff --git a/src/hooks/dhcp/user_chk/tests/user_file_unittests.cc b/src/hooks/dhcp/user_chk/tests/user_file_unittests.cc
new file mode 100644
index 0000000..bc98c9a
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/user_file_unittests.cc
@@ -0,0 +1,171 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <test_data_files_config.h>
+#include <user_file.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace user_chk;
+
+namespace {
+
+/// @brief Convenience method for reliably building test file path names.
+///
+/// Function prefixes the given file name with a path to unit tests directory
+/// so we can reliably find test data files.
+///
+/// @param name base file name of the test file
+std::string testFilePath(const std::string& name) {
+ return (std::string(USER_CHK_TEST_DIR) + "/" + name);
+}
+
+/// @brief Tests the UserFile constructor.
+TEST(UserFile, construction) {
+ // Verify that a UserFile with no file name is rejected.
+ ASSERT_THROW(UserFile(""), UserFileError);
+
+ // Verify that a UserFile with a non-blank file name is accepted.
+ ASSERT_NO_THROW(UserFile("someName"));
+}
+
+/// @brief Tests opening and closing UserFile
+TEST(UserFile, openFile) {
+ UserFilePtr user_file;
+
+ // Construct a user file that refers to a non existant file.
+ ASSERT_NO_THROW(user_file.reset(new UserFile("NoSuchFile")));
+ EXPECT_FALSE(user_file->isOpen());
+
+ // Verify a non-existant file fails to open
+ ASSERT_THROW(user_file->open(), UserFileError);
+ EXPECT_FALSE(user_file->isOpen());
+
+ // Construct a user file that should exist.
+ ASSERT_NO_THROW(user_file.reset(new UserFile
+ (testFilePath("test_users_1.txt"))));
+ // File should not be open.
+ EXPECT_FALSE(user_file->isOpen());
+
+ // Verify that we can open it.
+ ASSERT_NO_THROW(user_file->open());
+ EXPECT_TRUE(user_file->isOpen());
+
+ // Verify that we cannot open an already open file.
+ ASSERT_THROW(user_file->open(), UserFileError);
+
+ // Verifyt we can close it.
+ ASSERT_NO_THROW(user_file->close());
+ EXPECT_FALSE(user_file->isOpen());
+
+ // Verify that we can reopen it.
+ ASSERT_NO_THROW(user_file->open());
+ EXPECT_TRUE(user_file->isOpen());
+}
+
+
+/// @brief Tests makeUser with invalid user strings
+TEST(UserFile, makeUser) {
+ const char* invalid_strs[]= {
+ // Missinge type element.
+ "{ \"id\" : \"01AC00F03344\" }",
+ // Invalid id type string value.
+ "{ \"type\" : \"BOGUS\", \"id\" : \"01AC00F03344\"}",
+ // Non-string id type
+ "{ \"type\" : 1, \"id\" : \"01AC00F03344\"}",
+ // Missing id element.
+ "{ \"type\" : \"HW_ADDR\" }",
+ // Odd number of digits in id value.
+ "{ \"type\" : \"HW_ADDR\", \"id\" : \"1AC00F03344\"}",
+ // Invalid characters in id value.
+ "{ \"type\" : \"HW_ADDR\", \"id\" : \"THIS IS BAD!\"}",
+ // Empty id value.
+ "{ \"type\" : \"HW_ADDR\", \"id\" : \"\"}",
+ // Non-string id.
+ "{ \"type\" : \"HW_ADDR\", \"id\" : 01AC00F03344 }",
+ // Option with non-string value
+ "{ \"type\" : \"HW_ADDR\", \"id\" : \"01AC00F03344\", \"opt\" : 4 }",
+ NULL
+ };
+
+ // Create a UseFile to work with.
+ UserFilePtr user_file;
+ ASSERT_NO_THROW(user_file.reset(new UserFile("noFile")));
+
+ // Iterate over the list of invalid user strings and verify
+ // each one fails.
+ const char** tmp = invalid_strs;;
+ while (*tmp) {
+ EXPECT_THROW(user_file->makeUser(*tmp), UserFileError)
+ << "Invalid str not caught: ["
+ << *tmp << "]" << std::endl;
+ ++tmp;
+ }
+}
+
+/// @brief Test reading from UserFile
+TEST(UserFile, readFile) {
+ UserFilePtr user_file;
+
+ // Construct and then open a known file.
+ ASSERT_NO_THROW(user_file.reset(new UserFile
+ (testFilePath("test_users_1.txt"))));
+ ASSERT_NO_THROW(user_file->open());
+ EXPECT_TRUE(user_file->isOpen());
+
+ // File should contain four valid users. Read and verify each.
+ UserPtr user;
+ int i = 0;
+ do {
+ ASSERT_NO_THROW(user = user_file->readNextUser());
+ switch (i++) {
+ case 0:
+ EXPECT_EQ(UserId::HW_ADDRESS, user->getUserId().getType());
+ EXPECT_EQ("01ac00f03344", user->getUserId().toText());
+ EXPECT_EQ("true", user->getProperty("opt1"));
+ break;
+ case 1:
+ // File entry should have colons in id.
+ EXPECT_EQ(UserId::HW_ADDRESS, user->getUserId().getType());
+ EXPECT_EQ("01ac00f03345", user->getUserId().toText());
+ EXPECT_EQ("true", user->getProperty("opt1"));
+ break;
+ case 2:
+ EXPECT_EQ(UserId::DUID, user->getUserId().getType());
+ EXPECT_EQ("225060de0a0b", user->getUserId().toText());
+ EXPECT_EQ("true", user->getProperty("opt1"));
+ break;
+ case 3:
+ // File entry should have colons in id.
+ EXPECT_EQ(UserId::DUID, user->getUserId().getType());
+ EXPECT_EQ("225060de0a0c", user->getUserId().toText());
+ EXPECT_EQ("true", user->getProperty("opt1"));
+ break;
+ default:
+ // Third time around, we are at EOF User should be null.
+ ASSERT_FALSE(user);
+ break;
+ }
+ } while (user);
+
+
+ ASSERT_NO_THROW(user_file->close());
+}
+
+} // end of anonymous namespace
diff --git a/src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc b/src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc
new file mode 100644
index 0000000..44e87c1
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/user_registry_unittests.cc
@@ -0,0 +1,217 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/hwaddr.h>
+#include <exceptions/exceptions.h>
+#include <user_registry.h>
+#include <user_file.h>
+#include <test_data_files_config.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace user_chk;
+
+namespace {
+
+/// @brief Convenience method for reliably building test file path names.
+///
+/// Function prefixes the given file name with a path to unit tests directory
+/// so we can reliably find test data files.
+///
+/// @param name base file name of the test file
+std::string testFilePath(const std::string& name) {
+ return (std::string(USER_CHK_TEST_DIR) + "/" + name);
+}
+
+/// @brief Tests UserRegistry construction.
+TEST(UserRegistry, constructor) {
+ // Currently there is only the default constructor which does not throw.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+}
+
+/// @brief Tests mechanics of adding, finding, removing Users.
+TEST(UserRegistry, userBasics) {
+ // Create an empty registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ // Verify that a blank user cannot be added.
+ UserPtr user;
+ ASSERT_THROW(reg->addUser(user), UserRegistryError);
+
+ // Make a new id and user.
+ UserIdPtr id;
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, "01010101")));
+ ASSERT_NO_THROW(user.reset(new User(*id)));
+
+ // Verify that we can add a user.
+ ASSERT_NO_THROW(reg->addUser(user));
+
+ // Verify that the user can be found.
+ UserPtr found_user;
+ ASSERT_NO_THROW(found_user = reg->findUser(*id));
+ EXPECT_TRUE(found_user);
+ EXPECT_EQ(found_user->getUserId(), *id);
+
+ // Verify that searching for a non-existant user returns empty user pointer.
+ UserIdPtr id2;
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS, "02020202")));
+ ASSERT_NO_THROW(found_user = reg->findUser(*id2));
+ EXPECT_FALSE(found_user);
+
+ // Verify that the user can be deleted.
+ ASSERT_NO_THROW(reg->removeUser(*id));
+ ASSERT_NO_THROW(found_user = reg->findUser(*id));
+ EXPECT_FALSE(found_user);
+}
+
+/// @brief Tests finding users by isc::dhcp::HWaddr instance.
+TEST(UserRegistry, findByHWAddr) {
+ // Create the registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ // Make a new user and add it.
+ UserPtr user;
+ ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, "01010101")));
+ ASSERT_NO_THROW(reg->addUser(user));
+
+ // Make a HWAddr instance using the same id value.
+ isc::dhcp::HWAddr hwaddr(user->getUserId().getId(), isc::dhcp::HTYPE_ETHER);
+
+ // Verify user can be found by HWAddr.
+ UserPtr found_user;
+ ASSERT_NO_THROW(found_user = reg->findUser(hwaddr));
+ EXPECT_TRUE(found_user);
+ EXPECT_EQ(found_user->getUserId(), user->getUserId());
+}
+
+/// @brief Tests finding users by isc::dhcp::DUID instance.
+TEST(UserRegistry, findByDUID) {
+ // Create the registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ // Make a new user and add it.
+ UserPtr user;
+ ASSERT_NO_THROW(user.reset(new User(UserId::DUID, "01010101")));
+ ASSERT_NO_THROW(reg->addUser(user));
+
+ // Make a DUID instance using the same id value.
+ isc::dhcp::DUID duid(user->getUserId().getId());
+
+ // Verify user can be found by DUID.
+ UserPtr found_user;
+ ASSERT_NO_THROW(found_user = reg->findUser(duid));
+ EXPECT_TRUE(found_user);
+ EXPECT_EQ(found_user->getUserId(), user->getUserId());
+}
+
+/// @brief Tests mixing users of different types.
+TEST(UserRegistry, oneOfEach) {
+ // Create the registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ // Make user ids.
+ UserIdPtr idh, idd;
+ ASSERT_NO_THROW(idh.reset(new UserId(UserId::HW_ADDRESS, "01010101")));
+ ASSERT_NO_THROW(idd.reset(new UserId(UserId::DUID, "03030303")));
+
+ // Make and add HW_ADDRESS user.
+ UserPtr user;
+ user.reset(new User(*idh));
+ ASSERT_NO_THROW(reg->addUser(user));
+
+ // Make and add DUID user.
+ user.reset(new User(*idd));
+ ASSERT_NO_THROW(reg->addUser(user));
+
+ // Verify we can find both.
+ UserPtr found_user;
+ ASSERT_NO_THROW(found_user = reg->findUser(*idh));
+ ASSERT_NO_THROW(found_user = reg->findUser(*idd));
+}
+
+/// @brief Tests loading the registry from a file.
+TEST(UserRegistry, refreshFromFile) {
+ // Create the registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ UserDataSourcePtr user_file;
+
+ // Verify that data source cannot be set to null source.
+ ASSERT_THROW(reg->setSource(user_file), UserRegistryError);
+
+ // Create the data source.
+ ASSERT_NO_THROW(user_file.reset(new UserFile
+ (testFilePath("test_users_1.txt"))));
+
+ // Set the registry's data source and refresh the registry.
+ ASSERT_NO_THROW(reg->setSource(user_file));
+ ASSERT_NO_THROW(reg->refresh());
+
+ // Verify we can find all the expected users.
+ UserIdPtr id;
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, "01ac00f03344")));
+ EXPECT_TRUE(reg->findUser(*id));
+
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::DUID, "225060de0a0b")));
+ EXPECT_TRUE(reg->findUser(*id));
+}
+
+/// @brief Tests preservation of registry upon refresh failure.
+TEST(UserRegistry, refreshFail) {
+ // Create the registry.
+ UserRegistryPtr reg;
+ ASSERT_NO_THROW(reg.reset(new UserRegistry()));
+
+ // Create the data source.
+ UserDataSourcePtr user_file;
+ ASSERT_NO_THROW(user_file.reset(new UserFile
+ (testFilePath("test_users_1.txt"))));
+
+ // Set the registry's data source and refresh the registry.
+ ASSERT_NO_THROW(reg->setSource(user_file));
+ ASSERT_NO_THROW(reg->refresh());
+
+ // Make user ids of expected users.
+ UserIdPtr id1, id2;
+ ASSERT_NO_THROW(id1.reset(new UserId(UserId::HW_ADDRESS, "01ac00f03344")));
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "225060de0a0b")));
+
+ // Verify we can find all the expected users.
+ EXPECT_TRUE(reg->findUser(*id1));
+ EXPECT_TRUE(reg->findUser(*id2));
+
+ // Replace original data source with a new one containing an invalid entry.
+ ASSERT_NO_THROW(user_file.reset(new UserFile
+ (testFilePath("test_users_err.txt"))));
+ ASSERT_NO_THROW(reg->setSource(user_file));
+
+ // Refresh should throw due to invalid data.
+ EXPECT_THROW(reg->refresh(), UserRegistryError);
+
+ // Verify we can still find all the original users.
+ EXPECT_TRUE(reg->findUser(*id1));
+ EXPECT_TRUE(reg->findUser(*id2));
+}
+
+} // end of anonymous namespace
diff --git a/src/hooks/dhcp/user_chk/tests/user_unittests.cc b/src/hooks/dhcp/user_chk/tests/user_unittests.cc
new file mode 100644
index 0000000..8d76732
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/user_unittests.cc
@@ -0,0 +1,97 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <user.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace user_chk;
+
+namespace {
+
+/// @brief Tests User construction variants.
+/// Note, since all constructors accept or rely on UserId, invalid id
+/// variants are tested under UserId not here.
+TEST(UserTest, construction) {
+ std::string test_address("01FF02AC030B0709");
+
+ // Create a user id.
+ UserIdPtr id;
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, test_address)));
+
+ // Verify construction from a UserId.
+ UserPtr user;
+ ASSERT_NO_THROW(user.reset(new User(*id)));
+
+ // Verify construction from an id type and an address vector.
+ ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, id->getId())));
+ ASSERT_NO_THROW(user.reset(new User(UserId::DUID, id->getId())));
+
+ // Verify construction from an id type and an address string.
+ ASSERT_NO_THROW(user.reset(new User(UserId::HW_ADDRESS, test_address)));
+ ASSERT_NO_THROW(user.reset(new User(UserId::DUID, test_address)));
+}
+
+/// @brief Tests property map fundamentals.
+TEST(UserTest, properties) {
+ // Create a user.
+ UserPtr user;
+ ASSERT_NO_THROW(user.reset(new User(UserId::DUID, "01020304050607")));
+
+ // Verify that we can add and retrieve a property.
+ std::string value = "";
+ EXPECT_NO_THROW(user->setProperty("one","1"));
+ EXPECT_NO_THROW(value = user->getProperty("one"));
+ EXPECT_EQ("1", value);
+
+ // Verify that we can update and then retrieve the property.
+ EXPECT_NO_THROW(user->setProperty("one","1.0"));
+ EXPECT_NO_THROW(value = user->getProperty("one"));
+ EXPECT_EQ("1.0", value);
+
+ // Verify that we can remove and then NOT find the property.
+ EXPECT_NO_THROW(user->delProperty("one"));
+ EXPECT_NO_THROW(value = user->getProperty("one"));
+ EXPECT_TRUE(value.empty());
+
+ // Verify that a blank property name is NOT permitted.
+ EXPECT_THROW(user->setProperty("", "blah"), isc::BadValue);
+
+ // Verify that a blank property value IS permitted.
+ EXPECT_NO_THROW(user->setProperty("one", ""));
+ EXPECT_NO_THROW(value = user->getProperty("one"));
+ EXPECT_TRUE(value.empty());
+
+ PropertyMap map;
+ map["one"]="1.0";
+ map["two"]="2.0";
+
+ ASSERT_NO_THROW(user->setProperties(map));
+
+ EXPECT_NO_THROW(value = user->getProperty("one"));
+ EXPECT_EQ("1.0", value);
+
+ EXPECT_NO_THROW(value = user->getProperty("two"));
+ EXPECT_EQ("2.0", value);
+
+ const PropertyMap& map2 = user->getProperties();
+ EXPECT_TRUE(map2 == map);
+}
+
+} // end of anonymous namespace
diff --git a/src/hooks/dhcp/user_chk/tests/userid_unittests.cc b/src/hooks/dhcp/user_chk/tests/userid_unittests.cc
new file mode 100644
index 0000000..cc96353
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/tests/userid_unittests.cc
@@ -0,0 +1,149 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <user.h>
+
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace user_chk;
+
+namespace {
+
+/// @brief Test invalid constructors.
+TEST(UserIdTest, invalidConstructors) {
+ // Verify that constructor does not allow empty id vector.
+ std::vector<uint8_t> empty_bytes;
+ ASSERT_THROW(UserId(UserId::HW_ADDRESS, empty_bytes), isc::BadValue);
+ ASSERT_THROW(UserId(UserId::DUID, empty_bytes), isc::BadValue);
+
+ // Verify that constructor does not allow empty id string.
+ ASSERT_THROW(UserId(UserId::HW_ADDRESS, ""), isc::BadValue);
+ ASSERT_THROW(UserId(UserId::DUID, ""), isc::BadValue);
+}
+
+/// @brief Test making and using HW_ADDRESS type UserIds
+TEST(UserIdTest, hwAddress_type) {
+ // Verify text label look up for HW_ADDRESS enum.
+ EXPECT_EQ(std::string(UserId::HW_ADDRESS_STR),
+ UserId::lookupTypeStr(UserId::HW_ADDRESS));
+
+ // Verify enum look up for HW_ADDRESS text label.
+ EXPECT_EQ(UserId::HW_ADDRESS,
+ UserId::lookupType(UserId::HW_ADDRESS_STR));
+
+ // Build a test address vector.
+ uint8_t tmp[] = { 0x01, 0xFF, 0x02, 0xAC, 0x03, 0x0B, 0x07, 0x08 };
+ std::vector<uint8_t> bytes(tmp, tmp + (sizeof(tmp)/sizeof(uint8_t)));
+
+ // Verify construction from an HW_ADDRESS id type and address vector.
+ UserIdPtr id;
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::HW_ADDRESS, bytes)));
+ // Verify that the id can be fetched.
+ EXPECT_EQ(id->getType(), UserId::HW_ADDRESS);
+ EXPECT_TRUE(bytes == id->getId());
+
+ // Check relational oeprators when a == b.
+ UserIdPtr id2;
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS, id->toText())));
+ EXPECT_TRUE(*id == *id2);
+ EXPECT_FALSE(*id != *id2);
+ EXPECT_FALSE(*id < *id2);
+
+ // Check relational oeprators when a < b.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS,
+ "01FF02AC030B0709")));
+ EXPECT_FALSE(*id == *id2);
+ EXPECT_TRUE(*id != *id2);
+ EXPECT_TRUE(*id < *id2);
+
+ // Check relational oeprators when a > b.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS,
+ "01FF02AC030B0707")));
+ EXPECT_FALSE(*id == *id2);
+ EXPECT_TRUE(*id != *id2);
+ EXPECT_FALSE(*id < *id2);
+
+ // Verify that colon delimiters are ok.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::HW_ADDRESS,
+ "01:FF:02:AC:03:0B:07:07")));
+ EXPECT_FALSE(*id == *id2);
+}
+
+/// @brief Test making and using DUID type UserIds
+TEST(UserIdTest, duid_type) {
+ // Verify text label look up for DUID enum.
+ EXPECT_EQ(std::string(UserId::DUID_STR),
+ UserId::lookupTypeStr(UserId::DUID));
+
+ // Verify enum look up for DUID text label.
+ EXPECT_EQ(UserId::DUID,
+ UserId::lookupType(UserId::DUID_STR));
+
+ // Build a test DUID vector.
+ uint8_t tmp[] = { 0x01, 0xFF, 0x02, 0xAC, 0x03, 0x0B, 0x07, 0x08 };
+ std::vector<uint8_t> bytes(tmp, tmp + (sizeof(tmp)/sizeof(uint8_t)));
+
+ // Verify construction from an DUID id type and address vector.
+ UserIdPtr id;
+ ASSERT_NO_THROW(id.reset(new UserId(UserId::DUID, bytes)));
+ // Verify that the id can be fetched.
+ EXPECT_EQ(id->getType(), UserId::DUID);
+ EXPECT_TRUE(bytes == id->getId());
+
+ // Check relational oeprators when a == b.
+ UserIdPtr id2;
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, id->toText())));
+ EXPECT_TRUE(*id == *id2);
+ EXPECT_FALSE(*id != *id2);
+ EXPECT_FALSE(*id < *id2);
+
+ // Check relational oeprators when a < b.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "01FF02AC030B0709")));
+ EXPECT_FALSE(*id == *id2);
+ EXPECT_TRUE(*id != *id2);
+ EXPECT_TRUE(*id < *id2);
+
+ // Check relational oeprators when a > b.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID, "01FF02AC030B0707")));
+ EXPECT_FALSE(*id == *id2);
+ EXPECT_TRUE(*id != *id2);
+ EXPECT_FALSE(*id < *id2);
+
+ // Verify that colon delimiters are ok.
+ ASSERT_NO_THROW(id2.reset(new UserId(UserId::DUID,
+ "01:FF:02:AC:03:0B:07:08")));
+ EXPECT_TRUE(*id == *id2);
+}
+
+/// @brief Tests that UserIds of different types compare correctly.
+TEST(UserIdTest, mixed_type_compare) {
+ UserIdPtr hw, duid;
+ // Create UserIds with different types, but same id data.
+ ASSERT_NO_THROW(hw.reset(new UserId(UserId::HW_ADDRESS,
+ "01FF02AC030B0709")));
+ ASSERT_NO_THROW(duid.reset(new UserId(UserId::DUID,
+ "01FF02AC030B0709")));
+
+ // Verify that UserIdType influences logical comparators.
+ EXPECT_FALSE(*hw == *duid);
+ EXPECT_TRUE(*hw != *duid);
+ EXPECT_TRUE(*hw < *duid);
+}
+
+} // end of anonymous namespace
diff --git a/src/hooks/dhcp/user_chk/user.cc b/src/hooks/dhcp/user_chk/user.cc
new file mode 100644
index 0000000..2bf95cf
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user.cc
@@ -0,0 +1,221 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/hwaddr.h>
+#include <dhcp/duid.h>
+#include <exceptions/exceptions.h>
+#include <util/encode/hex.h>
+
+#include <user.h>
+
+#include <iomanip>
+#include <sstream>
+
+namespace user_chk {
+
+//********************************* UserId ******************************
+
+const char* UserId::HW_ADDRESS_STR = "HW_ADDR";
+const char* UserId::DUID_STR = "DUID";
+
+UserId::UserId(UserIdType id_type, const std::vector<uint8_t>& id)
+ : id_type_(id_type), id_(id) {
+ if (id.size() == 0) {
+ isc_throw(isc::BadValue, "UserId id may not be blank");
+ }
+}
+
+UserId::UserId(UserIdType id_type, const std::string & id_str) :
+ id_type_(id_type) {
+ if (id_str.empty()) {
+ isc_throw(isc::BadValue, "UserId id string may not be blank");
+ }
+
+ // Convert the id string to vector.
+ // Input is expected to be 2-digits per bytes, no delimiters.
+ std::vector<uint8_t> addr_bytes;
+
+ // Strip out colon delimeters, decodeHex doesn't like them.
+ std::string clean_id_str = id_str;
+ std::string::iterator end_pos = std::remove(clean_id_str.begin(),
+ clean_id_str.end(), ':');
+ clean_id_str.erase(end_pos, clean_id_str.end());
+
+ isc::util::encode::decodeHex(clean_id_str, addr_bytes);
+
+ // Attempt to instantiate the appropriate id class to leverage validation.
+ switch (id_type) {
+ case HW_ADDRESS: {
+ isc::dhcp::HWAddr hwaddr(addr_bytes, isc::dhcp::HTYPE_ETHER);
+ break;
+ }
+ case DUID: {
+ isc::dhcp::DUID duid(addr_bytes);
+ break;
+ }
+ default:
+ isc_throw (isc::BadValue, "Invalid id_type: " << id_type);
+ break;
+ }
+
+ // It's a valid id.
+ id_ = addr_bytes;
+}
+
+UserId::~UserId() {
+}
+
+const std::vector<uint8_t>&
+UserId::getId() const {
+ return (id_);
+}
+
+UserId::UserIdType
+UserId::getType() const {
+ return (id_type_);
+}
+
+std::string
+UserId::toText(char delim_char) const {
+ std::stringstream tmp;
+ tmp << std::hex;
+ bool delim = false;
+ for (std::vector<uint8_t>::const_iterator it = id_.begin();
+ it != id_.end(); ++it) {
+ if (delim_char && delim) {
+ tmp << delim_char;
+ }
+
+ tmp << std::setw(2) << std::setfill('0')
+ << static_cast<unsigned int>(*it);
+ delim = true;
+ }
+
+ return (tmp.str());
+}
+
+bool
+UserId::operator ==(const UserId & other) const {
+ return ((this->id_type_ == other.id_type_) && (this->id_ == other.id_));
+}
+
+bool
+UserId::operator !=(const UserId & other) const {
+ return (!(*this == other));
+}
+
+bool
+UserId::operator <(const UserId & other) const {
+ return ((this->id_type_ < other.id_type_) ||
+ ((this->id_type_ == other.id_type_) && (this->id_ < other.id_)));
+}
+
+std::string
+UserId::lookupTypeStr(UserIdType type) {
+ const char* tmp = NULL;
+ switch (type) {
+ case HW_ADDRESS:
+ tmp = HW_ADDRESS_STR;
+ break;
+ case DUID:
+ tmp = DUID_STR;
+ break;
+ default:
+ isc_throw(isc::BadValue, "Invalid UserIdType:" << type);
+ break;
+ }
+
+ return (std::string(tmp));
+}
+
+UserId::UserIdType
+UserId::lookupType(const std::string& type_str) {
+ if (type_str.compare(HW_ADDRESS_STR) == 0) {
+ return (HW_ADDRESS);
+ } else if (type_str.compare(DUID_STR) == 0) {
+ return (DUID);
+ }
+
+ isc_throw(isc::BadValue, "Invalid UserIdType string:" << type_str);
+}
+
+std::ostream&
+operator<<(std::ostream& os, const UserId& user_id) {
+ std::string tmp = UserId::lookupTypeStr(user_id.getType());
+ os << tmp << "=" << user_id.toText();
+ return (os);
+}
+
+//********************************* User ******************************
+
+User::User(const UserId& user_id) : user_id_(user_id) {
+}
+
+User::User(UserId::UserIdType id_type, const std::vector<uint8_t>& id)
+ : user_id_(id_type, id) {
+}
+
+User::User(UserId::UserIdType id_type, const std::string& id_str)
+ : user_id_(id_type, id_str) {
+}
+
+User::~User() {
+}
+
+const PropertyMap&
+User::getProperties() const {
+ return (properties_);
+}
+
+void
+User::setProperties(const PropertyMap& properties) {
+ properties_ = properties;
+}
+
+void User::setProperty(const std::string& name, const std::string& value) {
+ if (name.empty()) {
+ isc_throw (isc::BadValue, "User property name cannot be blank");
+ }
+
+ // Note that if the property exists its value will be updated.
+ properties_[name]=value;
+}
+
+std::string
+User::getProperty(const std::string& name) const {
+ PropertyMap::const_iterator it = properties_.find(name);
+ if (it != properties_.end()) {
+ return ((*it).second);
+ }
+
+ // By returning an empty string rather than throwing, we allow the
+ // flexibility of defaulting to blank if not specified. Let the caller
+ // decide if that is valid or not.
+ return ("");
+}
+
+void
+User::delProperty(const std::string & name) {
+ PropertyMap::iterator it = properties_.find(name);
+ if (it != properties_.end()) {
+ properties_.erase(it);
+ }
+}
+
+const UserId&
+User::getUserId() const {
+ return (user_id_);
+}
+
+} // namespace user_chk
diff --git a/src/hooks/dhcp/user_chk/user.h b/src/hooks/dhcp/user_chk/user.h
new file mode 100644
index 0000000..b2d0f16
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user.h
@@ -0,0 +1,253 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef USER_H
+#define USER_H
+
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <stdint.h>
+#include <vector>
+
+namespace user_chk {
+
+/// @file user.h This file defines classes: UserId and User.
+/// @brief These classes are used to describe and recognize DHCP lease
+/// clients.
+
+/// @brief Encapsulates a unique identifier for a DHCP client.
+/// This class provides a generic wrapper around the information used to
+/// uniquely identify the requester in a DHCP request packet. It provides
+/// the necessary operators such that it can be used as a key within STL
+/// containers such as maps. It supports both IPv4 and IPv6 clients.
+class UserId {
+public:
+ /// @brief Defines the supported types of user ids.
+ // Use explicit values to ensure consistent numeric ordering for key
+ // comparisons.
+ enum UserIdType {
+ /// @brief Hardware addresses (MAC) are used for IPv4 clients.
+ HW_ADDRESS = 0,
+ /// @brief DUIDs are used for IPv6 clients.
+ DUID = 1
+ };
+
+ /// @brief Defines the text label hardware address id type.
+ static const char* HW_ADDRESS_STR;
+ /// @brief Define the text label DUID id type.
+ static const char* DUID_STR;
+
+ /// @brief Constructor
+ ///
+ /// Constructs a UserId from an id type and id vector.
+ ///
+ /// @param id_type The type of user id contained in vector
+ /// @param id a vector of unsigned bytes containing the id
+ ///
+ /// @throw isc::BadValue if the vector is empty.
+ UserId(UserIdType id_type, const std::vector<uint8_t>& id);
+
+ /// @brief Constructor
+ ///
+ /// Constructs a UserId from an id type and id string.
+ ///
+ /// @param id_type The type of user id contained in string.
+ /// The string is expected to contain an even number of hex digits
+ /// with or without colon (':') as a delimiter.
+ ///
+ /// @param id a vector of unsigned bytes containing the id
+ ///
+ /// @throw isc::BadValue if the string is empty, contains non
+ /// valid hex digits, or an odd number of hex digits.
+ UserId(UserIdType id_type, const std::string& id_str);
+
+ /// @brief Destructor.
+ ~UserId();
+
+ /// @brief Returns a const reference to the actual id value
+ const std::vector<uint8_t>& getId() const;
+
+ /// @brief Returns the user id type
+ UserIdType getType() const;
+
+ /// @brief Returns textual representation of the id
+ ///
+ /// Outputs a string of hex digits representing the id with an
+ /// optional delimiter between digit pairs (i.e. bytes).
+ ///
+ /// Without a delimiter:
+ /// "0c220F"
+ ///
+ /// with colon as a delimiter:
+ /// "0c:22:0F"
+ ///
+ /// @param delim_char The delimiter to place in between
+ /// "bytes". It defaults to none.
+ /// (e.g. 00010203ff)
+ std::string toText(char delim_char=0x0) const;
+
+ /// @brief Compares two UserIds for equality
+ bool operator ==(const UserId & other) const;
+
+ /// @brief Compares two UserIds for inequality
+ bool operator !=(const UserId & other) const;
+
+ /// @brief Performs less than comparison of two UserIds
+ bool operator <(const UserId & other) const;
+
+ /// @brief Returns the text label for a given id type
+ ///
+ /// @param type The id type value for which the label is desired
+ ///
+ /// @throw isc::BadValue if type is not valid.
+ static std::string lookupTypeStr(UserIdType type);
+
+ /// @brief Returns the id type for a given text label
+ ///
+ /// @param type_str The text label for which the id value is desired
+ ///
+ /// @throw isc::BadValue if type_str is not a valid text label.
+ static UserIdType lookupType(const std::string& type_str);
+
+private:
+ /// @brief The type of id value
+ UserIdType id_type_;
+
+ /// @brief The id value
+ std::vector<uint8_t> id_;
+
+};
+
+/// @brief Outputs the UserId contents in a string to the given stream.
+///
+/// The output string has the form "<type>=<id>" where:
+///
+/// <type> is the text label returned by UserId::lookupTypeStr()
+/// <id> is the output of UserId::toText() without a delimiter.
+///
+/// Examples:
+/// HW_ADDR=0c0e0a01ff06
+/// DUID=0001000119efe63b000c01020306
+///
+/// @param os output stream to which to write
+/// @param user_id source object to output
+std::ostream&
+operator<<(std::ostream& os, const UserId& user_id);
+
+/// @brief Defines a smart pointer to UserId
+typedef boost::shared_ptr<UserId> UserIdPtr;
+
+/// @brief Defines a map of string values keyed by string labels.
+typedef std::map<std::string, std::string> PropertyMap;
+
+/// @brief Represents a unique DHCP user
+/// This class is used to represent a specific DHCP user who is identified by a
+/// unique id and who possesses a set of properties.
+class User {
+public:
+ /// @brief Constructor
+ ///
+ /// Constructs a new User from a given id with an empty set of properties.
+ ///
+ /// @param user_id Id to assign to the user
+ ///
+ /// @throw isc::BadValue if user id is blank.
+ User(const UserId & user_id);
+
+ /// @brief Constructor
+ ///
+ /// Constructs a new User from a given id type and vector containing the
+ /// id data with an empty set of properties.
+ ///
+ /// @param user_id Type of id contained in the id vector
+ /// @param id Vector of data representing the user's id
+ ///
+ /// @throw isc::BadValue if user id vector is empty.
+ User(UserId::UserIdType id_type, const std::vector<uint8_t>& id);
+
+ /// @brief Constructor
+ ///
+ /// Constructs a new User from a given id type and string containing the
+ /// id data with an empty set of properties.
+ ///
+ /// @param user_id Type of id contained in the id vector
+ /// @param id string of hex digits representing the user's id
+ ///
+ /// @throw isc::BadValue if user id string is empty or invalid
+ User(UserId::UserIdType id_type, const std::string& id_str);
+
+ /// @brief Destructor
+ ~User();
+
+ /// @brief Returns a reference to the map of properties.
+ ///
+ /// Note that this reference can go out of scope and should not be
+ /// relied upon other than for momentary use.
+ const PropertyMap& getProperties() const;
+
+ /// @brief Sets the user's properties from a given property map
+ ///
+ /// Replaces the contents of the user's property map with the given
+ /// property map.
+ ///
+ /// @param properties property map to assign to the user
+ void setProperties(const PropertyMap& properties);
+
+ /// @brief Sets a given property to the given value
+ ///
+ /// Adds or updates the given property to the given value.
+ ///
+ /// @param name string by which the property is identified (keyed)
+ /// @param value string data to associate with the property
+ ///
+ /// @throw isc::BadValue if name is blank.
+ void setProperty(const std::string& name, const std::string& value);
+
+ /// @brief Fetches the string value for a given property name.
+ ///
+ /// @param name property name to fetch
+ ///
+ /// @return Returns the string value for the given name or an empty string
+ /// if the property is not found in the property map.
+ std::string getProperty(const std::string& name) const;
+
+ /// @brief Removes the given property from the property map.
+ ///
+ /// Removes the given property from the map if found. If not, no harm no
+ /// foul.
+ ///
+ /// @param name property name to remove
+ void delProperty(const std::string& name);
+
+ /// @brief Returns the user's id.
+ ///
+ /// Note that this reference can go out of scope and should not be
+ /// relied upon other than for momentary use.
+ const UserId& getUserId() const;
+
+private:
+ /// @brief The user's id.
+ UserId user_id_;
+
+ /// @brief The user's property map.
+ PropertyMap properties_;
+};
+
+/// @brief Defines a smart pointer to a User.
+typedef boost::shared_ptr<User> UserPtr;
+
+} // namespace user_chk
+
+#endif
diff --git a/src/hooks/dhcp/user_chk/user_chk.h b/src/hooks/dhcp/user_chk/user_chk.h
new file mode 100644
index 0000000..1bc2fc3
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_chk.h
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef USER_CHK_H
+#define USER_CHK_H
+
+#include <user_registry.h>
+#include <fstream>
+#include <string>
+
+using namespace std;
+using namespace user_chk;
+
+// The following constants are used throughout the library. They are defined
+// in load_unload.cc
+
+/// @brief Pointer to the registry instance.
+extern UserRegistryPtr user_registry;
+
+/// @brief Output filestream for recording user check outcomes.
+extern std::fstream user_chk_output;
+
+/// @brief User registry input file name.
+extern const char* registry_fname;
+
+/// @brief User check outcome file name.
+extern const char* user_chk_output_fname;
+
+/// @brief Text label of user id in the inbound query in callout context
+extern const char* query_user_id_label;
+
+/// @brief Text label of registered user pointer in callout context
+extern const char* registered_user_label;
+
+/// @brief Text id used to identify the default IPv4 user in the registry
+extern const char* default_user4_id_str;
+
+/// @brief Text id used to identify the default IPv6 user in the registry
+extern const char* default_user6_id_str;
+
+#endif
diff --git a/src/hooks/dhcp/user_chk/user_chk_log.cc b/src/hooks/dhcp/user_chk/user_chk_log.cc
new file mode 100644
index 0000000..7e4d87f
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_chk_log.cc
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// Defines the logger used by the user check hooks library.
+#include <user_chk_log.h>
+
+namespace user_chk {
+
+isc::log::Logger user_chk_logger("user_chk");
+
+} // namespace user_chk
diff --git a/src/hooks/dhcp/user_chk/user_chk_log.h b/src/hooks/dhcp/user_chk/user_chk_log.h
new file mode 100644
index 0000000..43fb364
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_chk_log.h
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef USER_CHK_LOG_H
+#define USER_CHK_LOG_H
+
+#include <log/message_initializer.h>
+#include <log/macros.h>
+#include <user_chk_messages.h>
+
+/// @brief User Check Logger
+///
+/// Define the logger used to log messages. We could define it in multiple
+/// modules, but defining in a single module and linking to it saves time and
+/// space.
+extern isc::log::Logger user_chk_logger;
+
+#endif // USER_CHK_LOG_H
diff --git a/src/hooks/dhcp/user_chk/user_chk_messages.mes b/src/hooks/dhcp/user_chk/user_chk_messages.mes
new file mode 100644
index 0000000..23e3023
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_chk_messages.mes
@@ -0,0 +1,43 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+% USER_CHK_HOOK_LOAD_ERROR DHCP UserCheckHook could not be loaded: %1
+This is an error message issued when the DHCP UserCheckHook could not be loaded.
+The exact cause should be explained in the log message. User subnet selection
+will revert to default processing.
+
+% USER_CHK_HOOK_UNLOAD_ERROR DHCP UserCheckHook an error occurred unloading the library: %1
+This is an error message issued when an error occurs while unloading the
+UserCheckHook library. This is unlikely to occur and normal operations of the
+library will likely resume when it is next loaded.
+
+% USER_CHK_SUBNET4_SELECT_ERROR DHCP UserCheckHook an unexpected error occured in subnet4_select callout: %1
+This is an error message issued when the DHCP UserCheckHook subnet4_select hook
+encounters an unexpected error. The message should contain a more detailed
+explanation.
+
+% USER_CHK_SUBNET4_SELECT_REGISTRY_NULL DHCP UserCheckHook UserRegistry has not been created.
+This is an error message issued when the DHCP UserCheckHook subnet4_select hook
+has been invoked but the UserRegistry has not been created. This is a
+programmatic error and should not occur.
+
+% USER_CHK_SUBNET6_SELECT_ERROR DHCP UserCheckHook an unexpected error occured in subnet6_select callout: %1
+This is an error message issued when the DHCP UserCheckHook subnet6_select hook
+encounters an unexpected error. The message should contain a more detailed
+explanation.
+
+% USER_CHK_SUBNET6_SELECT_REGISTRY_NULL DHCP UserCheckHook UserRegistry has not been created.
+This is an error message issued when the DHCP UserCheckHook subnet6_select hook
+has been invoked but the UserRegistry has not been created. This is a
+programmatic error and should not occur.
diff --git a/src/hooks/dhcp/user_chk/user_data_source.h b/src/hooks/dhcp/user_chk/user_data_source.h
new file mode 100644
index 0000000..5145e10
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_data_source.h
@@ -0,0 +1,79 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_DATA_SOURCE_H
+#define _USER_DATA_SOURCE_H
+
+/// @file user_data_source.h Defines the base class, UserDataSource.
+#include <exceptions/exceptions.h>
+#include <user.h>
+
+namespace user_chk {
+
+/// @brief Thrown if UserDataSource encounters an error
+class UserDataSourceError : public isc::Exception {
+public:
+ UserDataSourceError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines an interface for reading user data into a registry.
+/// This is an abstract class which defines the interface for reading Users
+/// from an IO source such as a file.
+class UserDataSource {
+public:
+ /// @brief Constructor.
+ UserDataSource() {};
+
+ /// @brief Virtual Destructor.
+ virtual ~UserDataSource() {};
+
+ /// @brief Opens the data source.
+ ///
+ /// Prepares the data source for reading. Upon successful completion the
+ /// data source is ready to read from the beginning of its content.
+ ///
+ /// @throw UserDataSourceError if the source fails to open.
+ virtual void open() = 0;
+
+ /// @brief Fetches the next user from the data source.
+ ///
+ /// Reads the next User from the data source and returns it. If no more
+ /// data is available it should return an empty (null) user.
+ ///
+ /// @throw UserDataSourceError if an error occurs.
+ virtual UserPtr readNextUser() = 0;
+
+ /// @brief Closes that data source.
+ ///
+ /// Closes the data source.
+ ///
+ /// This method must not throw exceptions.
+ virtual void close() = 0;
+
+ /// @brief Returns true if the data source is open.
+ ///
+ /// This method should return true once the data source has been
+ /// successfully opened and until it has been closed.
+ ///
+ /// It is assumed to be exception safe.
+ virtual bool isOpen() const = 0;
+};
+
+/// @brief Defines a smart pointer to a UserDataSource.
+typedef boost::shared_ptr<UserDataSource> UserDataSourcePtr;
+
+} // namespace user_chk
+
+#endif
diff --git a/src/hooks/dhcp/user_chk/user_file.cc b/src/hooks/dhcp/user_chk/user_file.cc
new file mode 100644
index 0000000..3ffc99e
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_file.cc
@@ -0,0 +1,162 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <cc/data.h>
+#include <user.h>
+#include <user_file.h>
+
+#include <boost/foreach.hpp>
+#include <errno.h>
+#include <iostream>
+
+namespace user_chk {
+
+UserFile::UserFile(const std::string& fname) : fname_(fname), file_() {
+ if (fname_.empty()) {
+ isc_throw(UserFileError, "file name cannot be blank");
+ }
+}
+
+UserFile::~UserFile(){
+ close();
+};
+
+void
+UserFile::open() {
+ if (isOpen()) {
+ isc_throw (UserFileError, "file is already open");
+ }
+
+ file_.open(fname_.c_str(), std::ifstream::in);
+ int sav_error = errno;
+ if (!file_.is_open()) {
+ isc_throw(UserFileError, "cannot open file:" << fname_
+ << " reason: " << strerror(sav_error));
+ }
+}
+
+UserPtr
+UserFile::readNextUser() {
+ if (!isOpen()) {
+ isc_throw (UserFileError, "cannot read, file is not open");
+ }
+
+ if (file_.good()) {
+ char buf[USER_ENTRY_MAX_LEN];
+
+ // Get the next line.
+ file_.getline(buf, sizeof(buf));
+
+ // We got something, try to make a user out of it.
+ if (file_.gcount() > 0) {
+ return(makeUser(buf));
+ }
+ }
+
+ // Returns an empty user on EOF.
+ return (UserPtr());
+}
+
+UserPtr
+UserFile::makeUser(const std::string& user_string) {
+ // This method leverages the existing JSON parsing provided by isc::data
+ // library. Should this prove to be a performance issue, it may be that
+ // lighter weight solution would be appropriate.
+
+ // Turn the string of JSON text into an Element set.
+ isc::data::ElementPtr elements;
+ try {
+ elements = isc::data::Element::fromJSON(user_string);
+ } catch (isc::data::JSONError& ex) {
+ isc_throw(UserFileError,
+ "UserFile entry is malformed JSON: " << ex.what());
+ }
+
+ // Get a map of the Elements, keyed by element name.
+ isc::data::ConstElementPtr element;
+ PropertyMap properties;
+ std::string id_type_str;
+ std::string id_str;
+
+ // Iterate over the elements, saving of "type" and "id" to their
+ // respective locals. Anything else is assumed to be an option so
+ // add it to the local property map.
+ std::pair<std::string, isc::data::ConstElementPtr> element_pair;
+ BOOST_FOREACH (element_pair, elements->mapValue()) {
+ // Get the element's label.
+ std::string label = element_pair.first;
+
+ // Currently everything must be a string.
+ if (element_pair.second->getType() != isc::data::Element::string) {
+ isc_throw (UserFileError, "UserFile entry: " << user_string
+ << "has non-string value for : " << label);
+ }
+
+ std::string value = element_pair.second->stringValue();
+
+ if (label == "type") {
+ id_type_str = value;
+ } else if (label == "id") {
+ id_str = value;
+ } else {
+ // JSON parsing reduces any duplicates to the last value parsed,
+ // so we will never see duplicates here.
+ properties[label]=value;
+ }
+ }
+
+ // First we attempt to translate the id type.
+ UserId::UserIdType id_type;
+ try {
+ id_type = UserId::lookupType(id_type_str);
+ } catch (const std::exception& ex) {
+ isc_throw (UserFileError, "UserFile entry has invalid type: "
+ << user_string << " " << ex.what());
+ }
+
+ // Id type is valid, so attempt to make the user based on that and
+ // the value we have for "id".
+ UserPtr user;
+ try {
+ user.reset(new User(id_type, id_str));
+ } catch (const std::exception& ex) {
+ isc_throw (UserFileError, "UserFile cannot create user form entry: "
+ << user_string << " " << ex.what());
+ }
+
+ // We have a new User, so add in the properties and return it.
+ user->setProperties(properties);
+ return (user);
+}
+
+bool
+UserFile::isOpen() const {
+ return (file_.is_open());
+}
+
+void
+UserFile::close() {
+ try {
+ if (file_.is_open()) {
+ file_.close();
+ }
+ } catch (const std::exception& ex) {
+ // Highly unlikely to occur but let's at least spit out an error.
+ // Beyond that we swallow it for tidiness.
+ std::cout << "UserFile unexpected error closing the file: "
+ << fname_ << " : " << ex.what() << std::endl;
+ }
+}
+
+} // namespace user_chk
diff --git a/src/hooks/dhcp/user_chk/user_file.h b/src/hooks/dhcp/user_chk/user_file.h
new file mode 100644
index 0000000..873dddf
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_file.h
@@ -0,0 +1,140 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_FILE_H
+#define _USER_FILE_H
+
+/// @file user_file.h Defines the class, UserFile, which implements the UserDataSource interface for text files.
+
+#include <user_data_source.h>
+#include <user.h>
+
+#include <boost/shared_ptr.hpp>
+#include <fstream>
+#include <string>
+
+using namespace std;
+
+namespace user_chk {
+
+/// @brief Thrown a UserFile encounters an error.
+/// Note that it derives from UserDataSourceError to comply with the interface.
+class UserFileError : public UserDataSourceError {
+public:
+ UserFileError(const char* file, size_t line,
+ const char* what) :
+ UserDataSourceError(file, line, what) { };
+};
+
+/// @brief Provides a UserDataSource implementation for JSON text files.
+/// This class allows a text file of JSON entries to be treated as a source of
+/// User entries. The format of the file is one user entry per line, where
+/// each line contains a JSON string as follows:
+///
+/// { "type" : "<user type>", "id" : "<user_id>" (options) }
+///
+/// where:
+///
+/// <user_type> text label of the id type: "HW_ADDR" or "DUID"
+/// <user_id> the user's id as a string of hex digits with or without
+/// colons (':') as a delimiter
+/// (options) zero or more string elements as name-value pairs, separated by
+/// commas: "opt1" : "val1", "other_opt", "77" ...
+///
+/// Each entry must have a valid entry for "type" and a valid entry or "id".
+///
+/// If an entry contains duplicate option names, that option will be assigend
+/// the last value found. This is typical JSON behavior.
+/// Currently, only string option values (i.e. enclosed in quotes) are
+/// supported.
+///
+/// Example file entries might look like this:
+/// @code
+///
+/// { "type" : "HW_ADDR", "id" : "01AC00F03344", "opt1" : "true" }
+/// { "type" : "DUID", "id" : "225060de0a0b", "opt1" : "false" }
+///
+/// @endcode
+class UserFile : public UserDataSource {
+public:
+ /// @brief Maximum length of a single user entry.
+ /// This value is somewhat arbitrary. 4K seems reasonably large. If it
+ /// goes beyond this, then a flat file is not likely the way to go.
+ static const size_t USER_ENTRY_MAX_LEN = 4096;
+
+ /// @brief Constructor
+ ///
+ /// Create a UserFile for the given file name without opening the file.
+ /// @param fname pathname to the input file.
+ ///
+ /// @throw UserFileError if given file name is empty.
+ UserFile(const std::string& fname);
+
+ /// @brief Destructor.
+ ////
+ /// The destructor does call the close method.
+ virtual ~UserFile();
+
+ /// @brief Opens the input file for reading.
+ ///
+ /// Upon successful completion, the file is opened and positioned to start
+ /// reading from the beginning of the file.
+ ///
+ /// @throw UserFileError if the file cannot be opened.
+ virtual void open();
+
+ /// @brief Fetches the next user from the file.
+ ///
+ /// Reads the next user entry from the file and attempts to create a
+ /// new User from the text therein. If there is no more data to be read
+ /// it returns an empty UserPtr.
+ ///
+ /// @return A UserPtr pointing to the new User or an empty pointer on EOF.
+ ///
+ /// @throw UserFileError if an error occurs while reading.
+ virtual UserPtr readNextUser();
+
+ /// @brief Closes the underlying file.
+ ///
+ /// Method is exception safe.
+ virtual void close();
+
+ /// @brief Returns true if the file is open.
+ ///
+ /// @return True if the underlying file is open, false otherwise.
+ virtual bool isOpen() const;
+
+ /// @brief Creates a new User instance from JSON text.
+ ///
+ /// @param user_string string the JSON text for a user entry.
+ ///
+ /// @return A pointer to the newly created User instance.
+ ///
+ /// @throw UserFileError if the entry is invalid.
+ UserPtr makeUser(const std::string& user_string);
+
+private:
+ /// @brief Pathname of the input text file.
+ string fname_;
+
+ /// @brief Input file stream.
+ std::ifstream file_;
+
+};
+
+/// @brief Defines a smart pointer to a UserFile.
+typedef boost::shared_ptr<UserFile> UserFilePtr;
+
+} // namespace user_chk
+
+#endif
diff --git a/src/hooks/dhcp/user_chk/user_registry.cc b/src/hooks/dhcp/user_chk/user_registry.cc
new file mode 100644
index 0000000..80e7a93
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_registry.cc
@@ -0,0 +1,125 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <user_registry.h>
+#include <user.h>
+
+namespace user_chk {
+
+UserRegistry::UserRegistry() {
+}
+
+UserRegistry::~UserRegistry(){
+}
+
+void
+UserRegistry::addUser(UserPtr& user) {
+ if (!user) {
+ isc_throw (UserRegistryError, "UserRegistry cannot add blank user");
+ }
+
+ UserPtr found_user;
+ if ((found_user = findUser(user->getUserId()))) {
+ isc_throw (UserRegistryError, "UserRegistry duplicate user: "
+ << user->getUserId());
+ }
+
+ users_[user->getUserId()] = user;
+}
+
+const UserPtr&
+UserRegistry::findUser(const UserId& id) const {
+ static UserPtr empty;
+ UserMap::const_iterator it = users_.find(id);
+ if (it != users_.end()) {
+ const UserPtr tmp = (*it).second;
+ return ((*it).second);
+ }
+
+ return empty;
+}
+
+void
+UserRegistry::removeUser(const UserId& id) {
+ static UserPtr empty;
+ UserMap::iterator it = users_.find(id);
+ if (it != users_.end()) {
+ users_.erase(it);
+ }
+}
+
+const UserPtr&
+UserRegistry::findUser(const isc::dhcp::HWAddr& hwaddr) const {
+ UserId id(UserId::HW_ADDRESS, hwaddr.hwaddr_);
+ return (findUser(id));
+}
+
+const UserPtr&
+UserRegistry::findUser(const isc::dhcp::DUID& duid) const {
+ UserId id(UserId::DUID, duid.getDuid());
+ return (findUser(id));
+}
+
+void UserRegistry::refresh() {
+ if (!source_) {
+ isc_throw(UserRegistryError,
+ "UserRegistry: cannot refresh, no data source");
+ }
+
+ // If the source isn't open, open it.
+ if (!source_->isOpen()) {
+ source_->open();
+ }
+
+ // Make a copy in case something goes wrong midstream.
+ UserMap backup(users_);
+
+ // Empty the registry then read users from source until source is empty.
+ clearall();
+ try {
+ UserPtr user;
+ while ((user = source_->readNextUser())) {
+ addUser(user);
+ }
+ } catch (const std::exception& ex) {
+ // Source was compromsised so restore registry from backup.
+ users_ = backup;
+ // Close the source.
+ source_->close();
+ isc_throw (UserRegistryError, "UserRegistry: refresh failed during read"
+ << ex.what());
+ }
+
+ // Close the source.
+ source_->close();
+}
+
+void UserRegistry::clearall() {
+ users_.clear();
+}
+
+void UserRegistry::setSource(UserDataSourcePtr& source) {
+ if (!source) {
+ isc_throw (UserRegistryError,
+ "UserRegistry: data source cannot be set to null");
+ }
+
+ source_ = source;
+}
+
+const UserDataSourcePtr& UserRegistry::getSource() {
+ return (source_);
+}
+
+} // namespace user_chk
diff --git a/src/hooks/dhcp/user_chk/user_registry.h b/src/hooks/dhcp/user_chk/user_registry.h
new file mode 100644
index 0000000..48dc32c
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/user_registry.h
@@ -0,0 +1,132 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+#ifndef _USER_REGISTRY_H
+#define _USER_REGISTRY_H
+
+/// @file user_registry.h Defines the class, UserRegistry.
+
+#include <dhcp/hwaddr.h>
+#include <dhcp/duid.h>
+#include <exceptions/exceptions.h>
+#include <user.h>
+#include <user_data_source.h>
+
+#include <string>
+
+using namespace std;
+
+namespace user_chk {
+
+/// @brief Thrown UserRegistry encounters an error
+class UserRegistryError : public isc::Exception {
+public:
+ UserRegistryError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Defines a map of unique Users keyed by UserId.
+typedef std::map<UserId,UserPtr> UserMap;
+
+/// @brief Embodies an update-able, searchable list of unique users
+/// This class provides the means to create and maintain a searchable list
+/// of unique users. List entries are pointers to instances of User, keyed
+/// by their UserIds.
+/// Users may be added and removed from the list individually or the list
+/// may be updated by loading it from a data source, such as a file.
+class UserRegistry {
+public:
+ /// @brief Constructor
+ ///
+ /// Creates a new registry with an empty list of users and no data source.
+ UserRegistry();
+
+ /// @brief Destructor
+ ~UserRegistry();
+
+ /// @brief Adds a given user to the registry.
+ ///
+ /// @param user A pointer to the user to add
+ ///
+ /// @throw UserRegistryError if the user is null or if the user already
+ /// exists in the registry.
+ void addUser(UserPtr& user);
+
+ /// @brief Finds a user in the registry by user id
+ ///
+ /// @param id The user id for which to search
+ ///
+ /// @return A pointer to the user if found or an null pointer if not.
+ const UserPtr& findUser(const UserId& id) const;
+
+ /// @brief Removes a user from the registry by user id
+ ///
+ /// Removes the user entry if found, if not simply return.
+ ///
+ /// @param id The user id of the user to remove
+ void removeUser(const UserId& id);
+
+ /// @brief Finds a user in the registry by hardware address
+ ///
+ /// @param hwaddr The hardware address for which to search
+ ///
+ /// @return A pointer to the user if found or an null pointer if not.
+ const UserPtr& findUser(const isc::dhcp::HWAddr& hwaddr) const;
+
+ /// @brief Finds a user in the registry by DUID
+ ///
+ /// @param duid The DUID for which to search
+ ///
+ /// @return A pointer to the user if found or an null pointer if not.
+ const UserPtr& findUser(const isc::dhcp::DUID& duid) const;
+
+ /// @brief Updates the registry from its data source.
+ ///
+ /// This method will replace the contents of the registry with new content
+ /// read from its data source. It will attempt to open the source and
+ /// then add users from the source to the registry until the source is
+ /// exhausted. If an error occurs accessing the source the registry
+ /// contents will be restored to that of before the call to refresh.
+ ///
+ /// @throw UserRegistryError if the data source has not been set (is null)
+ /// or if an error occurs accessing the data source.
+ void refresh();
+
+ /// @brief Removes all entries from the registry.
+ void clearall();
+
+ /// @brief Returns a reference to the data source.
+ const UserDataSourcePtr& getSource();
+
+ /// @brief Sets the data source to the given value.
+ ///
+ /// @param source reference to the data source to use.
+ ///
+ /// @throw UserRegistryError if new source value is null.
+ void setSource(UserDataSourcePtr& source);
+
+private:
+ /// @brief The registry of users.
+ UserMap users_;
+
+ /// @brief The current data source of users.
+ UserDataSourcePtr source_;
+};
+
+/// @brief Define a smart pointer to a UserRegistry.
+typedef boost::shared_ptr<UserRegistry> UserRegistryPtr;
+
+} // namespace user_chk
+
+#endif
diff --git a/src/hooks/dhcp/user_chk/version.cc b/src/hooks/dhcp/user_chk/version.cc
new file mode 100644
index 0000000..eaa9d50
--- /dev/null
+++ b/src/hooks/dhcp/user_chk/version.cc
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+// version.cc
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+/// @brief Version function required by Hooks API for compatibility checks.
+int version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+}
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
index f636c0d..59cb8e1 100644
--- a/src/lib/Makefile.am
+++ b/src/lib/Makefile.am
@@ -1,3 +1,8 @@
-SUBDIRS = exceptions util log cryptolink dns cc config acl xfr bench \
- asiolink asiodns nsas cache resolve testutils datasrc \
- server_common python dhcp dhcpsrv statistics
+if BUILD_EXPERIMENTAL_RESOLVER
+# Build resolver only with --enable-experimental-resolver
+experimental_resolver = resolve
+endif
+
+SUBDIRS = exceptions util log hooks cryptolink dns cc config acl xfr bench \
+ asiolink asiodns nsas cache $(experimental_resolver) testutils \
+ datasrc server_common python dhcp dhcp_ddns dhcpsrv statistics
diff --git a/src/lib/asiodns/.gitignore b/src/lib/asiodns/.gitignore
index dedf17e..189824e 100644
--- a/src/lib/asiodns/.gitignore
+++ b/src/lib/asiodns/.gitignore
@@ -1,2 +1,3 @@
/asiodns_messages.cc
/asiodns_messages.h
+/s-messages
diff --git a/src/lib/asiodns/Makefile.am b/src/lib/asiodns/Makefile.am
index 321de8b..dab1a32 100644
--- a/src/lib/asiodns/Makefile.am
+++ b/src/lib/asiodns/Makefile.am
@@ -8,11 +8,14 @@ AM_CPPFLAGS += -I$(top_srcdir)/src/lib/util -I$(top_builddir)/src/lib/util
AM_CXXFLAGS = $(B10_CXXFLAGS)
-CLEANFILES = *.gcno *.gcda asiodns_messages.h asiodns_messages.cc
+CLEANFILES = *.gcno *.gcda asiodns_messages.h asiodns_messages.cc s-messages
# Define rule to build logging source files from message file
-asiodns_messages.h asiodns_messages.cc: asiodns_messages.mes
+asiodns_messages.h asiodns_messages.cc: s-messages
+
+s-messages: asiodns_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/asiodns/asiodns_messages.mes
+ touch $@
BUILT_SOURCES = asiodns_messages.h asiodns_messages.cc
@@ -35,9 +38,6 @@ EXTRA_DIST = asiodns_messages.mes
# Note: the ordering matters: -Wno-... must follow -Wextra (defined in
# B10_CXXFLAGS)
libb10_asiodns_la_CXXFLAGS = $(AM_CXXFLAGS)
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-libb10_asiodns_la_CXXFLAGS += -Wno-error
-endif
libb10_asiodns_la_CPPFLAGS = $(AM_CPPFLAGS)
-libb10_asiodns_la_LIBADD = $(top_builddir)/src/lib/log/libb10-log.la
+libb10_asiodns_la_LIBADD = $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+libb10_asiodns_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
diff --git a/src/lib/asiodns/README b/src/lib/asiodns/README
index 596d1df..ad27177 100644
--- a/src/lib/asiodns/README
+++ b/src/lib/asiodns/README
@@ -26,9 +26,6 @@ So, in simplified form, the behavior of a DNS Server is:
if not parent:
break
- # This callback informs the caller that a packet has arrived, and
- # gives it a chance to update configuration, etc
- SimpleCallback(packet)
YIELD answer = DNSLookup(packet, this)
response = DNSAnswer(answer)
YIELD send(response)
@@ -37,7 +34,7 @@ At each "YIELD" point, the coroutine initiates an asynchronous operation,
then pauses and turns over control to some other task on the ASIO service
queue. When the operation completes, the coroutine resumes.
-DNSLookup, DNSAnswer and SimpleCallback define callback methods
+DNSLookup and DNSAnswer define callback methods
used by a DNS Server to communicate with the module that called it.
They are abstract-only classes whose concrete implementations
are supplied by the calling module.
diff --git a/src/lib/asiodns/asiodns_messages.mes b/src/lib/asiodns/asiodns_messages.mes
index 5f2a264..71aded7 100644
--- a/src/lib/asiodns/asiodns_messages.mes
+++ b/src/lib/asiodns/asiodns_messages.mes
@@ -103,6 +103,12 @@ would be better to not be too verbose, but you might want to increase
the log level and make sure there's no resource leak or other system
level troubles when it's logged.
+% ASIODNS_TCP_CLOSE_NORESP_FAIL failed to close DNS/TCP socket with a client: %1
+A TCP DNS server tried to close a TCP socket used to communicate with
+a client without returning an answer (which normally happens for zone
+transfer requests), but it failed to do that. See ASIODNS_TCP_CLOSE_FAIL
+for more details.
+
% ASIODNS_TCP_GETREMOTE_FAIL failed to get remote address of a DNS TCP connection: %1
A TCP DNS server tried to get the address and port of a remote client
on a connected socket but failed. It's expected to be rare but can
diff --git a/src/lib/asiodns/dns_service.cc b/src/lib/asiodns/dns_service.cc
index 0324980..58a9918 100644
--- a/src/lib/asiodns/dns_service.cc
+++ b/src/lib/asiodns/dns_service.cc
@@ -37,9 +37,9 @@ class DNSAnswer;
class DNSServiceImpl {
public:
- DNSServiceImpl(IOService& io_service, SimpleCallback* checkin,
+ DNSServiceImpl(IOService& io_service,
DNSLookup* lookup, DNSAnswer* answer) :
- io_service_(io_service), checkin_(checkin), lookup_(lookup),
+ io_service_(io_service), lookup_(lookup),
answer_(answer), tcp_recv_timeout_(5000)
{}
@@ -50,13 +50,12 @@ public:
typedef boost::shared_ptr<TCPServer> TCPServerPtr;
typedef boost::shared_ptr<DNSServer> DNSServerPtr;
std::vector<DNSServerPtr> servers_;
- SimpleCallback* checkin_;
DNSLookup* lookup_;
DNSAnswer* answer_;
size_t tcp_recv_timeout_;
template<class Ptr, class Server> void addServerFromFD(int fd, int af) {
- Ptr server(new Server(io_service_.get_io_service(), fd, af, checkin_,
+ Ptr server(new Server(io_service_.get_io_service(), fd, af,
lookup_, answer_));
startServer(server);
}
@@ -64,8 +63,9 @@ public:
// SyncUDPServer has different constructor signature so it cannot be
// templated.
void addSyncUDPServerFromFD(int fd, int af) {
- SyncUDPServerPtr server(new SyncUDPServer(io_service_.get_io_service(),
- fd, af, lookup_));
+ SyncUDPServerPtr server(SyncUDPServer::create(
+ io_service_.get_io_service(), fd, af,
+ lookup_));
startServer(server);
}
@@ -87,9 +87,9 @@ private:
}
};
-DNSService::DNSService(IOService& io_service, SimpleCallback* checkin,
+DNSService::DNSService(IOService& io_service,
DNSLookup* lookup, DNSAnswer *answer) :
- impl_(new DNSServiceImpl(io_service, checkin, lookup, answer)),
+ impl_(new DNSServiceImpl(io_service, lookup, answer)),
io_service_(io_service)
{
}
diff --git a/src/lib/asiodns/dns_service.h b/src/lib/asiodns/dns_service.h
index 01b6310..00d7158 100644
--- a/src/lib/asiodns/dns_service.h
+++ b/src/lib/asiodns/dns_service.h
@@ -107,8 +107,8 @@ public:
/// DNSService is the service that handles DNS queries and answers with
/// a given IOService. This class is mainly intended to hold all the
/// logic that is shared between the authoritative and the recursive
-/// server implementations. As such, it handles asio, including config
-/// updates (through the 'Checkinprovider'), and listening sockets.
+/// server implementations. As such, it handles asio and listening
+/// sockets.
class DNSService : public DNSServiceBase {
///
/// \name Constructors and Destructor
@@ -132,11 +132,9 @@ public:
/// Use addServerTCPFromFD() or addServerUDPFromFD() to add some servers.
///
/// \param io_service The IOService to work with
- /// \param checkin Provider for cc-channel events (see \c SimpleCallback)
/// \param lookup The lookup provider (see \c DNSLookup)
/// \param answer The answer provider (see \c DNSAnswer)
DNSService(asiolink::IOService& io_service,
- isc::asiolink::SimpleCallback* checkin,
DNSLookup* lookup, DNSAnswer* answer);
/// \brief The destructor.
diff --git a/src/lib/asiodns/io_fetch.cc b/src/lib/asiodns/io_fetch.cc
index eed5fdf..a09d8df 100644
--- a/src/lib/asiodns/io_fetch.cc
+++ b/src/lib/asiodns/io_fetch.cc
@@ -410,10 +410,9 @@ void IOFetch::logIOFailure(asio::error_code ec) {
(data_->origin == ASIODNS_READ_DATA) ||
(data_->origin == ASIODNS_UNKNOWN_ORIGIN));
- static const char* PROTOCOL[2] = {"TCP", "UDP"};
LOG_ERROR(logger, data_->origin).arg(ec.value()).
arg((data_->remote_snd->getProtocol() == IPPROTO_TCP) ?
- PROTOCOL[0] : PROTOCOL[1]).
+ "TCP" : "UDP").
arg(data_->remote_snd->getAddress().toText()).
arg(data_->remote_snd->getPort());
}
diff --git a/src/lib/asiodns/sync_udp_server.cc b/src/lib/asiodns/sync_udp_server.cc
index 9a06691..d3e30af 100644
--- a/src/lib/asiodns/sync_udp_server.cc
+++ b/src/lib/asiodns/sync_udp_server.cc
@@ -26,6 +26,8 @@
#include <boost/bind.hpp>
+#include <cassert>
+
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
@@ -38,13 +40,19 @@ using namespace isc::asiolink;
namespace isc {
namespace asiodns {
+SyncUDPServerPtr
+SyncUDPServer::create(asio::io_service& io_service, const int fd,
+ const int af, DNSLookup* lookup)
+{
+ return (SyncUDPServerPtr(new SyncUDPServer(io_service, fd, af, lookup)));
+}
+
SyncUDPServer::SyncUDPServer(asio::io_service& io_service, const int fd,
const int af, DNSLookup* lookup) :
output_buffer_(new isc::util::OutputBuffer(0)),
query_(new isc::dns::Message(isc::dns::Message::PARSE)),
udp_endpoint_(sender_), lookup_callback_(lookup),
- resume_called_(false), done_(false), stopped_(false),
- recv_callback_(boost::bind(&SyncUDPServer::handleRead, this, _1, _2))
+ resume_called_(false), done_(false), stopped_(false)
{
if (af != AF_INET && af != AF_INET6) {
isc_throw(InvalidParameter, "Address family must be either AF_INET "
@@ -69,12 +77,20 @@ SyncUDPServer::SyncUDPServer(asio::io_service& io_service, const int fd,
void
SyncUDPServer::scheduleRead() {
- socket_->async_receive_from(asio::mutable_buffers_1(data_, MAX_LENGTH),
- sender_, recv_callback_);
+ socket_->async_receive_from(
+ asio::mutable_buffers_1(data_, MAX_LENGTH), sender_,
+ boost::bind(&SyncUDPServer::handleRead, shared_from_this(), _1, _2));
}
void
SyncUDPServer::handleRead(const asio::error_code& ec, const size_t length) {
+ if (stopped_) {
+ // stopped_ can be set to true only after the socket object is closed.
+ // checking this would also detect premature destruction of 'this'
+ // object.
+ assert(socket_ && !socket_->is_open());
+ return;
+ }
if (ec) {
using namespace asio::error;
const asio::error_code::value_type err_val = ec.value();
diff --git a/src/lib/asiodns/sync_udp_server.h b/src/lib/asiodns/sync_udp_server.h
index cabc8bb..e7315be 100644
--- a/src/lib/asiodns/sync_udp_server.h
+++ b/src/lib/asiodns/sync_udp_server.h
@@ -33,23 +33,43 @@
#include <boost/function.hpp>
#include <boost/noncopyable.hpp>
#include <boost/scoped_ptr.hpp>
+#include <boost/enable_shared_from_this.hpp>
#include <stdint.h>
namespace isc {
namespace asiodns {
+class SyncUDPServer;
+typedef boost::shared_ptr<SyncUDPServer> SyncUDPServerPtr;
+
/// \brief An UDP server that doesn't asynchronous lookup handlers.
///
/// That means, the lookup handler must provide the answer right away.
/// This allows for implementation with less overhead, compared with
/// the \c UDPServer class.
-class SyncUDPServer : public DNSServer, public boost::noncopyable {
+///
+/// This class inherits from boost::enable_shared_from_this so a shared
+/// pointer of this object can be passed in an ASIO callback and won't be
+/// accidentally destroyed while waiting for events. To enforce this style
+/// of creation, a static factory method is provided, and the constructor is
+/// hidden as a private.
+class SyncUDPServer : public DNSServer,
+ public boost::enable_shared_from_this<SyncUDPServer>,
+ boost::noncopyable
+{
+private:
+ /// \brief Constructor.
+ ///
+ /// This is hidden as private (see the class description).
+ SyncUDPServer(asio::io_service& io_service, const int fd, const int af,
+ DNSLookup* lookup);
+
public:
- /// \brief Constructor
+ /// \brief Factory of SyncUDPServer object in the form of shared_ptr.
///
/// Due to the nature of this server, it's meaningless if the lookup
- /// callback is NULL. So the constructor explicitly rejects that case
+ /// callback is NULL. So this method explicitly rejects that case
/// with an exception. Likewise, it doesn't take "checkin" or "answer"
/// callbacks. In fact, calling "checkin" from receive callback does not
/// make sense for any of the DNSServer variants (see Trac #2935);
@@ -67,8 +87,8 @@ public:
/// \throw isc::InvalidParameter lookup is NULL
/// \throw isc::asiolink::IOError when a low-level error happens, like the
/// fd is not a valid descriptor.
- SyncUDPServer(asio::io_service& io_service, const int fd, const int af,
- DNSLookup* lookup);
+ static SyncUDPServerPtr create(asio::io_service& io_service, const int fd,
+ const int af, DNSLookup* lookup);
/// \brief Start the SyncUDPServer.
///
@@ -133,7 +153,7 @@ private:
// requires it.
isc::dns::MessagePtr query_, answer_;
// The socket used for the communication
- std::auto_ptr<asio::ip::udp::socket> socket_;
+ boost::scoped_ptr<asio::ip::udp::socket> socket_;
// Wrapper of socket_ in the form of asiolink::IOSocket.
// "DummyIOCallback" is not necessary for this class, but using the
// template is the easiest way to create a UDP instance of IOSocket.
@@ -156,24 +176,6 @@ private:
// Placeholder for error code object. It will be passed to ASIO library
// to have it set in case of error.
asio::error_code ec_;
- // The callback functor for internal asynchronous read event. This is
- // stateless (and it will be copied in the ASIO library anyway), so
- // can be const.
- // SunStudio doesn't like a boost::function object to be passed, so
- // we use the wrapper class as a workaround.
- class CallbackWrapper {
- public:
- CallbackWrapper(boost::function<void(const asio::error_code&, size_t)>
- callback) :
- callback_(callback)
- {}
- void operator()(const asio::error_code& error, size_t len) {
- callback_(error, len);
- }
- private:
- boost::function<void(const asio::error_code&, size_t)> callback_;
- };
- const CallbackWrapper recv_callback_;
// Auxiliary functions
diff --git a/src/lib/asiodns/tcp_server.cc b/src/lib/asiodns/tcp_server.cc
index 32a43ce..2e96671 100644
--- a/src/lib/asiodns/tcp_server.cc
+++ b/src/lib/asiodns/tcp_server.cc
@@ -14,8 +14,6 @@
#include <config.h>
-#include <log/dummylog.h>
-
#include <util/buffer.h>
#include <asio.hpp>
@@ -33,8 +31,11 @@
#include <sys/socket.h>
#include <errno.h>
-using namespace asio;
-using asio::ip::udp;
+// Note: we intentionally avoid 'using namespace asio' to avoid conflicts with
+// std:: definitions in C++11.
+using asio::io_service;
+using asio::buffer;
+using asio::const_buffer;
using asio::ip::tcp;
using namespace std;
@@ -49,11 +50,10 @@ namespace asiodns {
///
/// The constructor
TCPServer::TCPServer(io_service& io_service, int fd, int af,
- const SimpleCallback* checkin,
const DNSLookup* lookup,
const DNSAnswer* answer) :
io_(io_service), done_(false),
- checkin_callback_(checkin), lookup_callback_(lookup),
+ lookup_callback_(lookup),
answer_callback_(answer)
{
if (af != AF_INET && af != AF_INET6) {
@@ -77,18 +77,18 @@ TCPServer::TCPServer(io_service& io_service, int fd, int af,
}
namespace {
- // Called by the timeout_ deadline timer if the client takes too long.
- // If not aborted, cancels the given socket
- // (in which case TCPServer::operator() will be called to continue,
- // with an 'aborted' error code
- void do_timeout(asio::ip::tcp::socket& socket,
- const asio::error_code& error)
- {
- if (error != asio::error::operation_aborted) {
- socket.cancel();
- }
+// Called by the timeout_ deadline timer if the client takes too long.
+// If not aborted, cancels the given socket
+// (in which case TCPServer::operator() will be called to continue,
+// with an 'aborted' error code.)
+void doTimeOut(boost::shared_ptr<asio::ip::tcp::socket> socket,
+ const asio::error_code& error)
+{
+ if (error != asio::error::operation_aborted) {
+ socket->cancel();
}
}
+}
void
TCPServer::operator()(asio::error_code ec, size_t length) {
@@ -110,7 +110,7 @@ TCPServer::operator()(asio::error_code ec, size_t length) {
CORO_YIELD acceptor_->async_accept(*socket_, *this);
if (ec) {
using namespace asio::error;
- const error_code::value_type err_val = ec.value();
+ const asio::error_code::value_type err_val = ec.value();
// The following two cases can happen when this server is
// stopped: operation_aborted in case it's stopped after
// starting accept(). bad_descriptor in case it's stopped
@@ -149,13 +149,16 @@ TCPServer::operator()(asio::error_code ec, size_t length) {
/// asynchronous read call.
data_.reset(new char[MAX_LENGTH]);
- /// Start a timer to drop the connection if it is idle
+ /// Start a timer to drop the connection if it is idle. note that
+ // we pass a shared_ptr of the socket object so that it won't be
+ // destroyed at least until the timeout callback (including abort)
+ // is called.
if (*tcp_recv_timeout_ > 0) {
timeout_.reset(new asio::deadline_timer(io_)); // shouldn't throw
timeout_->expires_from_now( // consider any exception fatal.
boost::posix_time::milliseconds(*tcp_recv_timeout_));
- timeout_->async_wait(boost::bind(&do_timeout, boost::ref(*socket_),
- asio::placeholders::error));
+ timeout_->async_wait(boost::bind(&doTimeOut, socket_,
+ asio::placeholders::error));
}
/// Read the message, in two parts. First, the message length:
@@ -202,16 +205,6 @@ TCPServer::operator()(asio::error_code ec, size_t length) {
io_message_.reset(new IOMessage(data_.get(), length, *iosock_,
*peer_));
- // Perform any necessary operations prior to processing the incoming
- // packet (e.g., checking for queued configuration messages).
- //
- // (XXX: it may be a performance issue to have this called for
- // every single incoming packet; we may wish to throttle it somehow
- // in the future.)
- if (checkin_callback_ != NULL) {
- (*checkin_callback_)(*io_message_);
- }
-
// If we don't have a DNS Lookup provider, there's no point in
// continuing; we exit the coroutine permanently.
if (lookup_callback_ == NULL) {
@@ -235,8 +228,15 @@ TCPServer::operator()(asio::error_code ec, size_t length) {
// The 'done_' flag indicates whether we have an answer
// to send back. If not, exit the coroutine permanently.
if (!done_) {
+ // Explicitly close() isn't necessary for most cases. But for the
+ // very connection, socket_ is shared with the original owner of
+ // the server object and would stay open.
// TODO: should we keep the connection open for a short time
// to see if new requests come in?
+ socket_->close(ec);
+ if (ec) {
+ LOG_DEBUG(logger, 0, ASIODNS_TCP_CLOSE_FAIL).arg(ec.message());
+ }
return;
}
diff --git a/src/lib/asiodns/tcp_server.h b/src/lib/asiodns/tcp_server.h
index 50e8717..3c01076 100644
--- a/src/lib/asiodns/tcp_server.h
+++ b/src/lib/asiodns/tcp_server.h
@@ -41,14 +41,12 @@ public:
/// \param io_service the asio::io_service to work with
/// \param fd the file descriptor of opened TCP socket
/// \param af address family of the socket, either AF_INET or AF_INET6
- /// \param checkin the callbackprovider for non-DNS events
/// \param lookup the callbackprovider for DNS lookup events
/// \param answer the callbackprovider for DNS answer events
/// \throw isc::InvalidParameter if af is neither AF_INET nor AF_INET6
/// \throw isc::asiolink::IOError when a low-level error happens, like the
/// fd is not a valid descriptor or it can't be listened on.
TCPServer(asio::io_service& io_service, int fd, int af,
- const isc::asiolink::SimpleCallback* checkin = NULL,
const DNSLookup* lookup = NULL, const DNSAnswer* answer = NULL);
void operator()(asio::error_code ec = asio::error_code(),
@@ -125,7 +123,6 @@ private:
bool done_;
// Callback functions provided by the caller
- const isc::asiolink::SimpleCallback* checkin_callback_;
const DNSLookup* lookup_callback_;
const DNSAnswer* answer_callback_;
diff --git a/src/lib/asiodns/tests/Makefile.am b/src/lib/asiodns/tests/Makefile.am
index 25f2ea8..25b524e 100644
--- a/src/lib/asiodns/tests/Makefile.am
+++ b/src/lib/asiodns/tests/Makefile.am
@@ -44,10 +44,6 @@ run_unittests_CXXFLAGS = $(AM_CXXFLAGS)
if USE_GXX
run_unittests_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-run_unittests_CXXFLAGS += -Wno-error
-endif
endif
noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/asiodns/tests/dns_server_unittest.cc b/src/lib/asiodns/tests/dns_server_unittest.cc
index dd92dcb..b2fff3e 100644
--- a/src/lib/asiodns/tests/dns_server_unittest.cc
+++ b/src/lib/asiodns/tests/dns_server_unittest.cc
@@ -89,8 +89,8 @@ class ServerStopper {
ServerStopper() : server_to_stop_(NULL) {}
virtual ~ServerStopper(){}
- void setServerToStop(DNSServer* server) {
- server_to_stop_ = server;
+ void setServerToStop(DNSServer& server) {
+ server_to_stop_ = &server;
}
void stopServer() const {
@@ -103,14 +103,6 @@ class ServerStopper {
DNSServer* server_to_stop_;
};
-// \brief no check logic at all,just provide a checkpoint to stop the server
-class DummyChecker : public SimpleCallback, public ServerStopper {
- public:
- virtual void operator()(const IOMessage&) const {
- stopServer();
- }
-};
-
// \brief no lookup logic at all,just provide a checkpoint to stop the server
class DummyLookup : public DNSLookup, public ServerStopper {
public:
@@ -282,6 +274,7 @@ class TCPClient : public SimpleClient {
server_ = server;
socket_.reset(new ip::tcp::socket(service));
socket_->open(ip::tcp::v6());
+ send_delay_timer_.reset(new deadline_timer(service));
}
@@ -332,15 +325,32 @@ class TCPClient : public SimpleClient {
size_t send_bytes)
{
if (!error && send_bytes == 2 && send_data_len_delay_ == 0) {
- sleep(send_data_delay_);
- socket_->async_send(buffer(data_to_send_.c_str(),
- data_to_send_.size() + 1),
- boost::bind(&TCPClient::finishSendHandler, this, _1, _2));
+ // We cannot block here (such as by sleep(3)) since otherwise
+ // the ASIO events may not reliably handled in the server side
+ // as the test expects. So we use async_wait, and make sure the
+ // control will be given back to the IO service.
+ if (send_data_delay_ > 0) {
+ send_delay_timer_->expires_from_now(boost::posix_time::
+ seconds(send_data_delay_));
+ send_delay_timer_->async_wait(
+ boost::bind(&TCPClient::sendMessageData, this));
+ return;
+ }
+ sendMessageData();
}
}
+ void sendMessageData() {
+ socket_->async_send(buffer(data_to_send_.c_str(),
+ data_to_send_.size() + 1),
+ boost::bind(&TCPClient::finishSendHandler, this,
+ _1, _2));
+ }
+
void finishSendHandler(const asio::error_code& error, size_t send_bytes) {
- if (!error && send_bytes == data_to_send_.size() + 1) {
+ if (error) {
+ getResponseCallBack(error, 0);
+ } else if (send_bytes == data_to_send_.size() + 1) {
socket_->async_receive(buffer(received_data_, MAX_DATA_LEN),
boost::bind(&SimpleClient::getResponseCallBack, this, _1,
_2));
@@ -351,6 +361,7 @@ class TCPClient : public SimpleClient {
ip::tcp::endpoint server_;
std::string data_to_send_;
uint16_t data_to_send_len_;
+ boost::shared_ptr<deadline_timer> send_delay_timer_;
size_t send_data_delay_;
size_t send_data_len_delay_;
@@ -369,7 +380,6 @@ class DNSServerTestBase : public::testing::Test {
protected:
DNSServerTestBase() :
server_address_(ip::address::from_string(server_ip)),
- checker_(new DummyChecker()),
lookup_(new DummyLookup()),
sync_lookup_(new SyncDummyLookup()),
answer_(new SimpleAnswer()),
@@ -378,40 +388,35 @@ class DNSServerTestBase : public::testing::Test {
server_port))),
tcp_client_(new TCPClient(service,
ip::tcp::endpoint(server_address_,
- server_port))),
- udp_server_(NULL),
- tcp_server_(NULL)
+ server_port)))
{
current_service = &service;
}
~ DNSServerTestBase() {
- if (udp_server_ != NULL) {
+ if (udp_server_) {
udp_server_->stop();
}
- if (tcp_server_ != NULL) {
+ if (tcp_server_) {
tcp_server_->stop();
}
- delete checker_;
delete lookup_;
delete sync_lookup_;
delete answer_;
- delete udp_server_;
delete udp_client_;
- delete tcp_server_;
delete tcp_client_;
// No delete here. The service is not allocated by new, but as our
// member. This only references it, so just cleaning the pointer.
current_service = NULL;
}
- void testStopServerByStopper(DNSServer* server, SimpleClient* client,
- ServerStopper* stopper)
+ void testStopServerByStopper(DNSServer& server, SimpleClient* client,
+ ServerStopper* stopper)
{
static const unsigned int IO_SERVICE_TIME_OUT = 5;
io_service_is_time_out = false;
stopper->setServerToStop(server);
- (*server)();
+ server();
client->sendDataThenWaitForFeedback(query_message);
// Since thread hasn't been introduced into the tool box, using
// signal to make sure run function will eventually return even
@@ -440,14 +445,13 @@ class DNSServerTestBase : public::testing::Test {
asio::io_service service;
const ip::address server_address_;
- DummyChecker* const checker_;
DummyLookup* lookup_; // we need to replace it in some cases
SyncDummyLookup* const sync_lookup_;
SimpleAnswer* const answer_;
UDPClient* const udp_client_;
TCPClient* const tcp_client_;
- UDPServerClass* udp_server_;
- TCPServer* tcp_server_;
+ boost::shared_ptr<UDPServerClass> udp_server_;
+ boost::shared_ptr<TCPServer> tcp_server_;
// To access them in signal handle function, the following
// variables have to be static.
@@ -508,27 +512,30 @@ protected:
this->udp_server_ = createServer(fd_udp, AF_INET6);
const int fd_tcp(getFd(SOCK_STREAM));
ASSERT_NE(-1, fd_tcp) << strerror(errno);
- this->tcp_server_ = new TCPServer(this->service, fd_tcp, AF_INET6,
- this->checker_, this->lookup_,
- this->answer_);
+ this->tcp_server_ =
+ boost::shared_ptr<TCPServer>(new TCPServer(
+ this->service, fd_tcp, AF_INET6,
+ this->lookup_,
+ this->answer_));
}
// A helper factory of the tested UDP server class: allow customization
// by template specialization.
- UDPServerClass* createServer(int fd, int af) {
- return (new UDPServerClass(this->service, fd, af,
- this->checker_, this->lookup_,
- this->answer_));
+ boost::shared_ptr<UDPServerClass> createServer(int fd, int af) {
+ return (boost::shared_ptr<UDPServerClass>(
+ new UDPServerClass(this->service, fd, af,
+ this->lookup_,
+ this->answer_)));
}
};
// Specialization for SyncUDPServer. It needs to use SyncDummyLookup.
template<>
-SyncUDPServer*
+boost::shared_ptr<SyncUDPServer>
FdInit<SyncUDPServer>::createServer(int fd, int af) {
delete this->lookup_;
this->lookup_ = new SyncDummyLookup;
- return (new SyncUDPServer(this->service, fd, af, this->lookup_));
+ return (SyncUDPServer::create(this->service, fd, af, this->lookup_));
}
// This makes it the template as gtest wants it.
@@ -557,7 +564,7 @@ asio::io_service* DNSServerTestBase<UDPServerClass>::current_service(NULL);
// get response, UDP server will be stopped, the io service won't quit
// if udp server doesn't stop successfully.
TYPED_TEST(DNSServerTest, stopUDPServerAfterOneQuery) {
- this->testStopServerByStopper(this->udp_server_, this->udp_client_,
+ this->testStopServerByStopper(*this->udp_server_, this->udp_client_,
this->udp_client_);
EXPECT_EQ(query_message, this->udp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -566,26 +573,15 @@ TYPED_TEST(DNSServerTest, stopUDPServerAfterOneQuery) {
// Test whether udp server stopped successfully before server start to serve
TYPED_TEST(DNSServerTest, stopUDPServerBeforeItStartServing) {
this->udp_server_->stop();
- this->testStopServerByStopper(this->udp_server_, this->udp_client_,
+ this->testStopServerByStopper(*this->udp_server_, this->udp_client_,
this->udp_client_);
EXPECT_EQ(std::string(""), this->udp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
}
-
-// Test whether udp server stopped successfully during message check.
-// This only works for non-sync server; SyncUDPServer doesn't use checkin
-// callback.
-TEST_F(AsyncServerTest, stopUDPServerDuringMessageCheck) {
- this->testStopServerByStopper(this->udp_server_, this->udp_client_,
- this->checker_);
- EXPECT_EQ(std::string(""), this->udp_client_->getReceivedData());
- EXPECT_TRUE(this->serverStopSucceed());
-}
-
// Test whether udp server stopped successfully during query lookup
TYPED_TEST(DNSServerTest, stopUDPServerDuringQueryLookup) {
- this->testStopServerByStopper(this->udp_server_, this->udp_client_,
+ this->testStopServerByStopper(*this->udp_server_, this->udp_client_,
this->lookup_);
EXPECT_EQ(std::string(""), this->udp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -595,7 +591,7 @@ TYPED_TEST(DNSServerTest, stopUDPServerDuringQueryLookup) {
// Only works for (non-sync) server because SyncUDPServer doesn't use answer
// callback.
TEST_F(AsyncServerTest, stopUDPServerDuringPrepareAnswer) {
- testStopServerByStopper(udp_server_, udp_client_, answer_);
+ testStopServerByStopper(*udp_server_, udp_client_, answer_);
EXPECT_EQ(std::string(""), udp_client_->getReceivedData());
EXPECT_TRUE(serverStopSucceed());
}
@@ -612,18 +608,17 @@ stopServerManyTimes(DNSServer *server, unsigned int times) {
TYPED_TEST(DNSServerTest, stopUDPServerMoreThanOnce) {
ASSERT_NO_THROW({
boost::function<void()> stop_server_3_times
- = boost::bind(stopServerManyTimes, this->udp_server_, 3);
+ = boost::bind(stopServerManyTimes, this->udp_server_.get(), 3);
this->udp_client_->setGetFeedbackCallback(stop_server_3_times);
- this->testStopServerByStopper(this->udp_server_,
+ this->testStopServerByStopper(*this->udp_server_,
this->udp_client_, this->udp_client_);
EXPECT_EQ(query_message, this->udp_client_->getReceivedData());
});
EXPECT_TRUE(this->serverStopSucceed());
}
-
TYPED_TEST(DNSServerTest, stopTCPServerAfterOneQuery) {
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ(query_message, this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -632,7 +627,7 @@ TYPED_TEST(DNSServerTest, stopTCPServerAfterOneQuery) {
TYPED_TEST(DNSServerTest, TCPTimeoutOnLen) {
this->tcp_server_->setTCPRecvTimeout(100);
this->tcp_client_->setSendDataLenDelay(2);
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ("", this->tcp_client_->getReceivedData());
EXPECT_FALSE(this->serverStopSucceed());
@@ -642,7 +637,7 @@ TYPED_TEST(DNSServerTest, TCPTimeout) {
// set delay higher than timeout
this->tcp_server_->setTCPRecvTimeout(100);
this->tcp_client_->setSendDataDelay(2);
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ("", this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -652,7 +647,7 @@ TYPED_TEST(DNSServerTest, TCPNoTimeout) {
// set delay lower than timeout
this->tcp_server_->setTCPRecvTimeout(3000);
this->tcp_client_->setSendDataDelay(1);
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ("BIND10 is awesome", this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -661,24 +656,16 @@ TYPED_TEST(DNSServerTest, TCPNoTimeout) {
// Test whether tcp server stopped successfully before server start to serve
TYPED_TEST(DNSServerTest, stopTCPServerBeforeItStartServing) {
this->tcp_server_->stop();
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ(std::string(""), this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
}
-// Test whether tcp server stopped successfully during message check
-TYPED_TEST(DNSServerTest, stopTCPServerDuringMessageCheck) {
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
- this->checker_);
- EXPECT_EQ(std::string(""), this->tcp_client_->getReceivedData());
- EXPECT_TRUE(this->serverStopSucceed());
-}
-
// Test whether tcp server stopped successfully during query lookup
TYPED_TEST(DNSServerTest, stopTCPServerDuringQueryLookup) {
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->lookup_);
EXPECT_EQ(std::string(""), this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -686,7 +673,7 @@ TYPED_TEST(DNSServerTest, stopTCPServerDuringQueryLookup) {
// Test whether tcp server stopped successfully during composing answer
TYPED_TEST(DNSServerTest, stopTCPServerDuringPrepareAnswer) {
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->answer_);
EXPECT_EQ(std::string(""), this->tcp_client_->getReceivedData());
EXPECT_TRUE(this->serverStopSucceed());
@@ -698,9 +685,9 @@ TYPED_TEST(DNSServerTest, stopTCPServerDuringPrepareAnswer) {
TYPED_TEST(DNSServerTest, stopTCPServeMoreThanOnce) {
ASSERT_NO_THROW({
boost::function<void()> stop_server_3_times
- = boost::bind(stopServerManyTimes, this->tcp_server_, 3);
+ = boost::bind(stopServerManyTimes, this->tcp_server_.get(), 3);
this->tcp_client_->setGetFeedbackCallback(stop_server_3_times);
- this->testStopServerByStopper(this->tcp_server_, this->tcp_client_,
+ this->testStopServerByStopper(*this->tcp_server_, this->tcp_client_,
this->tcp_client_);
EXPECT_EQ(query_message, this->tcp_client_->getReceivedData());
});
@@ -712,7 +699,7 @@ TYPED_TEST(DNSServerTest, stopTCPServeMoreThanOnce) {
TYPED_TEST(DNSServerTestBase, invalidFamily) {
// We abuse DNSServerTestBase for this test, as we don't need the
// initialization.
- EXPECT_THROW(TCPServer(this->service, 0, AF_UNIX, this->checker_,
+ EXPECT_THROW(TCPServer(this->service, 0, AF_UNIX,
this->lookup_, this->answer_),
isc::InvalidParameter);
}
@@ -731,10 +718,10 @@ TYPED_TEST(DNSServerTestBase, invalidTCPFD) {
asio backend does fail as it tries to insert it right away, but
not the others, maybe we could make it run this at last on epoll-based
systems).
- EXPECT_THROW(UDPServer(service, -1, AF_INET, checker_, lookup_,
+ EXPECT_THROW(UDPServer(service, -1, AF_INET, lookup_,
answer_), isc::asiolink::IOError);
*/
- EXPECT_THROW(TCPServer(this->service, -1, AF_INET, this->checker_,
+ EXPECT_THROW(TCPServer(this->service, -1, AF_INET,
this->lookup_, this->answer_),
isc::asiolink::IOError);
}
@@ -759,14 +746,36 @@ TEST_F(SyncServerTest, unsupportedOps) {
// Check it rejects forgotten resume (eg. insists that it is synchronous)
TEST_F(SyncServerTest, mustResume) {
lookup_->allow_resume_ = false;
- ASSERT_THROW(testStopServerByStopper(udp_server_, udp_client_, lookup_),
+ ASSERT_THROW(testStopServerByStopper(*udp_server_, udp_client_, lookup_),
isc::Unexpected);
}
// SyncUDPServer doesn't allow NULL lookup callback.
TEST_F(SyncServerTest, nullLookupCallback) {
- EXPECT_THROW(SyncUDPServer(service, 0, AF_INET, NULL),
+ EXPECT_THROW(SyncUDPServer::create(service, 0, AF_INET, NULL),
isc::InvalidParameter);
}
+TEST_F(SyncServerTest, resetUDPServerBeforeEvent) {
+ // Reset the UDP server object after starting and before it would get
+ // an event from io_service (in this case abort event). The following
+ // sequence confirms it's shut down immediately, and without any
+ // disruption.
+
+ // Since we'll stop the server run() should immediately return, and
+ // it's very unlikely to cause hangup. But we'll make very sure it
+ // doesn't happen.
+ const unsigned int IO_SERVICE_TIME_OUT = 5;
+ (*udp_server_)();
+ udp_server_->stop();
+ udp_server_.reset();
+ void (*prev_handler)(int) = std::signal(SIGALRM, stopIOService);
+ current_service = &service;
+ alarm(IO_SERVICE_TIME_OUT);
+ service.run();
+ alarm(0);
+ std::signal(SIGALRM, prev_handler);
+ EXPECT_FALSE(io_service_is_time_out);
+}
+
}
diff --git a/src/lib/asiodns/tests/dns_service_unittest.cc b/src/lib/asiodns/tests/dns_service_unittest.cc
index ce8eee9..c35897c 100644
--- a/src/lib/asiodns/tests/dns_service_unittest.cc
+++ b/src/lib/asiodns/tests/dns_service_unittest.cc
@@ -81,7 +81,7 @@ protected:
UDPDNSServiceTest() :
first_buffer_(NULL), second_buffer_(NULL),
lookup(&first_buffer_, &second_buffer_, io_service),
- dns_service(io_service, NULL, &lookup, NULL),
+ dns_service(io_service, &lookup, NULL),
client_socket(io_service.get_io_service(), asio::ip::udp::v6()),
server_ep(asio::ip::address::from_string(TEST_IPV6_ADDR),
lexical_cast<uint16_t>(TEST_SERVER_PORT)),
diff --git a/src/lib/asiodns/udp_server.cc b/src/lib/asiodns/udp_server.cc
index 7221296..54fdfd7 100644
--- a/src/lib/asiodns/udp_server.cc
+++ b/src/lib/asiodns/udp_server.cc
@@ -21,8 +21,6 @@
#include <config.h>
-#include <log/dummylog.h>
-
#include <asio.hpp>
#include <asio/error.hpp>
#include <asiolink/dummy_io_cb.h>
@@ -33,9 +31,12 @@
#include <dns/opcode.h>
-using namespace asio;
+// Avoid 'using namespace asio' (see tcp_server.cc)
+using asio::io_service;
+using asio::socket_base;
+using asio::buffer;
using asio::ip::udp;
-using isc::log::dlog;
+using asio::ip::address;
using namespace std;
using namespace isc::dns;
@@ -59,10 +60,10 @@ struct UDPServer::Data {
* query, it will only hold parameters until we wait for the
* first packet. But we do initialize the socket in here.
*/
- Data(io_service& io_service, const ip::address& addr, const uint16_t port,
- SimpleCallback* checkin, DNSLookup* lookup, DNSAnswer* answer) :
+ Data(io_service& io_service, const address& addr, const uint16_t port,
+ DNSLookup* lookup, DNSAnswer* answer) :
io_(io_service), bytes_(0), done_(false),
- checkin_callback_(checkin),lookup_callback_(lookup),
+ lookup_callback_(lookup),
answer_callback_(answer)
{
// We must use different instantiations for v4 and v6;
@@ -75,10 +76,10 @@ struct UDPServer::Data {
}
socket_->bind(udp::endpoint(addr, port));
}
- Data(io_service& io_service, int fd, int af, SimpleCallback* checkin,
+ Data(io_service& io_service, int fd, int af,
DNSLookup* lookup, DNSAnswer* answer) :
io_(io_service), bytes_(0), done_(false),
- checkin_callback_(checkin),lookup_callback_(lookup),
+ lookup_callback_(lookup),
answer_callback_(answer)
{
if (af != AF_INET && af != AF_INET6) {
@@ -105,7 +106,6 @@ struct UDPServer::Data {
*/
Data(const Data& other) :
io_(other.io_), socket_(other.socket_), bytes_(0), done_(false),
- checkin_callback_(other.checkin_callback_),
lookup_callback_(other.lookup_callback_),
answer_callback_(other.answer_callback_)
{
@@ -169,7 +169,6 @@ struct UDPServer::Data {
bool done_;
// Callback functions provided by the caller
- const SimpleCallback* checkin_callback_;
const DNSLookup* lookup_callback_;
const DNSAnswer* answer_callback_;
@@ -182,9 +181,9 @@ struct UDPServer::Data {
/// The constructor. It just creates new internal state object
/// and lets it handle the initialization.
UDPServer::UDPServer(io_service& io_service, int fd, int af,
- SimpleCallback* checkin, DNSLookup* lookup,
+ DNSLookup* lookup,
DNSAnswer* answer) :
- data_(new Data(io_service, fd, af, checkin, lookup, answer))
+ data_(new Data(io_service, fd, af, lookup, answer))
{ }
/// The function operator is implemented with the "stackless coroutine"
@@ -215,7 +214,7 @@ UDPServer::operator()(asio::error_code ec, size_t length) {
// See TCPServer::operator() for details on error handling.
if (ec) {
using namespace asio::error;
- const error_code::value_type err_val = ec.value();
+ const asio::error_code::value_type err_val = ec.value();
if (err_val == operation_aborted ||
err_val == bad_descriptor) {
return;
@@ -263,15 +262,6 @@ UDPServer::operator()(asio::error_code ec, size_t length) {
data_->io_message_.reset(new IOMessage(data_->data_.get(),
data_->bytes_, *data_->iosock_, *data_->peer_));
- // Perform any necessary operations prior to processing an incoming
- // query (e.g., checking for queued configuration messages).
- //
- // (XXX: it may be a performance issue to check in for every single
- // incoming query; we may wish to throttle this in the future.)
- if (data_->checkin_callback_ != NULL) {
- (*data_->checkin_callback_)(*data_->io_message_);
- }
-
// If we don't have a DNS Lookup provider, there's no point in
// continuing; we exit the coroutine permanently.
if (data_->lookup_callback_ == NULL) {
diff --git a/src/lib/asiodns/udp_server.h b/src/lib/asiodns/udp_server.h
index c2b1b96..1c1dd9f 100644
--- a/src/lib/asiodns/udp_server.h
+++ b/src/lib/asiodns/udp_server.h
@@ -43,14 +43,12 @@ public:
/// \param io_service the asio::io_service to work with
/// \param fd the file descriptor of opened UDP socket
/// \param af address family, either AF_INET or AF_INET6
- /// \param checkin the callbackprovider for non-DNS events
/// \param lookup the callbackprovider for DNS lookup events
/// \param answer the callbackprovider for DNS answer events
/// \throw isc::InvalidParameter if af is neither AF_INET nor AF_INET6
/// \throw isc::asiolink::IOError when a low-level error happens, like the
/// fd is not a valid descriptor.
UDPServer(asio::io_service& io_service, int fd, int af,
- isc::asiolink::SimpleCallback* checkin = NULL,
DNSLookup* lookup = NULL, DNSAnswer* answer = NULL);
/// \brief The function operator
diff --git a/src/lib/asiolink/Makefile.am b/src/lib/asiolink/Makefile.am
index 3505982..504dd78 100644
--- a/src/lib/asiolink/Makefile.am
+++ b/src/lib/asiolink/Makefile.am
@@ -32,13 +32,14 @@ libb10_asiolink_la_SOURCES += tcp_endpoint.h
libb10_asiolink_la_SOURCES += tcp_socket.h
libb10_asiolink_la_SOURCES += udp_endpoint.h
libb10_asiolink_la_SOURCES += udp_socket.h
+libb10_asiolink_la_SOURCES += local_socket.h local_socket.cc
# Note: the ordering matters: -Wno-... must follow -Wextra (defined in
# B10_CXXFLAGS)
libb10_asiolink_la_CXXFLAGS = $(AM_CXXFLAGS)
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-libb10_asiolink_la_CXXFLAGS += -Wno-error
-endif
libb10_asiolink_la_CPPFLAGS = $(AM_CPPFLAGS)
-libb10_asiolink_la_LIBADD = $(top_builddir)/src/lib/log/libb10-log.la
+libb10_asiolink_la_LIBADD = $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+
+# IOAddress is sometimes used in user-library code
+libb10_asiolink_includedir = $(pkgincludedir)/asiolink
+libb10_asiolink_include_HEADERS = io_address.h
diff --git a/src/lib/asiolink/io_address.cc b/src/lib/asiolink/io_address.cc
index 90bdf57..7986725 100644
--- a/src/lib/asiolink/io_address.cc
+++ b/src/lib/asiolink/io_address.cc
@@ -114,5 +114,11 @@ IOAddress::operator uint32_t() const {
}
}
+std::ostream&
+operator<<(std::ostream& os, const IOAddress& address) {
+ os << address.toText();
+ return (os);
+}
+
} // namespace asiolink
} // namespace isc
diff --git a/src/lib/asiolink/io_address.h b/src/lib/asiolink/io_address.h
index 17705d6..53edb35 100644
--- a/src/lib/asiolink/io_address.h
+++ b/src/lib/asiolink/io_address.h
@@ -232,6 +232,22 @@ private:
asio::ip::address asio_address_;
};
+/// \brief Insert the IOAddress as a string into stream.
+///
+/// This method converts the \c address into a string and inserts it
+/// into the output stream \c os.
+///
+/// This function overloads the global operator<< to behave as described
+/// in ostream::operator<< but applied to \c IOAddress objects.
+///
+/// \param os A \c std::ostream object on which the insertion operation is
+/// performed.
+/// \param address The \c IOAddress object output by the operation.
+/// \return A reference to the same \c std::ostream object referenced by
+/// parameter \c os after the insertion operation.
+std::ostream&
+operator<<(std::ostream& os, const IOAddress& address);
+
} // namespace asiolink
} // namespace isc
#endif // IO_ADDRESS_H
diff --git a/src/lib/asiolink/io_asio_socket.h b/src/lib/asiolink/io_asio_socket.h
index a3f3f97..afd2884 100644
--- a/src/lib/asiolink/io_asio_socket.h
+++ b/src/lib/asiolink/io_asio_socket.h
@@ -344,7 +344,7 @@ public:
///
/// Must be supplied as it is abstract in the base class.
/// The parameters are unused.
- virtual void asyncReceive(void* data, size_t, size_t, IOEndpoint*, C&) {
+ virtual void asyncReceive(void*, size_t, size_t, IOEndpoint*, C&) {
}
/// \brief Checks if the data received is complete.
@@ -357,10 +357,8 @@ public:
/// \param outbuff Unused.
///
/// \return Always true
- virtual bool receiveComplete(const void* staging, size_t length,
- size_t& cumulative, size_t& offset,
- size_t& expected,
- isc::util::OutputBufferPtr& outbuff)
+ virtual bool receiveComplete(const void*, size_t, size_t&, size_t&,
+ size_t&, isc::util::OutputBufferPtr&)
{
return (true);
}
diff --git a/src/lib/asiolink/io_endpoint.cc b/src/lib/asiolink/io_endpoint.cc
index 2354521..e6928f6 100644
--- a/src/lib/asiolink/io_endpoint.cc
+++ b/src/lib/asiolink/io_endpoint.cc
@@ -64,12 +64,12 @@ IOEndpoint::operator!=(const IOEndpoint& other) const {
ostream&
operator<<(ostream& os, const IOEndpoint& endpoint) {
if (endpoint.getFamily() == AF_INET6) {
- os << "[" << endpoint.getAddress().toText() << "]";
+ os << "[" << endpoint.getAddress() << "]";
} else {
// In practice this should be AF_INET, but it's not guaranteed by
// the interface. We'll use the result of textual address
// representation opaquely.
- os << endpoint.getAddress().toText();
+ os << endpoint.getAddress();
}
os << ":" << boost::lexical_cast<string>(endpoint.getPort());
return (os);
diff --git a/src/lib/asiolink/local_socket.cc b/src/lib/asiolink/local_socket.cc
new file mode 100644
index 0000000..f47226e
--- /dev/null
+++ b/src/lib/asiolink/local_socket.cc
@@ -0,0 +1,102 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/local_socket.h>
+#include <asiolink/io_service.h>
+#include <asiolink/io_error.h>
+
+#include <asio.hpp>
+
+#include <boost/bind.hpp>
+
+#include <string>
+#include <sys/socket.h>
+
+namespace isc {
+namespace asiolink {
+class LocalSocket::Impl {
+public:
+ Impl(IOService& io_service, int fd) :
+ asio_sock_(io_service.get_io_service(),
+ asio::local::stream_protocol(), fd)
+ {
+ // Depending on the underlying demultiplex API, the constructor may or
+ // may not throw in case fd is invalid. To catch such cases sooner,
+ // we try to get the local endpoint (we don't need it in the rest of
+ // this implementation).
+ asio_sock_.local_endpoint(ec_);
+ if (ec_) {
+ isc_throw(IOError, "failed to open local socket with FD " << fd
+ << " (local endpoint unknown): " << ec_.message());
+ }
+ }
+
+ asio::local::stream_protocol::socket asio_sock_;
+ asio::error_code ec_;
+};
+
+LocalSocket::LocalSocket(IOService& io_service, int fd) :
+ impl_(NULL)
+{
+ try {
+ impl_ = new Impl(io_service, fd);
+ } catch (const asio::system_error& error) {
+ // Catch and convert any exception from asio's constructor
+ isc_throw(IOError, "failed to open local socket with FD " << fd
+ << ": " << error.what());
+ }
+}
+
+LocalSocket::~LocalSocket() {
+ delete impl_;
+}
+
+int
+LocalSocket::getNative() const {
+ return (impl_->asio_sock_.native());
+}
+
+int
+LocalSocket::getProtocol() const {
+ return (AF_UNIX);
+}
+
+namespace {
+// Wrapper callback for async_read that simply adjusts asio-native parameters
+// for the LocalSocket interface. Note that this is a free function and
+// doesn't rely on internal member variables of LocalSocket.
+// So it can be called safely even after the LocalSocket object on which
+// asyncRead() was called is destroyed.
+void
+readCompleted(const asio::error_code& ec,
+ LocalSocket::ReadCallback user_callback)
+{
+ // assumption check: we pass non empty string iff ec indicates an error.
+ const std::string err_msg = ec ? ec.message() : std::string();
+ assert(ec || err_msg.empty());
+
+ user_callback(err_msg);
+}
+}
+
+void
+LocalSocket::asyncRead(const ReadCallback& callback, void* buf,
+ size_t buflen)
+{
+ asio::async_read(impl_->asio_sock_, asio::buffer(buf, buflen),
+ boost::bind(readCompleted, _1, callback));
+}
+
+} // namespace asiolink
+} // namespace isc
diff --git a/src/lib/asiolink/local_socket.h b/src/lib/asiolink/local_socket.h
new file mode 100644
index 0000000..6269b7c
--- /dev/null
+++ b/src/lib/asiolink/local_socket.h
@@ -0,0 +1,132 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LOCAL_SOCKET_H
+#define LOCAL_SOCKET_H 1
+
+#include <asiolink/io_socket.h>
+#include <asiolink/io_service.h>
+
+#include <boost/noncopyable.hpp>
+
+namespace isc {
+namespace asiolink {
+
+/// \brief A wrapper for ASIO stream socket in the local (AF_UNIX) domain.
+///
+/// This class provides a simple, limited set of wrapper interfaces to an
+/// ASIO stream socket object in the local domain. Unlike other concrete
+/// derived classes of \c IOSocket, this class is intended to be instantiated
+/// directly. Right now it only provides read interface due to the limited
+/// expected usage, but it can be extended as we see need for other operations
+/// on this socket.
+///
+/// Note that in the initial implementation there's even no stop() or cancel()
+/// method; for these cases users are expected to just destroy the socket
+/// object (this may be extended in future, too).
+class LocalSocket : boost::noncopyable, public IOSocket {
+public:
+ /// \brief Constructor from a native file descriptor of AF_UNIX stream
+ /// socket.
+ ///
+ /// Parameter \c fd must be an open stream-type socket of the AF_UNIX
+ /// domain. The constructor tries to detect some invalid cases, but
+ /// it may not reject all invalid cases. It's generally the
+ /// responsibility of the caller.
+ ///
+ /// \throw IOError Failed to create the socket object, most likely because
+ /// the given file descriptor is invalid.
+ ///
+ /// \param io_service The IO service object to handle events on this
+ /// socket.
+ /// \param fd File descriptor of an AF_UNIX socket.
+ LocalSocket(IOService& io_service, int fd);
+
+ /// \brief Destructor.
+ ///
+ /// \throw None.
+ virtual ~LocalSocket();
+
+ /// \brief Local socket version of getNative().
+ ///
+ /// \throw None.
+ virtual int getNative() const;
+
+ /// \brief Local socket version of getProtocol().
+ ///
+ /// It always returns \c AF_UNIX.
+ ///
+ /// \throw None.
+ virtual int getProtocol() const;
+
+ /// \brief The callback functor for the \c asyncRead method.
+ ///
+ /// The callback takes one parameter, \c error. It will be set to
+ /// non empty string if read operation fails and the string explains
+ /// the reason for the failure. On success \c error will be empty.
+ typedef boost::function<void(const std::string& error)> ReadCallback;
+
+ /// \brief Start asynchronous read on the socket.
+ ///
+ /// This method registers an interest on a new read event on the local
+ /// socket for the specified length of data (\c buflen bytes). This
+ /// method returns immediately. When the specified amount of data
+ /// are available for read from the socket or an error happens, the
+ /// specified callback will be called. In the former case the data are
+ /// copied into the given buffer (pointed to by \c buf); in the latter
+ /// case, the \c error parameter of the callback will be set to a non
+ /// empty string.
+ ///
+ /// In the case of error, this socket should be considered
+ /// unusable anymore, because this class doesn't provide a feasible way
+ /// to identify where in the input stream to restart reading. So,
+ /// in practice, the user of this socket should destroy this socket,
+ /// and, if necessary to continue, create a new one.
+ ///
+ /// \c buf must point to a memory region that has at least \c buflen
+ /// bytes of valid space. That region must be kept valid until the
+ /// callback is called or the \c IOService passed to the constructor
+ /// is stopped. This method and class do not check these conditions;
+ /// it's the caller's responsibility to guarantee them.
+ ///
+ /// \note If asyncRead() has been called and hasn't been completed (with
+ /// the callback being called), it's possible that the callback is called
+ /// even after the \c LocalSocket object is destroyed. So the caller
+ /// has to make sure that either \c LocalSocket is valid until the
+ /// callback is called or the callback does not depend on \c LocalSocket;
+ /// alternatively, the caller can stop the \c IOService. This will make
+ /// sure the callback will not be called regardless of when and how
+ /// the \c LocalSocket is destroyed.
+ ///
+ /// \throw None.
+ ///
+ /// \brief callback The callback functor to be called on the completion
+ /// of read.
+ /// \brief buf Buffer to read in data from the socket.
+ /// \brief buflen Length of data to read.
+ void asyncRead(const ReadCallback& callback, void* buf, size_t buflen);
+
+private:
+ class Impl;
+ Impl* impl_;
+};
+
+} // namespace asiolink
+} // namespace isc
+
+#endif // LOCAL_SOCKET_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/asiolink/tcp_socket.h b/src/lib/asiolink/tcp_socket.h
index 6b0a43c..7df4a80 100644
--- a/src/lib/asiolink/tcp_socket.h
+++ b/src/lib/asiolink/tcp_socket.h
@@ -19,7 +19,6 @@
#error "asio.hpp must be included before including this, see asiolink.h as to why"
#endif
-#include <log/dummylog.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h> // for some IPC/network system calls
diff --git a/src/lib/asiolink/tests/Makefile.am b/src/lib/asiolink/tests/Makefile.am
index 70b94dc..8525c2a 100644
--- a/src/lib/asiolink/tests/Makefile.am
+++ b/src/lib/asiolink/tests/Makefile.am
@@ -34,6 +34,7 @@ run_unittests_SOURCES += tcp_socket_unittest.cc
run_unittests_SOURCES += udp_endpoint_unittest.cc
run_unittests_SOURCES += udp_socket_unittest.cc
run_unittests_SOURCES += io_service_unittest.cc
+run_unittests_SOURCES += local_socket_unittest.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
@@ -51,10 +52,6 @@ run_unittests_CXXFLAGS = $(AM_CXXFLAGS)
if USE_GXX
run_unittests_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-run_unittests_CXXFLAGS += -Wno-error
-endif
endif
noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/asiolink/tests/io_address_unittest.cc b/src/lib/asiolink/tests/io_address_unittest.cc
index 4bd7626..5486d5c 100644
--- a/src/lib/asiolink/tests/io_address_unittest.cc
+++ b/src/lib/asiolink/tests/io_address_unittest.cc
@@ -21,6 +21,7 @@
#include <algorithm>
#include <cstring>
#include <vector>
+#include <sstream>
using namespace isc::asiolink;
@@ -83,7 +84,7 @@ TEST(IOAddressTest, fromBytes) {
EXPECT_NO_THROW({
addr = IOAddress::fromBytes(AF_INET, v4);
});
- EXPECT_EQ(addr.toText(), IOAddress("192.0.2.3").toText());
+ EXPECT_EQ(addr, IOAddress("192.0.2.3"));
}
TEST(IOAddressTest, toBytesV4) {
@@ -172,3 +173,12 @@ TEST(IOAddressTest, lessThanEqual) {
EXPECT_TRUE(addr6 <= addr7);
}
+
+// test operator<<. We simply confirm it appends the result of toText().
+TEST(IOAddressTest, LeftShiftOperator) {
+ const IOAddress addr("192.0.2.5");
+
+ std::ostringstream oss;
+ oss << addr;
+ EXPECT_EQ(addr.toText(), oss.str());
+}
diff --git a/src/lib/asiolink/tests/io_endpoint_unittest.cc b/src/lib/asiolink/tests/io_endpoint_unittest.cc
index 462a2fb..c953974 100644
--- a/src/lib/asiolink/tests/io_endpoint_unittest.cc
+++ b/src/lib/asiolink/tests/io_endpoint_unittest.cc
@@ -41,7 +41,7 @@ TEST(IOEndpointTest, createUDPv4) {
EXPECT_EQ(53210, ep->getPort());
EXPECT_EQ(AF_INET, ep->getFamily());
EXPECT_EQ(AF_INET, ep->getAddress().getFamily());
- EXPECT_EQ(IPPROTO_UDP, ep->getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP), ep->getProtocol());
}
TEST(IOEndpointTest, createTCPv4) {
@@ -51,7 +51,7 @@ TEST(IOEndpointTest, createTCPv4) {
EXPECT_EQ(5301, ep->getPort());
EXPECT_EQ(AF_INET, ep->getFamily());
EXPECT_EQ(AF_INET, ep->getAddress().getFamily());
- EXPECT_EQ(IPPROTO_TCP, ep->getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP), ep->getProtocol());
}
TEST(IOEndpointTest, createUDPv6) {
@@ -62,7 +62,7 @@ TEST(IOEndpointTest, createUDPv6) {
EXPECT_EQ(5302, ep->getPort());
EXPECT_EQ(AF_INET6, ep->getFamily());
EXPECT_EQ(AF_INET6, ep->getAddress().getFamily());
- EXPECT_EQ(IPPROTO_UDP, ep->getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP), ep->getProtocol());
}
TEST(IOEndpointTest, createTCPv6) {
@@ -73,7 +73,7 @@ TEST(IOEndpointTest, createTCPv6) {
EXPECT_EQ(5303, ep->getPort());
EXPECT_EQ(AF_INET6, ep->getFamily());
EXPECT_EQ(AF_INET6, ep->getAddress().getFamily());
- EXPECT_EQ(IPPROTO_TCP, ep->getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP), ep->getProtocol());
}
TEST(IOEndpointTest, equality) {
diff --git a/src/lib/asiolink/tests/io_socket_unittest.cc b/src/lib/asiolink/tests/io_socket_unittest.cc
index 15afc17..44e3630 100644
--- a/src/lib/asiolink/tests/io_socket_unittest.cc
+++ b/src/lib/asiolink/tests/io_socket_unittest.cc
@@ -23,8 +23,10 @@
using namespace isc::asiolink;
TEST(IOSocketTest, dummySockets) {
- EXPECT_EQ(IPPROTO_UDP, IOSocket::getDummyUDPSocket().getProtocol());
- EXPECT_EQ(IPPROTO_TCP, IOSocket::getDummyTCPSocket().getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP),
+ IOSocket::getDummyUDPSocket().getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP),
+ IOSocket::getDummyTCPSocket().getProtocol());
EXPECT_EQ(-1, IOSocket::getDummyUDPSocket().getNative());
EXPECT_EQ(-1, IOSocket::getDummyTCPSocket().getNative());
}
diff --git a/src/lib/asiolink/tests/local_socket_unittest.cc b/src/lib/asiolink/tests/local_socket_unittest.cc
new file mode 100644
index 0000000..72efd6e
--- /dev/null
+++ b/src/lib/asiolink/tests/local_socket_unittest.cc
@@ -0,0 +1,253 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/local_socket.h>
+#include <asiolink/io_error.h>
+
+#include <gtest/gtest.h>
+
+#include <boost/noncopyable.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <boost/bind.hpp>
+
+#include <csignal>
+#include <cstring>
+#include <vector>
+
+#include <sys/socket.h>
+#include <stdint.h>
+#include <unistd.h> // for alarm(3)
+
+using namespace isc::asiolink;
+
+namespace {
+
+// duration (in seconds) until we break possible hangup; value is an
+// arbitrary choice.
+const unsigned IO_TIMEOUT = 10;
+
+// A simple RAII wrapper for a file descriptor so test sockets are safely
+// closed in each test.
+class ScopedSocket : boost::noncopyable {
+public:
+ ScopedSocket() : fd_(-1) {}
+ ~ScopedSocket() {
+ if (fd_ >= 0) {
+ EXPECT_EQ(0, ::close(fd_));
+ }
+ }
+ void set(int fd) {
+ assert(fd_ == -1);
+ fd_ = fd;
+ }
+ int get() { return (fd_); }
+ int release() {
+ const int ret = fd_;
+ fd_ = -1;
+ return (ret);
+ }
+private:
+ int fd_;
+};
+
+class LocalSocketTest : public ::testing::Test {
+protected:
+ LocalSocketTest() {
+ int sock_pair[2];
+ EXPECT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sock_pair));
+ sock_pair_[0].set(sock_pair[0]);
+ sock_pair_[1].set(sock_pair[1]);
+
+ // For tests using actual I/O we use a timer to prevent hangup
+ // due to a bug. Set up the signal handler for the timer here.
+ g_io_service_ = &io_service_;
+ prev_handler_ = std::signal(SIGALRM, stopIOService);
+ }
+
+ ~LocalSocketTest() {
+ alarm(0);
+ // reset the global to NULL to detect any invalid access to freed
+ // io_service (this shouldn't happen, so we don't change stopIOService
+ // itself)
+ g_io_service_ = NULL;
+ std::signal(SIGALRM, prev_handler_);
+ }
+
+ // Common set of tests for async read
+ void checkAsyncRead(size_t data_len);
+
+ IOService io_service_;
+ ScopedSocket sock_pair_[2];
+ std::vector<uint8_t> read_buf_;
+private:
+ static IOService* g_io_service_; // will be set to &io_service_
+ void (*prev_handler_)(int);
+
+ // SIGALRM handler to prevent hangup. This must be a static method
+ // so it can be passed to std::signal().
+ static void stopIOService(int) {
+ g_io_service_->stop();
+ }
+};
+
+IOService* LocalSocketTest::g_io_service_ = NULL;
+
+TEST_F(LocalSocketTest, construct) {
+ const int fd = sock_pair_[0].release();
+ LocalSocket sock(io_service_, fd);
+ EXPECT_EQ(fd, sock.getNative());
+ EXPECT_EQ(AF_UNIX, sock.getProtocol());
+}
+
+TEST_F(LocalSocketTest, constructError) {
+ // try to construct a LocalSocket object with a closed socket. It should
+ // fail.
+ const int fd = sock_pair_[0].release();
+ EXPECT_EQ(0, close(fd));
+ EXPECT_THROW(LocalSocket(io_service_, fd), IOError);
+}
+
+TEST_F(LocalSocketTest, autoClose) {
+ // Confirm that passed FD will be closed on destruction of LocalSocket
+ const int fd = sock_pair_[0].release();
+ {
+ LocalSocket sock(io_service_, fd);
+ }
+ // fd should have been closed, so close() should fail (we assume there's
+ // no other open() call since then)
+ EXPECT_EQ(-1, ::close(fd));
+}
+
+void
+callback(const std::string& error, IOService* io_service, bool* called,
+ bool expect_error)
+{
+ if (expect_error) {
+ EXPECT_NE("", error);
+ } else {
+ EXPECT_EQ("", error);
+ }
+ *called = true;
+ io_service->stop();
+}
+
+void
+LocalSocketTest::checkAsyncRead(size_t data_len) {
+ LocalSocket sock(io_service_, sock_pair_[0].release());
+ bool callback_called = false;
+ read_buf_.resize(data_len);
+ sock.asyncRead(boost::bind(&callback, _1, &io_service_, &callback_called,
+ false), &read_buf_[0], data_len);
+
+ std::vector<uint8_t> expected_data(data_len);
+ for (size_t i = 0; i < data_len; ++i) {
+ expected_data[i] = i & 0xff;
+ }
+ alarm(IO_TIMEOUT);
+ // If write blocks, it will eventually fail due to signal interruption.
+ // Since io_service has been stopped already, run() would immediately
+ // return and test should complete (with failure). But to make very sure
+ // it never cause hangup we rather return from the test at the point of
+ // failure of write. In either case it signals a failure and need for
+ // a fix.
+ ASSERT_EQ(data_len, write(sock_pair_[1].get(), &expected_data[0],
+ data_len));
+ io_service_.run();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(0, std::memcmp(&expected_data[0], &read_buf_[0], data_len));
+
+}
+
+TEST_F(LocalSocketTest, asyncRead) {
+ // A simple case of asynchronous read: wait for 1 byte and successfully
+ // read it in the run() loop.
+ checkAsyncRead(1);
+}
+
+TEST_F(LocalSocketTest, asyncLargeRead) {
+ // Similar to the previous case, but for moderately larger data.
+ // (for the moment) we don't expect to use this interface with much
+ // larger data that could cause blocking write.
+ checkAsyncRead(1024);
+}
+
+TEST_F(LocalSocketTest, asyncPartialRead) {
+ alarm(IO_TIMEOUT);
+
+ // specify reading 4 bytes of data, and send 3 bytes. It shouldn't cause
+ // callback. while we actually don't use the buffer, we'll initialize it
+ // to make valgrind happy.
+ char recv_buf[4];
+ std::memset(recv_buf, 0, sizeof(recv_buf));
+ bool callback_called = false;
+ LocalSocket sock(io_service_, sock_pair_[0].release());
+ sock.asyncRead(boost::bind(&callback, _1, &io_service_, &callback_called,
+ false), recv_buf, sizeof(recv_buf));
+ EXPECT_EQ(3, write(sock_pair_[1].get(), recv_buf, 3));
+
+ // open another pair of sockets so we can stop the IO service after run.
+ int socks[2];
+ char ch = 0;
+ EXPECT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, socks));
+ ScopedSocket aux_sockpair[2];
+ aux_sockpair[0].set(socks[0]);
+ aux_sockpair[1].set(socks[1]);
+ LocalSocket aux_sock(io_service_, aux_sockpair[0].get());
+ aux_sockpair[0].release(); // on successful construction we should release
+ bool aux_callback_called = false;
+ aux_sock.asyncRead(boost::bind(&callback, _1, &io_service_,
+ &aux_callback_called, false), &ch, 1);
+ EXPECT_EQ(1, write(aux_sockpair[1].get(), &ch, 1));
+
+ // run the IO service, it will soon be stopped via the auxiliary callback.
+ // the main callback shouldn't be called.
+ io_service_.run();
+ EXPECT_FALSE(callback_called);
+ EXPECT_TRUE(aux_callback_called);
+}
+
+TEST_F(LocalSocketTest, asyncReadError) {
+ const int sock_fd = sock_pair_[0].release();
+ LocalSocket sock(io_service_, sock_fd);
+ bool callback_called = false;
+ read_buf_.resize(1);
+ read_buf_.at(0) = 53; // dummy data to check it later
+ const char ch = 35; // send different data to the read socket with data
+ EXPECT_EQ(1, write(sock_pair_[1].get(), &ch, 1));
+ close(sock_fd); // invalidate the read socket
+ // we'll get callback with an error (e.g. 'bad file descriptor)
+ sock.asyncRead(boost::bind(&callback, _1, &io_service_, &callback_called,
+ true), &read_buf_[0], 1);
+
+ io_service_.run();
+ EXPECT_TRUE(callback_called);
+ EXPECT_EQ(53, read_buf_.at(0));
+}
+
+TEST_F(LocalSocketTest, asyncReadThenDestroy) {
+ // destroy the socket before running the IO service. we'll still get
+ // callback with an error.
+ boost::scoped_ptr<LocalSocket> sock(
+ new LocalSocket(io_service_, sock_pair_[0].release()));
+ read_buf_.resize(1);
+ bool callback_called = false;
+ sock->asyncRead(boost::bind(&callback, _1, &io_service_, &callback_called,
+ true), &read_buf_[0], 1);
+ sock.reset();
+
+ io_service_.run();
+ EXPECT_TRUE(callback_called);
+}
+
+}
diff --git a/src/lib/asiolink/tests/tcp_endpoint_unittest.cc b/src/lib/asiolink/tests/tcp_endpoint_unittest.cc
index 6988082..79f330f 100644
--- a/src/lib/asiolink/tests/tcp_endpoint_unittest.cc
+++ b/src/lib/asiolink/tests/tcp_endpoint_unittest.cc
@@ -37,7 +37,7 @@ TEST(TCPEndpointTest, v4Address) {
EXPECT_TRUE(address == endpoint.getAddress());
EXPECT_EQ(test_port, endpoint.getPort());
- EXPECT_EQ(IPPROTO_TCP, endpoint.getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP), endpoint.getProtocol());
EXPECT_EQ(AF_INET, endpoint.getFamily());
}
@@ -50,6 +50,6 @@ TEST(TCPEndpointTest, v6Address) {
EXPECT_TRUE(address == endpoint.getAddress());
EXPECT_EQ(test_port, endpoint.getPort());
- EXPECT_EQ(IPPROTO_TCP, endpoint.getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP), endpoint.getProtocol());
EXPECT_EQ(AF_INET6, endpoint.getFamily());
}
diff --git a/src/lib/asiolink/tests/udp_endpoint_unittest.cc b/src/lib/asiolink/tests/udp_endpoint_unittest.cc
index 03de6b8..507103c 100644
--- a/src/lib/asiolink/tests/udp_endpoint_unittest.cc
+++ b/src/lib/asiolink/tests/udp_endpoint_unittest.cc
@@ -37,7 +37,7 @@ TEST(UDPEndpointTest, v4Address) {
EXPECT_TRUE(address == endpoint.getAddress());
EXPECT_EQ(test_port, endpoint.getPort());
- EXPECT_EQ(IPPROTO_UDP, endpoint.getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP), endpoint.getProtocol());
EXPECT_EQ(AF_INET, endpoint.getFamily());
}
@@ -50,6 +50,6 @@ TEST(UDPEndpointTest, v6Address) {
EXPECT_TRUE(address == endpoint.getAddress());
EXPECT_EQ(test_port, endpoint.getPort());
- EXPECT_EQ(IPPROTO_UDP, endpoint.getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP), endpoint.getProtocol());
EXPECT_EQ(AF_INET6, endpoint.getFamily());
}
diff --git a/src/lib/asiolink/udp_socket.h b/src/lib/asiolink/udp_socket.h
index 5712957..4ac49c9 100644
--- a/src/lib/asiolink/udp_socket.h
+++ b/src/lib/asiolink/udp_socket.h
@@ -19,7 +19,6 @@
#error "asio.hpp must be included before including this, see asiolink.h as to why"
#endif
-#include <log/dummylog.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h> // for some IPC/network system calls
diff --git a/src/lib/cache/.gitignore b/src/lib/cache/.gitignore
index a33f3f0..65ae291 100644
--- a/src/lib/cache/.gitignore
+++ b/src/lib/cache/.gitignore
@@ -1,2 +1,3 @@
/cache_messages.cc
/cache_messages.h
+/s-messages
diff --git a/src/lib/cache/Makefile.am b/src/lib/cache/Makefile.am
index 00ca16e..7a84dd6 100644
--- a/src/lib/cache/Makefile.am
+++ b/src/lib/cache/Makefile.am
@@ -36,9 +36,12 @@ nodist_libb10_cache_la_SOURCES = cache_messages.cc cache_messages.h
BUILT_SOURCES = cache_messages.cc cache_messages.h
-cache_messages.cc cache_messages.h: cache_messages.mes
+cache_messages.cc cache_messages.h: s-messages
+
+s-messages: cache_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/cache/cache_messages.mes
+ touch $@
-CLEANFILES = *.gcno *.gcda cache_messages.cc cache_messages.h
+CLEANFILES = *.gcno *.gcda cache_messages.cc cache_messages.h s-messages
EXTRA_DIST = cache_messages.mes
diff --git a/src/lib/cc/.gitignore b/src/lib/cc/.gitignore
index d1e56df..d375181 100644
--- a/src/lib/cc/.gitignore
+++ b/src/lib/cc/.gitignore
@@ -3,3 +3,4 @@
/proto_defs.h
/session_config.h
/session_config.h.pre
+/s-messages
diff --git a/src/lib/cc/Makefile.am b/src/lib/cc/Makefile.am
index 06e9309..55c14c8 100644
--- a/src/lib/cc/Makefile.am
+++ b/src/lib/cc/Makefile.am
@@ -13,12 +13,6 @@ if USE_GXX
AM_CXXFLAGS += -Wno-unused-parameter
AM_CXXFLAGS += -fno-strict-aliasing
endif
-if USE_CLANGPP
-# Likewise, ASIO header files will trigger various warnings with clang++.
-# Worse, there doesn't seem to be any option to disable one of the warnings
-# in any way, so we need to turn off -Werror.
-AM_CXXFLAGS += -Wno-error
-endif
lib_LTLIBRARIES = libb10-cc.la
libb10_cc_la_SOURCES = data.cc data.h session.cc session.h
@@ -29,13 +23,16 @@ nodist_libb10_cc_la_SOURCES += proto_defs.h
libb10_cc_la_LIBADD = $(top_builddir)/src/lib/log/libb10-log.la
CLEANFILES = *.gcno *.gcda session_config.h cc_messages.cc cc_messages.h \
- proto_defs.h
+ proto_defs.h s-messages
session_config.h: session_config.h.pre
$(SED) -e "s|@@LOCALSTATEDIR@@|$(localstatedir)|" session_config.h.pre >$@
-cc_messages.cc cc_messages.h: cc_messages.mes
+cc_messages.cc cc_messages.h: s-messages
+
+s-messages: cc_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/cc/cc_messages.mes
+ touch $@
BUILT_SOURCES = session_config.h cc_messages.cc cc_messages.h proto_defs.h
diff --git a/src/lib/cc/data.cc b/src/lib/cc/data.cc
index af3602a..a06ccdd 100644
--- a/src/lib/cc/data.cc
+++ b/src/lib/cc/data.cc
@@ -24,8 +24,11 @@
#include <iostream>
#include <string>
#include <sstream>
+#include <cerrno>
+#include <climits>
#include <boost/algorithm/string.hpp> // for iequals
+#include <boost/lexical_cast.hpp>
#include <cmath>
@@ -58,7 +61,7 @@ Element::toWire(std::ostream& ss) const {
}
bool
-Element::getValue(long int&) const {
+Element::getValue(int64_t&) const {
return (false);
}
@@ -88,7 +91,7 @@ Element::getValue(std::map<std::string, ConstElementPtr>&) const {
}
bool
-Element::setValue(const long int) {
+Element::setValue(const long long int) {
return (false);
}
@@ -142,6 +145,11 @@ Element::size() const {
isc_throw(TypeError, "size() called on a non-list Element");
}
+bool
+Element::empty() const {
+ isc_throw(TypeError, "empty() called on a non-list Element");
+}
+
ConstElementPtr
Element::get(const std::string&) const {
isc_throw(TypeError, "get(string) called on a non-map Element");
@@ -206,8 +214,8 @@ Element::create() {
}
ElementPtr
-Element::create(const long int i) {
- return (ElementPtr(new IntElement(i)));
+Element::create(const long long int i) {
+ return (ElementPtr(new IntElement(static_cast<int64_t>(i))));
}
ElementPtr
@@ -267,11 +275,11 @@ skipChars(std::istream& in, const char* chars, int& line, int& pos) {
}
// skip on the input stream to one of the characters in chars
-// if another character is found this function returns false
+// if another character is found this function throws JSONError
// unless that character is specified in the optional may_skip
//
-// the character found is left on the stream
-void
+// It returns the found character (as an int value).
+int
skipTo(std::istream& in, const std::string& file, int& line,
int& pos, const char* chars, const char* may_skip="")
{
@@ -290,18 +298,18 @@ skipTo(std::istream& in, const std::string& file, int& line,
if (in.peek() == '\n') {
pos = 1;
++line;
+ } else {
+ ++pos;
}
in.ignore();
- ++pos;
}
- in.putback(c);
- --pos;
- return;
+ return (c);
} else {
throwJSONError(std::string("'") + std::string(1, c) + "' read, one of \"" + chars + "\" expected", file, line, pos);
}
}
throwJSONError(std::string("EOF read, one of \"") + chars + "\" expected", file, line, pos);
+ return (c); // shouldn't reach here, but some compilers require it
}
// TODO: Should we check for all other official escapes here (and
@@ -389,37 +397,24 @@ numberFromStringstream(std::istream& in, int& pos) {
// Should we change from IntElement and DoubleElement to NumberElement
// that can also hold an e value? (and have specific getters if the
// value is larger than an int can handle)
+//
ElementPtr
fromStringstreamNumber(std::istream& in, int& pos) {
- long int i;
- double d = 0.0;
- bool is_double = false;
- char* endptr;
-
std::string number = numberFromStringstream(in, pos);
- i = strtol(number.c_str(), &endptr, 10);
- if (*endptr != '\0') {
- d = strtod(number.c_str(), &endptr);
- is_double = true;
- if (*endptr != '\0') {
- isc_throw(JSONError, std::string("Bad number: ") + number);
- } else {
- if (d == HUGE_VAL || d == -HUGE_VAL) {
- isc_throw(JSONError, std::string("Number overflow: ") + number);
- }
+ if (number.find_first_of(".eE") < number.size()) {
+ try {
+ return (Element::create(boost::lexical_cast<double>(number)));
+ } catch (const boost::bad_lexical_cast&) {
+ isc_throw(JSONError, std::string("Number overflow: ") + number);
}
} else {
- if (i == LONG_MAX || i == LONG_MIN) {
+ try {
+ return (Element::create(boost::lexical_cast<int64_t>(number)));
+ } catch (const boost::bad_lexical_cast&) {
isc_throw(JSONError, std::string("Number overflow: ") + number);
}
}
-
- if (is_double) {
- return (Element::create(d));
- } else {
- return (Element::create(i));
- }
}
ElementPtr
@@ -471,10 +466,11 @@ fromStringstreamList(std::istream& in, const std::string& file, int& line,
if (in.peek() != ']') {
cur_list_element = Element::fromJSON(in, file, line, pos);
list->add(cur_list_element);
- skipTo(in, file, line, pos, ",]", WHITESPACE);
+ c = skipTo(in, file, line, pos, ",]", WHITESPACE);
+ } else {
+ c = in.get();
+ ++pos;
}
- c = in.get();
- pos++;
}
return (list);
}
@@ -497,15 +493,11 @@ fromStringstreamMap(std::istream& in, const std::string& file, int& line,
skipTo(in, file, line, pos, ":", WHITESPACE);
// skip the :
- in.ignore();
- pos++;
ConstElementPtr value = Element::fromJSON(in, file, line, pos);
map->set(key, value);
- skipTo(in, file, line, pos, ",}", WHITESPACE);
- c = in.get();
- pos++;
+ c = skipTo(in, file, line, pos, ",}", WHITESPACE);
}
}
return (map);
@@ -601,6 +593,7 @@ Element::fromJSON(std::istream& in, const std::string& file, int& line,
case '+':
case '.':
in.putback(c);
+ --pos;
element = fromStringstreamNumber(in, pos);
el_read = true;
break;
@@ -609,17 +602,20 @@ Element::fromJSON(std::istream& in, const std::string& file, int& line,
case 'f':
case 'F':
in.putback(c);
+ --pos;
element = fromStringstreamBool(in, file, line, pos);
el_read = true;
break;
case 'n':
case 'N':
in.putback(c);
+ --pos;
element = fromStringstreamNull(in, file, line, pos);
el_read = true;
break;
case '"':
in.putback('"');
+ --pos;
element = fromStringstreamString(in, file, line, pos);
el_read = true;
break;
@@ -688,10 +684,9 @@ NullElement::toJSON(std::ostream& ss) const {
void
StringElement::toJSON(std::ostream& ss) const {
ss << "\"";
- char c;
const std::string& str = stringValue();
for (size_t i = 0; i < str.size(); ++i) {
- c = str[i];
+ const char c = str[i];
// Escape characters as defined in JSON spec
// Note that we do not escape forward slash; this
// is allowed, but not mandatory.
diff --git a/src/lib/cc/data.h b/src/lib/cc/data.h
index d0e0d03..bea9dbb 100644
--- a/src/lib/cc/data.h
+++ b/src/lib/cc/data.h
@@ -124,7 +124,7 @@ public:
/// If you want an exception-safe getter method, use
/// getValue() below
//@{
- virtual long int intValue() const
+ virtual int64_t intValue() const
{ isc_throw(TypeError, "intValue() called on non-integer Element"); };
virtual double doubleValue() const
{ isc_throw(TypeError, "doubleValue() called on non-double Element"); };
@@ -151,7 +151,7 @@ public:
/// data to the given reference and returning true
///
//@{
- virtual bool getValue(long int& t) const;
+ virtual bool getValue(int64_t& t) const;
virtual bool getValue(double& t) const;
virtual bool getValue(bool& t) const;
virtual bool getValue(std::string& t) const;
@@ -166,8 +166,12 @@ public:
/// the right type. Set the value and return true if the Elements
/// is of the correct type
///
+ /// Notes: Read notes of IntElement definition about the use of
+ /// long long int, long int and int.
//@{
- virtual bool setValue(const long int v);
+ virtual bool setValue(const long long int v);
+ bool setValue(const long int i) { return (setValue(static_cast<long long int>(i))); };
+ bool setValue(const int i) { return (setValue(static_cast<long long int>(i))); };
virtual bool setValue(const double v);
virtual bool setValue(const bool t);
virtual bool setValue(const std::string& v);
@@ -206,6 +210,9 @@ public:
/// Returns the number of elements in the list.
virtual size_t size() const;
+
+ /// Return true if there are no elements in the list.
+ virtual bool empty() const;
//@}
@@ -271,10 +278,14 @@ public:
/// underlying system).
/// (Note that that is different from an NullElement, which
/// represents an empty value, and is created with Element::create())
+ ///
+ /// Notes: Read notes of IntElement definition about the use of
+ /// long long int, long int and int.
//@{
static ElementPtr create();
- static ElementPtr create(const long int i);
- static ElementPtr create(const int i) { return (create(static_cast<long int>(i))); };
+ static ElementPtr create(const long long int i);
+ static ElementPtr create(const int i) { return (create(static_cast<long long int>(i))); };
+ static ElementPtr create(const long int i) { return (create(static_cast<long long int>(i))); };
static ElementPtr create(const double d);
static ElementPtr create(const bool b);
static ElementPtr create(const std::string& s);
@@ -370,16 +381,27 @@ public:
//@}
};
+/// Notes: IntElement type is changed to int64_t.
+/// Due to C++ problems on overloading and automatic type conversion,
+/// (C++ tries to convert integer type values and reference/pointer
+/// if value types do not match exactly)
+/// We decided the storage as int64_t,
+/// three (long long, long, int) override function defintions
+/// and cast int/long/long long to int64_t via long long.
+/// Therefore, call by value methods (create, setValue) have three
+/// (int,long,long long) definitions. Others use int64_t.
+///
class IntElement : public Element {
- long int i;
+ int64_t i;
+private:
public:
- IntElement(long int v) : Element(integer), i(v) { }
- long int intValue() const { return (i); }
+ IntElement(int64_t v) : Element(integer), i(v) { }
+ int64_t intValue() const { return (i); }
using Element::getValue;
- bool getValue(long int& t) const { t = i; return (true); }
+ bool getValue(int64_t& t) const { t = i; return (true); }
using Element::setValue;
- bool setValue(const long int v) { i = v; return (true); }
+ bool setValue(long long int v) { i = v; return (true); }
void toJSON(std::ostream& ss) const;
bool equals(const Element& other) const;
};
@@ -460,6 +482,7 @@ public:
void remove(int i) { l.erase(l.begin() + i); };
void toJSON(std::ostream& ss) const;
size_t size() const { return (l.size()); }
+ bool empty() const { return (l.empty()); }
bool equals(const Element& other) const;
};
diff --git a/src/lib/cc/proto_defs.cc b/src/lib/cc/proto_defs.cc
index 8f7fb91..2806c2a 100644
--- a/src/lib/cc/proto_defs.cc
+++ b/src/lib/cc/proto_defs.cc
@@ -43,6 +43,8 @@ const char* const CC_COMMAND_STOP = "stop";
// The wildcards of some headers
const char* const CC_TO_WILDCARD = "*";
const char* const CC_INSTANCE_WILDCARD = "*";
+// Prefixes for groups
+const char* const CC_GROUP_NOTIFICATION_PREFIX = "notifications/";
// Reply codes
const int CC_REPLY_NO_RECPT = -1;
const int CC_REPLY_SUCCESS = 0;
@@ -50,6 +52,7 @@ const int CC_REPLY_SUCCESS = 0;
const char *const CC_PAYLOAD_LNAME = "lname";
const char *const CC_PAYLOAD_RESULT = "result";
const char *const CC_PAYLOAD_COMMAND = "command";
+const char *const CC_PAYLOAD_NOTIFICATION = "notification";
}
}
diff --git a/src/lib/cc/session.cc b/src/lib/cc/session.cc
index cb1ca39..0a2f11d 100644
--- a/src/lib/cc/session.cc
+++ b/src/lib/cc/session.cc
@@ -535,7 +535,7 @@ Session::reply(ConstElementPtr envelope, ConstElementPtr newmsg) {
bool
Session::hasQueuedMsgs() const {
- return (impl_->queue_->size() > 0);
+ return (!impl_->queue_->empty());
}
void
diff --git a/src/lib/cc/tests/Makefile.am b/src/lib/cc/tests/Makefile.am
index 1c2b4b8..2afcf14 100644
--- a/src/lib/cc/tests/Makefile.am
+++ b/src/lib/cc/tests/Makefile.am
@@ -6,9 +6,6 @@ AM_CXXFLAGS = $(B10_CXXFLAGS)
if USE_GXX #XXX: see ../Makefile.am
AM_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_CLANGPP
-AM_CXXFLAGS += -Wno-error
-endif
if USE_STATIC_LINK
AM_LDFLAGS = -static
diff --git a/src/lib/cc/tests/data_unittests.cc b/src/lib/cc/tests/data_unittests.cc
index 9f015d2..fa8e5fc 100644
--- a/src/lib/cc/tests/data_unittests.cc
+++ b/src/lib/cc/tests/data_unittests.cc
@@ -15,6 +15,7 @@
#include <gtest/gtest.h>
#include <boost/foreach.hpp>
#include <boost/assign/std/vector.hpp>
+#include <climits>
#include <cc/data.h>
@@ -97,16 +98,24 @@ TEST(Element, from_and_to_json) {
sv.push_back("\"\xFF\"");
BOOST_FOREACH(const std::string& s, sv) {
- // test << operator, which uses Element::str()
- std::ostringstream stream;
- el = Element::fromJSON(s);
- stream << *el;
- EXPECT_EQ(s, stream.str());
-
- // test toWire(ostream), which should also be the same now
- std::ostringstream wire_stream;
- el->toWire(wire_stream);
- EXPECT_EQ(s, wire_stream.str());
+ // Test two types of fromJSON(): with string and istream.
+ for (int i = 0; i < 2; ++i) {
+ // test << operator, which uses Element::str()
+ if (i == 0) {
+ el = Element::fromJSON(s);
+ } else {
+ std::istringstream iss(s);
+ el = Element::fromJSON(iss);
+ }
+ std::ostringstream stream;
+ stream << *el;
+ EXPECT_EQ(s, stream.str());
+
+ // test toWire(ostream), which should also be the same now
+ std::ostringstream wire_stream;
+ el->toWire(wire_stream);
+ EXPECT_EQ(s, wire_stream.str());
+ }
}
// some parse errors
@@ -146,6 +155,17 @@ TEST(Element, from_and_to_json) {
EXPECT_EQ("100", Element::fromJSON("1e2")->str());
EXPECT_EQ("100", Element::fromJSON("+1e2")->str());
EXPECT_EQ("-100", Element::fromJSON("-1e2")->str());
+
+ EXPECT_NO_THROW({
+ EXPECT_EQ("9223372036854775807", Element::fromJSON("9223372036854775807")->str());
+ });
+ EXPECT_NO_THROW({
+ EXPECT_EQ("-9223372036854775808", Element::fromJSON("-9223372036854775808")->str());
+ });
+ EXPECT_THROW({
+ EXPECT_NE("9223372036854775808", Element::fromJSON("9223372036854775808")->str());
+ }, JSONError);
+
EXPECT_EQ("0.01", Element::fromJSON("1e-2")->str());
EXPECT_EQ("0.01", Element::fromJSON(".01")->str());
EXPECT_EQ("-0.01", Element::fromJSON("-1e-2")->str());
@@ -173,6 +193,8 @@ TEST(Element, from_and_to_json) {
EXPECT_THROW(Element::fromJSON("-1.1e12345678901234567890")->str(), JSONError);
EXPECT_THROW(Element::fromJSON("1e12345678901234567890")->str(), JSONError);
EXPECT_THROW(Element::fromJSON("1e50000")->str(), JSONError);
+ // number underflow
+ // EXPECT_THROW(Element::fromJSON("1.1e-12345678901234567890")->str(), JSONError);
}
@@ -180,7 +202,10 @@ template <typename T>
void
testGetValueInt() {
T el;
- long int i;
+ int64_t i;
+ int32_t i32;
+ long l;
+ long long ll;
double d;
bool b;
std::string s;
@@ -188,7 +213,9 @@ testGetValueInt() {
std::map<std::string, ConstElementPtr> m;
el = Element::create(1);
- EXPECT_NO_THROW(el->intValue());
+ EXPECT_NO_THROW({
+ EXPECT_EQ(1, el->intValue());
+ });
EXPECT_THROW(el->doubleValue(), TypeError);
EXPECT_THROW(el->boolValue(), TypeError);
EXPECT_THROW(el->stringValue(), TypeError);
@@ -201,13 +228,44 @@ testGetValueInt() {
EXPECT_FALSE(el->getValue(v));
EXPECT_FALSE(el->getValue(m));
EXPECT_EQ(1, i);
+
+ el = Element::create(9223372036854775807LL);
+ EXPECT_NO_THROW({
+ EXPECT_EQ(9223372036854775807LL, el->intValue());
+ });
+ EXPECT_TRUE(el->getValue(i));
+ EXPECT_EQ(9223372036854775807LL, i);
+
+ ll = 9223372036854775807LL;
+ el = Element::create(ll);
+ EXPECT_NO_THROW({
+ EXPECT_EQ(ll, el->intValue());
+ });
+ EXPECT_TRUE(el->getValue(i));
+ EXPECT_EQ(ll, i);
+
+ i32 = 2147483647L;
+ el = Element::create(i32);
+ EXPECT_NO_THROW({
+ EXPECT_EQ(i32, el->intValue());
+ });
+ EXPECT_TRUE(el->getValue(i));
+ EXPECT_EQ(i32, i);
+
+ l = 2147483647L;
+ el = Element::create(l);
+ EXPECT_NO_THROW({
+ EXPECT_EQ(l, el->intValue());
+ });
+ EXPECT_TRUE(el->getValue(i));
+ EXPECT_EQ(l, i);
}
template <typename T>
void
testGetValueDouble() {
T el;
- long int i;
+ int64_t i;
double d;
bool b;
std::string s;
@@ -234,7 +292,7 @@ template <typename T>
void
testGetValueBool() {
T el;
- long int i;
+ int64_t i;
double d;
bool b;
std::string s;
@@ -261,7 +319,7 @@ template <typename T>
void
testGetValueString() {
T el;
- long int i;
+ int64_t i;
double d;
bool b;
std::string s;
@@ -288,7 +346,7 @@ template <typename T>
void
testGetValueList() {
T el;
- long int i;
+ int64_t i;
double d;
bool b;
std::string s;
@@ -315,7 +373,7 @@ template <typename T>
void
testGetValueMap() {
T el;
- long int i;
+ int64_t i;
double d;
bool b;
std::string s;
@@ -343,7 +401,7 @@ TEST(Element, create_and_value_throws) {
// incorrect type is requested
ElementPtr el;
ConstElementPtr cel;
- long int i = 0;
+ int64_t i = 0;
double d = 0.0;
bool b = false;
std::string s("asdf");
@@ -368,6 +426,7 @@ TEST(Element, create_and_value_throws) {
EXPECT_THROW(el->add(el), TypeError);
EXPECT_THROW(el->remove(1), TypeError);
EXPECT_THROW(el->size(), TypeError);
+ EXPECT_THROW(el->empty(), TypeError);
EXPECT_THROW(el->get("foo"), TypeError);
EXPECT_THROW(el->set("foo", el), TypeError);
EXPECT_THROW(el->remove("foo"), TypeError);
@@ -391,6 +450,7 @@ TEST(Element, create_and_value_throws) {
EXPECT_THROW(el->add(el), TypeError);
EXPECT_THROW(el->remove(1), TypeError);
EXPECT_THROW(el->size(), TypeError);
+ EXPECT_THROW(el->empty(), TypeError);
EXPECT_THROW(el->get("foo"), TypeError);
EXPECT_THROW(el->set("foo", el), TypeError);
EXPECT_THROW(el->remove("foo"), TypeError);
@@ -414,6 +474,7 @@ TEST(Element, create_and_value_throws) {
EXPECT_THROW(el->add(el), TypeError);
EXPECT_THROW(el->remove(1), TypeError);
EXPECT_THROW(el->size(), TypeError);
+ EXPECT_THROW(el->empty(), TypeError);
EXPECT_THROW(el->get("foo"), TypeError);
EXPECT_THROW(el->set("foo", el), TypeError);
EXPECT_THROW(el->remove("foo"), TypeError);
@@ -437,6 +498,7 @@ TEST(Element, create_and_value_throws) {
EXPECT_THROW(el->add(el), TypeError);
EXPECT_THROW(el->remove(1), TypeError);
EXPECT_THROW(el->size(), TypeError);
+ EXPECT_THROW(el->empty(), TypeError);
EXPECT_THROW(el->get("foo"), TypeError);
EXPECT_THROW(el->set("foo", el), TypeError);
EXPECT_THROW(el->remove("foo"), TypeError);
@@ -447,8 +509,10 @@ TEST(Element, create_and_value_throws) {
testGetValueList<ConstElementPtr>();
el = Element::createList();
+ EXPECT_TRUE(el->empty());
v.push_back(Element::create(1));
EXPECT_TRUE(el->setValue(v));
+ EXPECT_FALSE(el->empty());
EXPECT_EQ("[ 1 ]", el->str());
testGetValueMap<ElementPtr>();
diff --git a/src/lib/config/.gitignore b/src/lib/config/.gitignore
index c7ec9d3..d666f24 100644
--- a/src/lib/config/.gitignore
+++ b/src/lib/config/.gitignore
@@ -1,2 +1,3 @@
/config_messages.cc
/config_messages.h
+/s-messages
diff --git a/src/lib/config/Makefile.am b/src/lib/config/Makefile.am
index b4fc2e0..9820f08 100644
--- a/src/lib/config/Makefile.am
+++ b/src/lib/config/Makefile.am
@@ -6,8 +6,11 @@ AM_CPPFLAGS += -I$(top_srcdir)/src/lib/log -I$(top_builddir)/src/lib/log
AM_CPPFLAGS += $(BOOST_INCLUDES)
# Define rule to build logging source files from message file
-config_messages.h config_messages.cc: config_messages.mes
+config_messages.h config_messages.cc: s-messages
+
+s-messages: config_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/config/config_messages.mes
+ touch $@
BUILT_SOURCES = config_messages.h config_messages.cc
@@ -27,4 +30,4 @@ nodist_libb10_cfgclient_la_SOURCES = config_messages.h config_messages.cc
# The message file should be in the distribution.
EXTRA_DIST = config_messages.mes
-CLEANFILES = *.gcno *.gcda config_messages.h config_messages.cc
+CLEANFILES = *.gcno *.gcda config_messages.h config_messages.cc s-messages
diff --git a/src/lib/config/ccsession.cc b/src/lib/config/ccsession.cc
index d094ab9..10bc728 100644
--- a/src/lib/config/ccsession.cc
+++ b/src/lib/config/ccsession.cc
@@ -144,7 +144,7 @@ parseCommand(ConstElementPtr& arg, ConstElementPtr command) {
command->contains(isc::cc::CC_PAYLOAD_COMMAND)) {
ConstElementPtr cmd = command->get(isc::cc::CC_PAYLOAD_COMMAND);
if (cmd->getType() == Element::list &&
- cmd->size() > 0 &&
+ !cmd->empty() &&
cmd->get(0)->getType() == Element::string) {
if (cmd->size() > 1) {
arg = cmd->get(1);
@@ -595,6 +595,8 @@ ModuleCCSession::checkModuleCommand(const std::string& cmd_str,
"Command given but no "
"command handler for module"));
}
+ } else if (unhandled_callback_) {
+ unhandled_callback_(cmd_str, target_module, arg);
}
return (ElementPtr());
}
@@ -609,6 +611,11 @@ ModuleCCSession::checkCommand() {
return (0);
}
+ // In case it is notification, eat it.
+ if (checkNotification(routing, data)) {
+ return (0);
+ }
+
/* ignore result messages (in case we're out of sync, to prevent
* pingpongs */
if (data->getType() != Element::map ||
@@ -884,5 +891,100 @@ ModuleCCSession::rpcCall(const std::string &command, const std::string &group,
}
}
+void
+ModuleCCSession::notify(const std::string& group, const std::string& name,
+ const ConstElementPtr& params)
+{
+ const ElementPtr message(Element::createMap());
+ const ElementPtr notification(Element::createList());
+ notification->add(Element::create(name));
+ if (params) {
+ notification->add(params);
+ }
+ message->set(isc::cc::CC_PAYLOAD_NOTIFICATION, notification);
+ groupSendMsg(message, isc::cc::CC_GROUP_NOTIFICATION_PREFIX + group,
+ isc::cc::CC_INSTANCE_WILDCARD,
+ isc::cc::CC_TO_WILDCARD, false);
+}
+
+ModuleCCSession::NotificationID
+ModuleCCSession::subscribeNotification(const std::string& notification_group,
+ const NotificationCallback& callback)
+{
+ // Either insert a new empty list of callbacks or get an existing one.
+ // Either way, get the iterator for its position.
+ const std::pair<SubscribedNotifications::iterator, bool>& inserted =
+ notifications_.insert(
+ std::pair<std::string, NotificationCallbacks>(notification_group,
+ NotificationCallbacks()));
+ if (inserted.second) {
+ // It was newly inserted. In that case, we need to subscribe to the
+ // group.
+ session_.subscribe(isc::cc::CC_GROUP_NOTIFICATION_PREFIX +
+ notification_group);
+ }
+ // Insert the callback to the chain
+ NotificationCallbacks& callbacks = inserted.first->second;
+ const NotificationCallbacks::iterator& callback_id =
+ callbacks.insert(callbacks.end(), callback);
+ // Just pack the iterators to form the ID
+ return (NotificationID(inserted.first, callback_id));
+}
+
+void
+ModuleCCSession::unsubscribeNotification(const NotificationID& notification) {
+ NotificationCallbacks& callbacks = notification.first->second;
+ // Remove the callback
+ callbacks.erase(notification.second);
+ // If it became empty, remove it from the map and unsubscribe
+ if (callbacks.empty()) {
+ session_.unsubscribe(isc::cc::CC_GROUP_NOTIFICATION_PREFIX +
+ notification.first->first);
+ notifications_.erase(notification.first);
+ }
+}
+
+bool
+ModuleCCSession::checkNotification(const data::ConstElementPtr& envelope,
+ const data::ConstElementPtr& msg)
+{
+ if (msg->getType() != data::Element::map) {
+ // If it's not a map, then it's not a notification
+ return (false);
+ }
+ if (msg->contains(isc::cc::CC_PAYLOAD_NOTIFICATION)) {
+ // There's a notification inside. Extract its parameters.
+ const std::string& group =
+ envelope->get(isc::cc::CC_HEADER_GROUP)->stringValue();
+ const std::string& notification_group =
+ group.substr(std::string(isc::cc::CC_GROUP_NOTIFICATION_PREFIX).
+ size());
+ const data::ConstElementPtr& notification =
+ msg->get(isc::cc::CC_PAYLOAD_NOTIFICATION);
+ // The first one is the event that happened
+ const std::string& event = notification->get(0)->stringValue();
+ // Any other params are second. But they may be missing
+ const data::ConstElementPtr params =
+ notification->size() == 1 ? data::ConstElementPtr() :
+ notification->get(1);
+ // Find the chain of notification callbacks
+ const SubscribedNotifications::iterator& chain_iter =
+ notifications_.find(notification_group);
+ if (chain_iter == notifications_.end()) {
+ // This means we no longer have any notifications for this group.
+ // This can happen legally as a race condition - if msgq sends
+ // us a notification, but we unsubscribe before we get to it
+ // in the input stream.
+ return (false);
+ }
+ BOOST_FOREACH(const NotificationCallback& callback,
+ chain_iter->second) {
+ callback(event, params);
+ }
+ return (true);
+ }
+ return (false); // Not a notification
+}
+
}
}
diff --git a/src/lib/config/ccsession.h b/src/lib/config/ccsession.h
index 995a5cd..75c3ee6 100644
--- a/src/lib/config/ccsession.h
+++ b/src/lib/config/ccsession.h
@@ -425,6 +425,31 @@ public:
params =
isc::data::ConstElementPtr());
+ /// \brief Send a notification to subscribed users
+ ///
+ /// Send a notification message to all users subscribed to the given
+ /// notification group.
+ ///
+ /// This method does not not block.
+ ///
+ /// See docs/design/ipc-high.txt for details about notifications and
+ /// the format of messages sent.
+ ///
+ /// \throw CCSessionError for low-level communication errors.
+ /// \param notification_group This parameter (indirectly) signifies what
+ /// users should receive the notification. Only the users that
+ /// subscribed to notifications on the same group receive it.
+ /// \param name The name of the event to notify about (for example
+ /// `new_group_member`).
+ /// \param params Other parameters that describe the event. This might
+ /// be, for example, the ID of the new member and the name of the
+ /// group. This can be any data element, but it is common for it to be
+ /// map.
+ void notify(const std::string& notification_group,
+ const std::string& name,
+ const isc::data::ConstElementPtr& params =
+ isc::data::ConstElementPtr());
+
/// \brief Convenience version of rpcCall
///
/// This is exactly the same as the previous version of rpcCall, except
@@ -550,6 +575,101 @@ public:
/// \param id The id of request as returned by groupRecvMsgAsync.
void cancelAsyncRecv(const AsyncRecvRequestID& id);
+ /// \brief Called when a notification comes
+ ///
+ /// The callback should be exception-free. If it raises an exception,
+ /// it'll leak through the event loop up and probably terminate the
+ /// application.
+ ///
+ /// \param event_name The identification of event type.
+ /// \param params The parameters of the event. This may be NULL
+ /// pointer in case no parameters were sent with the event.
+ typedef boost::function<void (const std::string& event_name,
+ const data::ConstElementPtr& params)>
+ NotificationCallback;
+
+ /// \brief Multiple notification callbacks for the same notification
+ typedef std::list<NotificationCallback> NotificationCallbacks;
+
+ /// \brief Mapping from groups to callbacks
+ typedef std::map<std::string, NotificationCallbacks>
+ SubscribedNotifications;
+
+ /// \brief Identification of single callback
+ typedef std::pair<SubscribedNotifications::iterator,
+ NotificationCallbacks::iterator>
+ NotificationID;
+
+ /// \brief Subscribe to a notification group
+ ///
+ /// From now on, every notification that is sent to the given group
+ /// triggers the passed callback.
+ ///
+ /// There may be multiple (independent) callbacks for the same channel.
+ /// This one adds a new one, to the end of the chain (the callbacks
+ /// are called in the same order as they were registered).
+ ///
+ /// \param notification_group The channel of notifications.
+ /// \param callback The callback to be added.
+ /// \return ID of the notification callback. It is an opaque ID and can
+ /// be used to remove this callback.
+ NotificationID subscribeNotification(const std::string& notification_group,
+ const NotificationCallback& callback);
+
+ /// \brief Unsubscribe the callback from its notification group.
+ ///
+ /// Express that the desire for this callback to be executed is no longer
+ /// relevant. All the other callbacks (even for the same notification
+ /// group) are left intact.
+ ///
+ /// \param notification The ID of notification callback returned by
+ /// subscribeNotification.
+ void unsubscribeNotification(const NotificationID& notification);
+
+ /// \brief Subscribe to a group
+ ///
+ /// Wrapper around the CCSession::subscribe.
+ void subscribe(const std::string& group) {
+ session_.subscribe(group, isc::cc::CC_INSTANCE_WILDCARD);
+ }
+
+ /// \brief Unsubscribe from a group.
+ ///
+ /// Wrapper around the CCSession::unsubscribe.
+ void unsubscribe(const std::string& group) {
+ session_.unsubscribe(group, isc::cc::CC_INSTANCE_WILDCARD);
+ }
+
+ /// \brief Callback type for unhandled commands
+ ///
+ /// The type of functions that are not handled by the ModuleCCSession
+ /// because they are not aimed at the module.
+ ///
+ /// The parameters are:
+ /// - Name of the command.
+ /// - The module it was aimed for (may be empty).
+ /// - The parameters of the command.
+ typedef boost::function<void (const std::string&, const std::string&,
+ const isc::data::ConstElementPtr&)>
+ UnhandledCallback;
+
+ /// \brief Register a callback for messages sent to foreign modules.
+ ///
+ /// Usually, a command aimed at foreign module (or sent directly)
+ /// is discarded. By registering a callback here, these can be
+ /// examined.
+ ///
+ /// \note A callback overwrites the previous one set.
+ /// \todo This is a temporary, unclean, solution. A more generic
+ /// one needs to be designed. Also, a solution that is able
+ /// to send an answer would be great.
+ ///
+ /// \param callback The new callback to use. It may be an empty
+ /// function.
+ void setUnhandledCallback(const UnhandledCallback& callback) {
+ unhandled_callback_ = callback;
+ }
+
private:
ModuleSpec readModuleSpecification(const std::string& filename);
void startCheck();
@@ -565,6 +685,8 @@ private:
/// otherwise.
bool checkAsyncRecv(const data::ConstElementPtr& envelope,
const data::ConstElementPtr& msg);
+ bool checkNotification(const data::ConstElementPtr& envelope,
+ const data::ConstElementPtr& msg);
/// \brief Checks if a message with this envelope matches the request
bool requestMatch(const AsyncRecvRequest& request,
const data::ConstElementPtr& envelope) const;
@@ -574,6 +696,8 @@ private:
isc::cc::AbstractSession& session_;
ModuleSpec module_specification_;
AsyncRecvRequests async_recv_requests_;
+ SubscribedNotifications notifications_;
+
isc::data::ConstElementPtr handleConfigUpdate(
isc::data::ConstElementPtr new_config);
@@ -599,6 +723,8 @@ private:
isc::data::ConstElementPtr new_config);
ModuleSpec fetchRemoteSpec(const std::string& module, bool is_filename);
+
+ UnhandledCallback unhandled_callback_;
};
/// \brief Default handler for logging config updates
diff --git a/src/lib/config/config_data.h b/src/lib/config/config_data.h
index 33df82d..7900aa9 100644
--- a/src/lib/config/config_data.h
+++ b/src/lib/config/config_data.h
@@ -29,7 +29,7 @@ namespace config {
/// point to anything defined in the .spec file)
class DataNotFoundError : public isc::Exception {
public:
- DataNotFoundError(const char* file, size_t line, const std::string& what) :
+ DataNotFoundError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) {}
};
diff --git a/src/lib/config/tests/ccsession_unittests.cc b/src/lib/config/tests/ccsession_unittests.cc
index c11cd24..b9c70d0 100644
--- a/src/lib/config/tests/ccsession_unittests.cc
+++ b/src/lib/config/tests/ccsession_unittests.cc
@@ -87,6 +87,65 @@ protected:
const std::string root_name;
};
+void
+notificationCallback(std::vector<std::string>* called,
+ const std::string& id, const std::string& notification,
+ const ConstElementPtr& params)
+{
+ called->push_back(id);
+ EXPECT_EQ("event", notification);
+ EXPECT_TRUE(el("{\"param\": true}")->equals(*params));
+}
+
+TEST_F(CCSessionTest, receiveNotification) {
+ // Not subscribed to the group yet
+ ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL,
+ false, false);
+ EXPECT_FALSE(session.haveSubscription("notifications/group", "*"));
+ std::vector<std::string> called;
+ // Subscribe to the notification. Twice.
+ const ModuleCCSession::NotificationID& first =
+ mccs.subscribeNotification("group", boost::bind(¬ificationCallback,
+ &called, "first",
+ _1, _2));
+ const ModuleCCSession::NotificationID& second =
+ mccs.subscribeNotification("group", boost::bind(¬ificationCallback,
+ &called, "second",
+ _1, _2));
+ EXPECT_TRUE(session.haveSubscription("notifications/group", "*"));
+ EXPECT_TRUE(called.empty());
+ // Send the notification
+ const isc::data::ConstElementPtr msg = el("{"
+ " \"notification\": ["
+ " \"event\", {"
+ " \"param\": true"
+ " }"
+ " ]"
+ " }");
+ session.addMessage(msg, "notifications/group", "*");
+ mccs.checkCommand();
+ ASSERT_EQ(2, called.size());
+ EXPECT_EQ("first", called[0]);
+ EXPECT_EQ("second", called[1]);
+ called.clear();
+ // Unsubscribe one of them
+ mccs.unsubscribeNotification(first);
+ // We are still subscribed to the group and handle the requests
+ EXPECT_TRUE(session.haveSubscription("notifications/group", "*"));
+ // Send the notification
+ session.addMessage(msg, "notifications/group", "*");
+ mccs.checkCommand();
+ ASSERT_EQ(1, called.size());
+ EXPECT_EQ("second", called[0]);
+ // Unsubscribe the other one too. That should cancel the upstream
+ // subscription
+ mccs.unsubscribeNotification(second);
+ EXPECT_FALSE(session.haveSubscription("notifications/group", "*"));
+ // Nothing crashes if out of sync notification comes unexpected
+ session.addMessage(msg, "notifications/group", "*");
+ EXPECT_NO_THROW(mccs.checkCommand());
+}
+
// Test we can send an RPC (command) and get an answer. The answer is success
// in this case.
TEST_F(CCSessionTest, rpcCallSuccess) {
@@ -117,6 +176,57 @@ TEST_F(CCSessionTest, rpcNoRecpt) {
RPCRecipientMissing);
}
+// Test sending a notification
+TEST_F(CCSessionTest, notify) {
+ ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL, false,
+ false);
+ mccs.notify("group", "event", el("{\"param\": true}"));
+ const ConstElementPtr notification(el(
+ "["
+ " \"notifications/group\","
+ " \"*\","
+ " {"
+ " \"notification\": ["
+ " \"event\", {"
+ " \"param\": true"
+ " }"
+ " ]"
+ " },"
+ " -1"
+ "]"));
+ EXPECT_TRUE(notification->equals(*session.getMsgQueue()->get(1))) <<
+ session.getMsgQueue()->get(1)->toWire();
+}
+
+// Test sending a notification
+TEST_F(CCSessionTest, notifyNoParams) {
+ ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL, false,
+ false);
+ mccs.notify("group", "event");
+ const ConstElementPtr notification(el(
+ "["
+ " \"notifications/group\","
+ " \"*\","
+ " {"
+ " \"notification\": [\"event\"]"
+ " },"
+ " -1"
+ "]"));
+ EXPECT_TRUE(notification->equals(*session.getMsgQueue()->get(1))) <<
+ session.getMsgQueue()->get(1)->toWire();
+}
+
+// Try to subscribe and unsubscribe once again
+TEST_F(CCSessionTest, subscribe) {
+ ModuleCCSession mccs(ccspecfile("spec1.spec"), session, NULL, NULL, false,
+ false);
+ EXPECT_FALSE(session.haveSubscription("A group", "*"));
+ mccs.subscribe("A group");
+ EXPECT_TRUE(session.haveSubscription("A group", "*"));
+ mccs.unsubscribe("A group");
+ EXPECT_FALSE(session.haveSubscription("A group", "*"));
+}
+
TEST_F(CCSessionTest, createAnswer) {
ConstElementPtr answer;
answer = createAnswer();
@@ -646,6 +756,16 @@ TEST_F(CCSessionTest, remoteConfig) {
}
}
+void
+callback(std::string* command, std::string* target, ConstElementPtr *params,
+ const std::string& command_real, const std::string& target_real,
+ const ConstElementPtr& params_real)
+{
+ *command = command_real;
+ *target = target_real;
+ *params = params_real;
+}
+
TEST_F(CCSessionTest, ignoreRemoteConfigCommands) {
// client will ask for config
session.getMessages()->add(createAnswer(0, el("{ }")));
@@ -681,6 +801,22 @@ TEST_F(CCSessionTest, ignoreRemoteConfigCommands) {
result = mccs.checkCommand();
EXPECT_EQ(0, session.getMsgQueue()->size());
EXPECT_EQ(0, result);
+
+ // Check that we can get the ignored commands by registering a callback
+ std::string command, target;
+ ConstElementPtr params;
+ mccs.setUnhandledCallback(boost::bind(&callback, &command, &target,
+ ¶ms, _1, _2, _3));
+ session.addMessage(el("{ \"command\": [ \"good_command\","
+ "{\"param\": true} ] }"), "Spec1", "*");
+ EXPECT_EQ(1, session.getMsgQueue()->size());
+ result = mccs.checkCommand();
+ EXPECT_EQ(0, session.getMsgQueue()->size());
+ EXPECT_EQ(0, result);
+
+ EXPECT_EQ("good_command", command);
+ EXPECT_EQ("Spec1", target);
+ EXPECT_TRUE(params && el("{\"param\": true}")->equals(*params));
}
TEST_F(CCSessionTest, initializationFail) {
diff --git a/src/lib/config/tests/fake_session.cc b/src/lib/config/tests/fake_session.cc
index e6d569e..c2fba57 100644
--- a/src/lib/config/tests/fake_session.cc
+++ b/src/lib/config/tests/fake_session.cc
@@ -104,7 +104,7 @@ FakeSession::recvmsg(ConstElementPtr& msg, bool nonblock, int) {
//cout << "[XX] client asks for message " << endl;
if (messages_ &&
messages_->getType() == Element::list &&
- messages_->size() > 0) {
+ !messages_->empty()) {
msg = messages_->get(0);
messages_->remove(0);
} else {
@@ -127,7 +127,7 @@ FakeSession::recvmsg(ConstElementPtr& env, ConstElementPtr& msg, bool nonblock,
env = ElementPtr();
if (messages_ &&
messages_->getType() == Element::list &&
- messages_->size() > 0) {
+ !messages_->empty()) {
// do we need initial message to have env[group] and [to] too?
msg = messages_->get(0);
messages_->remove(0);
@@ -210,13 +210,13 @@ FakeSession::reply(ConstElementPtr envelope, ConstElementPtr newmsg) {
bool
FakeSession::hasQueuedMsgs() const {
- return (msg_queue_ && msg_queue_->size() > 0);
+ return (msg_queue_ && !msg_queue_->empty());
}
ConstElementPtr
FakeSession::getFirstMessage(std::string& group, std::string& to) const {
ConstElementPtr el;
- if (msg_queue_ && msg_queue_->size() > 0) {
+ if (msg_queue_ && !msg_queue_->empty()) {
el = msg_queue_->get(0);
msg_queue_->remove(0);
group = el->get(0)->stringValue();
diff --git a/src/lib/datasrc/.gitignore b/src/lib/datasrc/.gitignore
index 4b199ed..0d473f3 100644
--- a/src/lib/datasrc/.gitignore
+++ b/src/lib/datasrc/.gitignore
@@ -5,3 +5,5 @@
/static.zone
/sqlite3_datasrc_messages.cc
/sqlite3_datasrc_messages.h
+/s-messages1
+/s-messages2
diff --git a/src/lib/datasrc/Makefile.am b/src/lib/datasrc/Makefile.am
index 5422f7d..e358c05 100644
--- a/src/lib/datasrc/Makefile.am
+++ b/src/lib/datasrc/Makefile.am
@@ -22,6 +22,7 @@ CLEANFILES = *.gcno *.gcda datasrc_messages.h datasrc_messages.cc
CLEANFILES += sqlite3_datasrc_messages.h sqlite3_datasrc_messages.cc
CLEANFILES += datasrc_config.h
CLEANFILES += static.zone
+CLEANFILES += s-messages1 s-messages2
lib_LTLIBRARIES = libb10-datasrc.la
libb10_datasrc_la_SOURCES = exceptions.h
@@ -65,10 +66,17 @@ libb10_datasrc_la_LIBADD += $(SQLITE_LIBS)
BUILT_SOURCES = datasrc_config.h datasrc_messages.h datasrc_messages.cc
BUILT_SOURCES += sqlite3_datasrc_messages.h sqlite3_datasrc_messages.cc
-datasrc_messages.h datasrc_messages.cc: Makefile datasrc_messages.mes
+datasrc_messages.h datasrc_messages.cc: s-messages1
+
+s-messages1: Makefile datasrc_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/datasrc/datasrc_messages.mes
-sqlite3_datasrc_messages.h sqlite3_datasrc_messages.cc: Makefile sqlite3_datasrc_messages.mes
+ touch $@
+
+sqlite3_datasrc_messages.h sqlite3_datasrc_messages.cc: s-messages2
+
+s-messages2: Makefile sqlite3_datasrc_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/datasrc/sqlite3_datasrc_messages.mes
+ touch $@
EXTRA_DIST = datasrc_messages.mes sqlite3_datasrc_messages.mes static.zone.pre
diff --git a/src/lib/datasrc/cache_config.cc b/src/lib/datasrc/cache_config.cc
index 3896414..ec3cfeb 100644
--- a/src/lib/datasrc/cache_config.cc
+++ b/src/lib/datasrc/cache_config.cc
@@ -177,7 +177,7 @@ CacheConfig::getLoadAction(const dns::RRClass& rrclass,
assert(datasrc_client_);
// If the specified zone name does not exist in our client of the source,
- // DataSourceError is thrown, which is exactly the result what we
+ // NoSuchZone is thrown, which is exactly the result what we
// want, so no need to handle it.
ZoneIteratorPtr iterator(datasrc_client_->getIterator(zone_name));
if (!iterator) {
diff --git a/src/lib/datasrc/cache_config.h b/src/lib/datasrc/cache_config.h
index ac16415..cfcdc7b 100644
--- a/src/lib/datasrc/cache_config.h
+++ b/src/lib/datasrc/cache_config.h
@@ -17,6 +17,7 @@
#include <exceptions/exceptions.h>
+#include <dns/name.h>
#include <dns/dns_fwd.h>
#include <cc/data.h>
#include <datasrc/memory/load_action.h>
@@ -157,12 +158,13 @@ public:
/// It doesn't throw an exception in this case because the expected caller
/// of this method would handle such a case internally.
///
- /// \throw DataSourceError error happens in the underlying data source
- /// storing the cache data. Most commonly it's because the specified zone
- /// doesn't exist there.
+ /// \throw NoSuchZone The specified zone doesn't exist in the
+ /// underlying data source storing the original data to be cached.
+ /// \throw DataSourceError Other, unexpected but possible error happens
+ /// in the underlying data source.
/// \throw Unexpected Unexpected error happens in the underlying data
- /// source storing the cache data. This shouldn't happen as long as the
- /// data source implementation meets the public API requirement.
+ /// source. This shouldn't happen as long as the data source
+ /// implementation meets the public API requirement.
///
/// \param rrclass The RR class of the zone
/// \param zone_name The origin name of the zone
diff --git a/src/lib/datasrc/client.h b/src/lib/datasrc/client.h
index 6929946..4145700 100644
--- a/src/lib/datasrc/client.h
+++ b/src/lib/datasrc/client.h
@@ -132,8 +132,8 @@ public:
/// \brief A helper structure to represent the search result of
/// \c find().
///
- /// This is a straightforward pair of the result code and a share pointer
- /// to the found zone to represent the result of \c find().
+ /// This is a straightforward tuple of the result code/flags and a shared
+ /// pointer to the found zone to represent the result of \c find().
/// We use this in order to avoid overloading the return value for both
/// the result code ("success" or "not found") and the found object,
/// i.e., avoid using \c NULL to mean "not found", etc.
@@ -146,10 +146,13 @@ public:
/// variables.
struct FindResult {
FindResult(result::Result param_code,
- const ZoneFinderPtr param_zone_finder) :
- code(param_code), zone_finder(param_zone_finder)
+ const ZoneFinderPtr param_zone_finder,
+ result::ResultFlags param_flags = result::FLAGS_DEFAULT) :
+ code(param_code), flags(param_flags),
+ zone_finder(param_zone_finder)
{}
const result::Result code;
+ const result::ResultFlags flags;
const ZoneFinderPtr zone_finder;
};
@@ -184,8 +187,12 @@ public:
/// - \c result::PARTIALMATCH: A zone whose origin is a
/// super domain of \c name is found (but there is no exact match)
/// - \c result::NOTFOUND: For all other cases.
+ /// - \c flags: usually FLAGS_DEFAULT, but if the zone data are not
+ /// available (possibly because an error was detected at load time)
+ /// the ZONE_EMPTY flag is set.
/// - \c zone_finder: Pointer to a \c ZoneFinder object for the found zone
- /// if one is found; otherwise \c NULL.
+ /// if one is found and is not empty (flags doesn't have ZONE_EMPTY);
+ /// otherwise \c NULL.
///
/// A specific derived version of this method may throw an exception.
/// This interface does not specify which exceptions can happen (at least
@@ -201,9 +208,6 @@ public:
/// This allows for traversing the whole zone. The returned object can
/// provide the RRsets one by one.
///
- /// This throws DataSourceError when the zone does not exist in the
- /// datasource.
- ///
/// The default implementation throws isc::NotImplemented. This allows
/// for easy and fast deployment of minimal custom data sources, where
/// the user/implementer doesn't have to care about anything else but
@@ -214,6 +218,14 @@ public:
/// It is not fixed if a concrete implementation of this method can throw
/// anything else.
///
+ /// \throw NoSuchZone the zone does not exist in the datasource.
+ /// \throw Others Possibly implementation specific exceptions (it is
+ /// not fixed if a concrete implementation of this method can throw
+ /// anything else.)
+ /// \throw EmptyZone the zone is supposed to exist in the data source,
+ /// but its content is not available. This generally means there's an
+ /// error in the content.
+ ///
/// \param name The name of zone apex to be traversed. It doesn't do
/// nearest match as findZone.
/// \param separate_rrs If true, the iterator will return each RR as a
diff --git a/src/lib/datasrc/client_list.cc b/src/lib/datasrc/client_list.cc
index c9bdee0..94afe96 100644
--- a/src/lib/datasrc/client_list.cc
+++ b/src/lib/datasrc/client_list.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -24,6 +24,7 @@
#include <datasrc/memory/zone_data_loader.h>
#include <datasrc/memory/zone_data_updater.h>
#include <datasrc/logger.h>
+#include <datasrc/zone_table_accessor_cache.h>
#include <dns/masterload.h>
#include <util/memory_segment_local.h>
@@ -82,6 +83,7 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
if (!config) {
isc_throw(isc::BadValue, "NULL configuration passed");
}
+
// TODO: Implement recycling from the old configuration.
size_t i(0); // Outside of the try to be able to access it in the catch
try {
@@ -90,33 +92,42 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
for (; i < config->size(); ++i) {
// Extract the parameters
const ConstElementPtr dconf(config->get(i));
- const ConstElementPtr typeElem(dconf->get("type"));
- if (typeElem == ConstElementPtr()) {
+ const ConstElementPtr type_elem(dconf->get("type"));
+ if (type_elem == ConstElementPtr()) {
isc_throw(ConfigurationError, "Missing the type option in "
"data source no " << i);
}
- const string type(typeElem->stringValue());
- ConstElementPtr paramConf(dconf->get("params"));
- if (paramConf == ConstElementPtr()) {
- paramConf.reset(new NullElement());
+ const string type(type_elem->stringValue());
+ ConstElementPtr param_conf(dconf->get("params"));
+ if (param_conf == ConstElementPtr()) {
+ param_conf.reset(new NullElement());
}
// Get the name (either explicit, or guess)
const ConstElementPtr name_elem(dconf->get("name"));
- const string name(name_elem ? name_elem->stringValue() : type);
- if (!used_names.insert(name).second) {
+ const string datasrc_name =
+ name_elem ? name_elem->stringValue() : type;
+ if (!used_names.insert(datasrc_name).second) {
isc_throw(ConfigurationError, "Duplicate name in client list: "
- << name);
+ << datasrc_name);
+ }
+
+ DataSourcePair dsrc_pair;
+ try {
+ // Create a client for the underling data source via
+ // factory. If it's our internal type of data source,
+ // this is essentially no-op. In the latter case, it's
+ // of no use unless cache is allowed; we simply skip
+ // building it in that case.
+ dsrc_pair = getDataSourceClient(type, param_conf);
+ } catch (const DataSourceLibraryError& ex) {
+ LOG_ERROR(logger, DATASRC_LIBRARY_ERROR).
+ arg(datasrc_name).arg(rrclass_).arg(ex.what());
+ continue;
}
- // Create a client for the underling data source via factory.
- // If it's our internal type of data source, this is essentially
- // no-op. In the latter case, it's of no use unless cache is
- // allowed; we simply skip building it in that case.
- const DataSourcePair dsrc_pair = getDataSourceClient(type,
- paramConf);
if (!allow_cache && !dsrc_pair.first) {
LOG_WARN(logger, DATASRC_LIST_NOT_CACHED).
- arg(name).arg(rrclass_);
+ arg(datasrc_name).arg(rrclass_);
continue;
}
@@ -129,13 +140,22 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
new_data_sources.push_back(DataSourceInfo(dsrc_pair.first,
dsrc_pair.second,
cache_conf, rrclass_,
- name));
+ datasrc_name));
- // If cache is disabled we are done for this data source.
+ // If cache is disabled, or the zone table segment is not (yet)
+ // writable, we are done for this data source.
// Otherwise load zones into the in-memory cache.
if (!cache_conf->isEnabled()) {
continue;
}
+ memory::ZoneTableSegment& zt_segment =
+ *new_data_sources.back().ztable_segment_;
+ if (!zt_segment.isWritable()) {
+ LOG_DEBUG(logger, DBGLVL_TRACE_BASIC,
+ DATASRC_LIST_CACHE_PENDING).arg(datasrc_name);
+ continue;
+ }
+
internal::CacheConfig::ConstZoneIterator end_of_zones =
cache_conf->end();
for (internal::CacheConfig::ConstZoneIterator zone_it =
@@ -144,25 +164,27 @@ ConfigurableClientList::configure(const ConstElementPtr& config,
++zone_it)
{
const Name& zname = zone_it->first;
- memory::LoadAction load_action;
try {
- load_action = cache_conf->getLoadAction(rrclass_, zname);
- } catch (const DataSourceError&) {
- isc_throw(ConfigurationError, "Data source error for "
- "loading a zone (possibly non-existent) "
- << zname << "/" << rrclass_);
- }
- assert(load_action); // in this loop this should be always true
- boost::scoped_ptr<memory::ZoneWriter> writer;
- try {
- writer.reset(new_data_sources.back().ztable_segment_->
- getZoneWriter(load_action, zname, rrclass_));
- writer->load();
- writer->install();
- writer->cleanup();
- } catch (const ZoneLoaderException& e) {
- LOG_ERROR(logger, DATASRC_LOAD_ZONE_ERROR)
- .arg(zname).arg(rrclass_).arg(name).arg(e.what());
+ const memory::LoadAction load_action =
+ cache_conf->getLoadAction(rrclass_, zname);
+ // in this loop this should be always true
+ assert(load_action);
+ // For the initial load, we'll let the writer handle
+ // loading error and install an empty zone in the table.
+ memory::ZoneWriter writer(zt_segment, load_action, zname,
+ rrclass_, true);
+
+ std::string error_msg;
+ writer.load(&error_msg);
+ if (!error_msg.empty()) {
+ LOG_ERROR(logger, DATASRC_LOAD_ZONE_ERROR).arg(zname).
+ arg(rrclass_).arg(datasrc_name).arg(error_msg);
+ }
+ writer.install();
+ writer.cleanup();
+ } catch (const NoSuchZone&) {
+ LOG_ERROR(logger, DATASRC_CACHE_ZONE_NOTFOUND).
+ arg(zname).arg(rrclass_).arg(datasrc_name);
}
}
}
@@ -309,47 +331,74 @@ ConfigurableClientList::findInternal(MutableResult& candidate,
// and the need_updater parameter is true, get the zone there.
}
-// We still provide this method for backward compatibility. But to not have
-// duplicate code, it is a thin wrapper around getCachedZoneWriter only.
-ConfigurableClientList::ReloadResult
-ConfigurableClientList::reload(const Name& name) {
- const ZoneWriterPair result(getCachedZoneWriter(name));
- if (result.first != ZONE_SUCCESS) {
- return (result.first);
+bool
+ConfigurableClientList::resetMemorySegment
+ (const std::string& datasrc_name,
+ ZoneTableSegment::MemorySegmentOpenMode mode,
+ ConstElementPtr config_params)
+{
+ BOOST_FOREACH(DataSourceInfo& info, data_sources_) {
+ if (info.name_ == datasrc_name) {
+ ZoneTableSegment& segment = *info.ztable_segment_;
+ segment.reset(mode, config_params);
+ return true;
+ }
}
-
- assert(result.second);
- result.second->load();
- result.second->install();
- result.second->cleanup();
-
- return (ZONE_SUCCESS);
+ return false;
}
ConfigurableClientList::ZoneWriterPair
-ConfigurableClientList::getCachedZoneWriter(const Name& name) {
+ConfigurableClientList::getCachedZoneWriter(const Name& name,
+ bool catch_load_error,
+ const std::string& datasrc_name)
+{
if (!allow_cache_) {
return (ZoneWriterPair(CACHE_DISABLED, ZoneWriterPtr()));
}
- // Try to find the correct zone.
- MutableResult result;
- findInternal(result, name, true, true);
- if (!result.finder) {
- return (ZoneWriterPair(ZONE_NOT_FOUND, ZoneWriterPtr()));
- }
+ // Find the data source from which the zone to be loaded into memory.
// Then get the appropriate load action and create a zone writer.
- // Note that getCacheConfig() must return non NULL in this module (only
- // tests could set it to a bogus value).
- const memory::LoadAction load_action =
- result.info->getCacheConfig()->getLoadAction(rrclass_, name);
- if (!load_action) {
- return (ZoneWriterPair(ZONE_NOT_CACHED, ZoneWriterPtr()));
+ BOOST_FOREACH(const DataSourceInfo& info, data_sources_) {
+ if (!datasrc_name.empty() && datasrc_name != info.name_) {
+ continue;
+ }
+ // If there's an underlying "real" data source and it doesn't contain
+ // the given name, obviously we cannot load it. If a specific data
+ // source is given by the name, search should stop here.
+ if (info.data_src_client_ &&
+ info.data_src_client_->findZone(name).code != result::SUCCESS) {
+ if (!datasrc_name.empty()) {
+ return (ZoneWriterPair(ZONE_NOT_FOUND, ZoneWriterPtr()));
+ }
+ continue;
+ }
+ // If the corresponding zone table segment is not (yet) writable,
+ // we cannot load at this time.
+ if (info.ztable_segment_ && !info.ztable_segment_->isWritable()) {
+ return (ZoneWriterPair(CACHE_NOT_WRITABLE, ZoneWriterPtr()));
+ }
+ // Note that getCacheConfig() must return non NULL in this module
+ // (only tests could set it to a bogus value).
+ const memory::LoadAction load_action =
+ info.getCacheConfig()->getLoadAction(rrclass_, name);
+ if (!load_action) {
+ return (ZoneWriterPair(ZONE_NOT_CACHED, ZoneWriterPtr()));
+ }
+ return (ZoneWriterPair(ZONE_SUCCESS,
+ ZoneWriterPtr(
+ new memory::ZoneWriter(
+ *info.ztable_segment_,
+ load_action, name, rrclass_,
+ catch_load_error))));
}
- return (ZoneWriterPair(ZONE_SUCCESS,
- ZoneWriterPtr(
- result.info->ztable_segment_->
- getZoneWriter(load_action, name, rrclass_))));
+
+ // We can't find the specified zone. If a specific data source was
+ // given, this means the given name of data source doesn't exist, so
+ // we report it so.
+ if (!datasrc_name.empty()) {
+ return (ZoneWriterPair(DATASRC_NOT_FOUND, ZoneWriterPtr()));
+ }
+ return (ZoneWriterPair(ZONE_NOT_FOUND, ZoneWriterPtr()));
}
// NOTE: This function is not tested, it would be complicated. However, the
@@ -373,14 +422,43 @@ vector<DataSourceStatus>
ConfigurableClientList::getStatus() const {
vector<DataSourceStatus> result;
BOOST_FOREACH(const DataSourceInfo& info, data_sources_) {
- // TODO: Once we support mapped cache, decide when we need the
- // SEGMENT_WAITING.
- result.push_back(DataSourceStatus(info.name_, info.cache_ ?
- SEGMENT_INUSE : SEGMENT_UNUSED,
- "local"));
+ if (info.ztable_segment_) {
+ result.push_back(DataSourceStatus(
+ info.name_,
+ (info.ztable_segment_->isUsable() ?
+ SEGMENT_INUSE : SEGMENT_WAITING),
+ info.ztable_segment_->getImplType()));
+ } else {
+ result.push_back(DataSourceStatus(info.name_));
+ }
}
return (result);
}
+ConstZoneTableAccessorPtr
+ConfigurableClientList::getZoneTableAccessor(const std::string& datasrc_name,
+ bool use_cache) const
+{
+ if (!use_cache) {
+ isc_throw(isc::NotImplemented,
+ "getZoneTableAccessor only implemented for cache");
+ }
+
+ // Find the matching data source
+ BOOST_FOREACH(const DataSourceInfo& info, data_sources_) {
+ if (!datasrc_name.empty() && datasrc_name != info.name_) {
+ continue;
+ }
+
+ const internal::CacheConfig* config(info.getCacheConfig());
+ // If caching is disabled for the named data source, this will
+ // return an accessor to an effectivley empty table.
+ return (ConstZoneTableAccessorPtr
+ (new internal::ZoneTableAccessorCache(*config)));
+ }
+
+ return (ConstZoneTableAccessorPtr());
+}
+
}
}
diff --git a/src/lib/datasrc/client_list.h b/src/lib/datasrc/client_list.h
index 813f3dd..31cb6c2 100644
--- a/src/lib/datasrc/client_list.h
+++ b/src/lib/datasrc/client_list.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -21,7 +21,8 @@
#include <dns/rrclass.h>
#include <cc/data.h>
#include <exceptions/exceptions.h>
-#include "memory/zone_table_segment.h"
+#include <datasrc/memory/zone_table_segment.h>
+#include <datasrc/zone_table_accessor.h>
#include <vector>
#include <boost/shared_ptr.hpp>
@@ -80,13 +81,26 @@ class DataSourceStatus {
public:
/// \brief Constructor
///
- /// Sets initial values. It doesn't matter what is provided for the type
- /// if state is SEGMENT_UNUSED, the value is effectively ignored.
+ /// Sets initial values. If you want to use \c SEGMENT_UNUSED as the
+ /// state, please use the other constructor.
DataSourceStatus(const std::string& name, MemorySegmentState state,
const std::string& type) :
name_(name),
type_(type),
state_(state)
+ {
+ assert (state != SEGMENT_UNUSED);
+ assert (!type.empty());
+ }
+
+ /// \brief Constructor
+ ///
+ /// Sets initial values. The state is set as \c SEGMENT_UNUSED and
+ /// the type is effectively unspecified.
+ DataSourceStatus(const std::string& name) :
+ name_(name),
+ type_(""),
+ state_(SEGMENT_UNUSED)
{}
/// \brief Get the segment state
@@ -231,20 +245,29 @@ public:
/// of the searched name is needed. Therefore, the call would look like:
///
/// \code FindResult result(list->find(queried_name));
- /// FindResult result(list->find(queried_name));
- /// if (result.datasrc_) {
- /// createTheAnswer(result.finder_);
+ /// if (result.dsrc_client_) {
+ /// if (result.finder_) {
+ /// createTheAnswer(result.finder_);
+ /// } else { // broken zone, return SERVFAIL
+ /// createErrorMessage(Rcode.SERVFAIL());
+ /// }
/// } else {
/// createNotAuthAnswer();
/// } \endcode
///
+ /// Note that it is possible that \c finder_ is NULL while \c datasrc_
+ /// is not. This happens if the zone is configured to be served from
+ /// the data source but it is broken in some sense and doesn't hold any
+ /// zone data, e.g., when the zone file has an error or the secondary
+ /// zone hasn't been transferred yet. The caller needs to expect the case
+ /// and handle it accordingly.
+ ///
/// The other scenario is manipulating zone data (XfrOut, XfrIn, DDNS,
/// ...). In this case, the finder itself is not so important. However,
/// we need an exact match (if we want to manipulate zone data, we must
/// know exactly, which zone we are about to manipulate). Then the call
///
/// \code FindResult result(list->find(zone_name, true, false));
- /// FindResult result(list->find(zone_name, true, false));
/// if (result.datasrc_) {
/// ZoneUpdaterPtr updater(result.datasrc_->getUpdater(zone_name);
/// ...
@@ -272,6 +295,19 @@ public:
virtual FindResult find(const dns::Name& zone,
bool want_exact_match = false,
bool want_finder = true) const = 0;
+
+ /// \brief Creates a ZoneTableAccessor object for the specified data
+ /// source.
+ ///
+ /// \param datasrc_name If not empty, the name of the data source.
+ /// \param use_cache If true, create a zone table for in-memory cache.
+ /// \throw NotImplemented if this method is not implemented.
+ /// \return A pointer to the accessor, or NULL if the requested data
+ /// source is not found.
+ virtual ConstZoneTableAccessorPtr
+ getZoneTableAccessor(const std::string& datasrc_name,
+ bool use_cache) const = 0;
+
};
/// \brief Shared pointer to the list.
@@ -339,60 +375,78 @@ public:
return (configuration_);
}
- /// \brief Result of the reload() method.
- enum ReloadResult {
- CACHE_DISABLED, ///< The cache is not enabled in this list.
- ZONE_NOT_CACHED, ///< Zone is served directly, not from cache
- /// (including the case cache is disabled for
- /// the specific data source).
- ZONE_NOT_FOUND, ///< Zone does not exist in this list.
- ZONE_SUCCESS ///< The zone was successfully reloaded or
- /// the writer provided.
- };
-
- /// \brief Reloads a cached zone.
+ /// \brief Resets the zone table segment for a datasource with a new
+ /// memory segment.
///
- /// This method finds a zone which is loaded into a cache and reloads it.
- /// This may be used to renew the cache when the underlying data source
- /// changes.
+ /// See documentation of \c ZoneTableSegment interface
+ /// implementations (such as \c ZoneTableSegmentMapped) for the
+ /// syntax of \c config_params.
///
- /// \param zone The origin of the zone to reload.
- /// \return A status if the command worked.
- /// \throw DataSourceError or anything else that the data source
- /// containing the zone might throw is propagated.
- /// \throw DataSourceError if something unexpected happens, like when
- /// the original data source no longer contains the cached zone.
- ReloadResult reload(const dns::Name& zone);
+ /// \param datasrc_name The name of the data source whose segment to reset
+ /// \param mode The open mode for the new memory segment
+ /// \param config_params The configuration for the new memory segment.
+ /// \return If the data source was found and reset.
+ bool resetMemorySegment
+ (const std::string& datasrc_name,
+ memory::ZoneTableSegment::MemorySegmentOpenMode mode,
+ isc::data::ConstElementPtr config_params);
-private:
/// \brief Convenience type shortcut
typedef boost::shared_ptr<memory::ZoneWriter> ZoneWriterPtr;
-public:
+
+ /// \brief Codes indicating in-memory cache status for a given zone name.
+ ///
+ /// This is used as a result of the getCachedZoneWriter() method.
+ enum CacheStatus {
+ CACHE_DISABLED, ///< The cache is not enabled in this list.
+ ZONE_NOT_CACHED, ///< Zone is not to be cached (including the case
+ /// where caching is disabled for the specific
+ /// data source).
+ ZONE_NOT_FOUND, ///< Zone does not exist in this list.
+ CACHE_NOT_WRITABLE, ///< The cache is not writable (and zones can't
+ /// be loaded)
+ DATASRC_NOT_FOUND, ///< Specific data source for load is specified
+ /// but it's not in the list
+ ZONE_SUCCESS ///< Zone to be cached is successfully found and
+ /// is ready to be loaded
+ };
/// \brief Return value of getCachedZoneWriter()
///
/// A pair containing status and the zone writer, for the
/// getCachedZoneWriter() method.
- typedef std::pair<ReloadResult, ZoneWriterPtr> ZoneWriterPair;
-
- /// \brief Return a zone writer that can be used to reload a zone.
- ///
- /// This looks up a cached copy of zone and returns the ZoneWriter
- /// that can be used to reload the content of the zone. This can
- /// be used instead of reload() -- reload() works synchronously, which
- /// is not what is needed every time.
- ///
- /// \param zone The origin of the zone to reload.
- /// \return The result has two parts. The first one is a status describing
+ typedef std::pair<CacheStatus, ZoneWriterPtr> ZoneWriterPair;
+
+ /// \brief Return a zone writer that can be used to (re)load a zone.
+ ///
+ /// By default this method identifies the first data source in the list
+ /// that should serve the zone of the given name, and returns a ZoneWriter
+ /// object that can be used to load the content of the zone, in a specific
+ /// way for that data source.
+ ///
+ /// If the optional \c datasrc_name parameter is provided with a non empty
+ /// string, this method only tries to load the specified zone into or with
+ /// the data source which has the given name, regardless where in the list
+ /// that data source is placed. Even if the given name of zone doesn't
+ /// exist in the data source, other data sources are not searched and
+ /// this method simply returns ZONE_NOT_FOUND in the first element
+ /// of the pair.
+ ///
+ /// \param zone The origin of the zone to load.
+ /// \param catch_load_errors Whether to make the zone writer catch
+ /// load errors (see \c ZoneWriter constructor documentation).
+ /// \param datasrc_name If not empty, the name of the data source
+ /// to be used for loading the zone (see above).
+ /// \return The result has two parts. The first one is a status indicating
/// if it worked or not (and in case it didn't, also why). If the
/// status is ZONE_SUCCESS, the second part contains a shared pointer
/// to the writer. If the status is anything else, the second part is
/// NULL.
/// \throw DataSourceError or anything else that the data source
/// containing the zone might throw is propagated.
- /// \throw DataSourceError if something unexpected happens, like when
- /// the original data source no longer contains the cached zone.
- ZoneWriterPair getCachedZoneWriter(const dns::Name& zone);
+ ZoneWriterPair getCachedZoneWriter(const dns::Name& zone,
+ bool catch_load_error,
+ const std::string& datasrc_name = "");
/// \brief Implementation of the ClientList::find.
virtual FindResult find(const dns::Name& zone,
@@ -400,8 +454,6 @@ public:
bool want_finder = true) const;
/// \brief This holds one data source client and corresponding information.
- ///
- /// \todo The content yet to be defined.
struct DataSourceInfo {
DataSourceInfo(DataSourceClient* data_src_client,
const DataSourceClientContainerPtr& container,
@@ -421,12 +473,12 @@ public:
boost::shared_ptr<memory::ZoneTableSegment> ztable_segment_;
std::string name_;
+ // cache_conf_ can be accessed only from this read-only getter,
+ // to protect its integrity as much as possible.
const internal::CacheConfig* getCacheConfig() const {
return (cache_conf_.get());
}
private:
- // this is kept private for now. When it needs to be accessed,
- // we'll add a read-only getter method.
boost::shared_ptr<internal::CacheConfig> cache_conf_;
};
@@ -473,7 +525,7 @@ public:
/// This may throw standard exceptions, such as std::bad_alloc. Otherwise,
/// it is exception free.
std::vector<DataSourceStatus> getStatus() const;
-public:
+
/// \brief Access to the data source clients.
///
/// It can be used to examine the loaded list of data sources clients
@@ -481,6 +533,22 @@ public:
/// it might be, so it is just made public (there's no real reason to
/// hide it).
const DataSources& getDataSources() const { return (data_sources_); }
+
+ /// \brief Creates a ZoneTableAccessor object for the specified data
+ /// source.
+ ///
+ /// \param datasrc_name If not empty, the name of the data source
+ /// \param use_cache If true, create a zone table for in-memory cache.
+ /// \note At present, the only concrete implementation of
+ /// ZoneTableAccessor is ZoneTableAccessorCache, so \c use_cache must be
+ /// true.
+ /// \throw NotImplemented if \c use_cache is false.
+ /// \return A pointer to the accessor, or NULL if the requested data
+ /// source is not found.
+ ConstZoneTableAccessorPtr
+ getZoneTableAccessor(const std::string& datasrc_name,
+ bool use_cache) const;
+
private:
struct MutableResult;
/// \brief Internal implementation of find.
diff --git a/src/lib/datasrc/database.cc b/src/lib/datasrc/database.cc
index 70f4df0..9c85054 100644
--- a/src/lib/datasrc/database.cc
+++ b/src/lib/datasrc/database.cc
@@ -1233,7 +1233,7 @@ public:
const pair<bool, int> zone(accessor_->getZone(zone_name.toText()));
if (!zone.first) {
// No such zone, can't continue
- isc_throw(DataSourceError, "Zone " + zone_name.toText() +
+ isc_throw(NoSuchZone, "Zone " + zone_name.toText() +
" can not be iterated, because it doesn't exist "
"in this data source");
}
diff --git a/src/lib/datasrc/datasrc_messages.mes b/src/lib/datasrc/datasrc_messages.mes
index 2c345c2..f9a76ed 100644
--- a/src/lib/datasrc/datasrc_messages.mes
+++ b/src/lib/datasrc/datasrc_messages.mes
@@ -70,6 +70,15 @@ The maximum allowed number of items of the hotspot cache is set to the given
number. If there are too many, some of them will be dropped. The size of 0
means no limit.
+% DATASRC_CACHE_ZONE_NOTFOUND Zone %1/%2 not found on data source '%3' to cache
+During data source configuration, a zone is to be loaded (cached) in
+to memory from the underlying data source, but the zone is not found
+in the data source. This particular zone was not loaded, but data
+source configuration continues, possibly loading other zones into
+memory. This is basically some kind of configuration or operation
+error: either the name is incorrect in the configuration, or the zone
+has been removed from the data source without updating the configuration.
+
% DATASRC_CHECK_ERROR post-load check of zone %1/%2 failed: %3
The zone was loaded into the data source successfully, but the content fails
basic sanity checks. See the message if you want to know what exactly is wrong
@@ -347,6 +356,13 @@ not contain RRs the requested type. AN NXRRSET indication is returned.
A debug message indicating that a query for the given name and RR type is being
processed.
+% DATASRC_LIST_CACHE_PENDING in-memory cache for data source '%1' is not yet writable, pending load
+While (re)configuring data source clients, zone data of the shown data
+source cannot be loaded to in-memory cache at that point because the
+cache is not yet ready for writing. This can happen for shared-memory
+type of cache, in which case the cache will be reset later, either
+by a higher level application or by a command from other module.
+
% DATASRC_LIST_NOT_CACHED zones in data source %1 for class %2 not cached, cache disabled globally. Will not be available.
The process disabled caching of RR data completely. However, this data source
is provided from a master file and it can be served from memory cache only.
@@ -354,12 +370,18 @@ Therefore, the entire data source will not be available for this process. If
this is a problem, you should configure the zones of that data source to some
database backend (sqlite3, for example) and use it from there.
-% DATASRC_LOAD_ZONE_ERROR Error loading zone %1/%2 on data source %3: %4
+% DATASRC_LIBRARY_ERROR failure loading %1 datasource library for class %2: %3
+There was a problem loading the dynamic library for a data source. This
+backend is hence not available, and any data sources that use this
+backend will not be available.
+
+% DATASRC_LOAD_ZONE_ERROR Error loading zone %1/%2 on data source '%3': %4
During data source configuration, an error was found in the zone data
when it was being loaded in to memory on the shown data source. This
particular zone was not loaded, but data source configuration
-continues, possibly loading other zones into memory. The specific
-error is shown in the message, and should be addressed.
+continues, possibly loading other zones into memory. Subsequent lookups
+will treat this zone as broken. The specific error is shown in the
+message, and should be addressed.
% DATASRC_MASTER_LOAD_ERROR %1:%2: Zone '%3/%4' contains error: %5
There's an error in the given master file. The zone won't be loaded for
diff --git a/src/lib/datasrc/exceptions.h b/src/lib/datasrc/exceptions.h
index f9c5655..f3bc06f 100644
--- a/src/lib/datasrc/exceptions.h
+++ b/src/lib/datasrc/exceptions.h
@@ -20,8 +20,14 @@
namespace isc {
namespace datasrc {
-/// This exception represents Backend-independent errors relating to
-/// data source operations.
+/// \brief Top level exception related to data source.
+///
+/// This exception is the most generic form of exception for errors or
+/// unexpected events that can happen in the data source module. In general,
+/// if an application needs to catch these conditions explicitly, it should
+/// catch more specific exceptions derived from this class; the severity
+/// of the conditions will vary very much, and such an application would
+/// normally like to behave differently depending on the severity.
class DataSourceError : public Exception {
public:
DataSourceError(const char* file, size_t line, const char* what) :
@@ -40,6 +46,27 @@ public:
DataSourceError(file, line, what) {}
};
+/// \brief A specified zone does not exist in the specified data source.
+///
+/// This exception is thrown from methods that take a zone name and perform
+/// some action regarding that zone on the corresponding data source.
+class NoSuchZone : public DataSourceError {
+public:
+ NoSuchZone(const char* file, size_t line, const char* what) :
+ DataSourceError(file, line, what) {}
+};
+
+/// \brief An error indicating a zone is recognized but its content is not
+/// available.
+///
+/// This generally indicates a condition that there's an error in the zone
+/// content and it's not successfully loaded.
+class EmptyZone : public DataSourceError {
+public:
+ EmptyZone(const char* file, size_t line, const char* what) :
+ DataSourceError(file, line, what) {}
+};
+
/// Base class for a number of exceptions that are thrown while working
/// with zones.
struct ZoneException : public Exception {
diff --git a/src/lib/datasrc/factory.cc b/src/lib/datasrc/factory.cc
index 73f7e4a..26b31dd 100644
--- a/src/lib/datasrc/factory.cc
+++ b/src/lib/datasrc/factory.cc
@@ -83,8 +83,8 @@ LibraryContainer::LibraryContainer(const std::string& name) {
if (ds_lib_ == NULL) {
// This may cause the filename to appear twice in the actual
// error, but the output of dlerror is implementation-dependent
- isc_throw(DataSourceLibraryError, "dlopen failed for " << name <<
- ": " << dlerror());
+ isc_throw(DataSourceLibraryOpenError,
+ "dlopen failed for " << name << ": " << dlerror());
}
}
diff --git a/src/lib/datasrc/factory.h b/src/lib/datasrc/factory.h
index 4e669ec..b9b6578 100644
--- a/src/lib/datasrc/factory.h
+++ b/src/lib/datasrc/factory.h
@@ -27,7 +27,7 @@ namespace isc {
namespace datasrc {
-/// \brief Raised if there is an error loading the datasource implementation
+/// \brief Raised if there is an error in the datasource implementation
/// library
class DataSourceLibraryError : public DataSourceError {
public:
@@ -35,13 +35,22 @@ public:
DataSourceError(file, line, what) {}
};
+/// \brief Raised if there is an error opening the the datasource
+/// implementation library
+class DataSourceLibraryOpenError : public DataSourceLibraryError {
+public:
+ DataSourceLibraryOpenError(const char* file, size_t line,
+ const char* what) :
+ DataSourceLibraryError(file, line, what) {}
+};
+
/// \brief Raised if there is an error reading a symbol from the datasource
/// implementation library
-class DataSourceLibrarySymbolError : public DataSourceError {
+class DataSourceLibrarySymbolError : public DataSourceLibraryError {
public:
DataSourceLibrarySymbolError(const char* file, size_t line,
const char* what) :
- DataSourceError(file, line, what) {}
+ DataSourceLibraryError(file, line, what) {}
};
typedef DataSourceClient* ds_creator(isc::data::ConstElementPtr config,
diff --git a/src/lib/datasrc/memory/.gitignore b/src/lib/datasrc/memory/.gitignore
index fbb6381..89122b9 100644
--- a/src/lib/datasrc/memory/.gitignore
+++ b/src/lib/datasrc/memory/.gitignore
@@ -1,2 +1,3 @@
/memory_messages.cc
/memory_messages.h
+/s-messages
diff --git a/src/lib/datasrc/memory/Makefile.am b/src/lib/datasrc/memory/Makefile.am
index c0ee688..197f3fa 100644
--- a/src/lib/datasrc/memory/Makefile.am
+++ b/src/lib/datasrc/memory/Makefile.am
@@ -17,16 +17,21 @@ libdatasrc_memory_la_SOURCES += rdata_serialization.h rdata_serialization.cc
libdatasrc_memory_la_SOURCES += zone_data.h zone_data.cc
libdatasrc_memory_la_SOURCES += rrset_collection.h rrset_collection.cc
libdatasrc_memory_la_SOURCES += segment_object_holder.h
+libdatasrc_memory_la_SOURCES += segment_object_holder.cc
libdatasrc_memory_la_SOURCES += logger.h logger.cc
libdatasrc_memory_la_SOURCES += zone_table.h zone_table.cc
libdatasrc_memory_la_SOURCES += zone_finder.h zone_finder.cc
libdatasrc_memory_la_SOURCES += zone_table_segment.h zone_table_segment.cc
libdatasrc_memory_la_SOURCES += zone_table_segment_local.h zone_table_segment_local.cc
+
+if USE_SHARED_MEMORY
+libdatasrc_memory_la_SOURCES += zone_table_segment_mapped.h zone_table_segment_mapped.cc
+endif
+
libdatasrc_memory_la_SOURCES += zone_data_updater.h zone_data_updater.cc
libdatasrc_memory_la_SOURCES += zone_data_loader.h zone_data_loader.cc
libdatasrc_memory_la_SOURCES += memory_client.h memory_client.cc
-libdatasrc_memory_la_SOURCES += zone_writer.h
-libdatasrc_memory_la_SOURCES += zone_writer_local.h zone_writer_local.cc
+libdatasrc_memory_la_SOURCES += zone_writer.h zone_writer.cc
libdatasrc_memory_la_SOURCES += load_action.h
libdatasrc_memory_la_SOURCES += util_internal.h
@@ -35,8 +40,11 @@ nodist_libdatasrc_memory_la_SOURCES = memory_messages.h memory_messages.cc
EXTRA_DIST = rdata_serialization_priv.cc
BUILT_SOURCES = memory_messages.h memory_messages.cc
-memory_messages.h memory_messages.cc: Makefile memory_messages.mes
+memory_messages.h memory_messages.cc: s-messages
+
+s-messages: Makefile memory_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/datasrc/memory/memory_messages.mes
+ touch $@
EXTRA_DIST += memory_messages.mes
-CLEANFILES += memory_messages.h memory_messages.cc
+CLEANFILES += memory_messages.h memory_messages.cc s-messages
diff --git a/src/lib/datasrc/memory/domaintree.h b/src/lib/datasrc/memory/domaintree.h
index 888f914..f309e4a 100644
--- a/src/lib/datasrc/memory/domaintree.h
+++ b/src/lib/datasrc/memory/domaintree.h
@@ -356,6 +356,31 @@ private:
}
}
+ /// \brief Returns if the node color is black
+ bool isBlack() const {
+ return (getColor() == BLACK);
+ }
+
+ /// \brief Static variant of isBlack() that also allows NULL nodes.
+ static bool isBlack(const DomainTreeNode<T>* node) {
+ if (!node) {
+ // NULL nodes are black.
+ return (true);
+ }
+
+ return (node->isBlack());
+ }
+
+ /// \brief Returns if the node color is red
+ bool isRed() const {
+ return (!isBlack());
+ }
+
+ /// \brief Static variant of isRed() that also allows NULL nodes.
+ static bool isRed(const DomainTreeNode<T>* node) {
+ return (!isBlack(node));
+ }
+
/// \brief Sets the color of this node
void setColor(const DomainTreeNodeColor color) {
if (color == RED) {
@@ -383,11 +408,20 @@ private:
return ((flags_ & FLAG_SUBTREE_ROOT) != 0);
}
+ /// \brief Static helper function used by const and non-const
+ /// variants of getSubTreeRoot()
+ template <typename TT>
+ static TT*
+ getSubTreeRootImpl(TT* node);
+
/// \brief returns the root of its subtree
///
/// This method takes a node and returns the root of its subtree.
///
/// This method never throws an exception.
+ DomainTreeNode<T>* getSubTreeRoot();
+
+ /// \brief returns the root of its subtree (const variant)
const DomainTreeNode<T>* getSubTreeRoot() const;
public:
@@ -399,6 +433,10 @@ public:
/// (which should be absolute), it will return \c NULL.
///
/// This method never throws an exception.
+ DomainTreeNode<T>* getUpperNode();
+
+ /// \brief returns the parent of the root of its subtree (const
+ /// variant)
const DomainTreeNode<T>* getUpperNode() const;
/// \brief return the next node which is bigger than current node
@@ -416,6 +454,10 @@ public:
/// returns \c NULL.
///
/// This method never throws an exception.
+ DomainTreeNode<T>* successor();
+
+ /// \brief return the next node which is bigger than current node
+ /// in the same subtree (const variant)
const DomainTreeNode<T>* successor() const;
/// \brief return the next node which is smaller than current node
@@ -433,8 +475,27 @@ public:
/// returns \c NULL.
///
/// This method never throws an exception.
+ DomainTreeNode<T>* predecessor();
+
+ /// \brief return the next node which is smaller than current node
+ /// in the same subtree (const variant)
const DomainTreeNode<T>* predecessor() const;
+ /// \brief returns the node distance from the root of its subtree
+ size_t getDistance() const {
+ size_t nodes = 1;
+ for (const DomainTreeNode<T>* node = this;
+ node != NULL;
+ node = node->getParent()) {
+ if (node->isSubTreeRoot()) {
+ break;
+ }
+ ++nodes;
+ }
+
+ return (nodes);
+ }
+
private:
/// \brief private shared implementation of successor and predecessor
///
@@ -447,12 +508,13 @@ private:
/// The overhead of the member pointers should be optimised out, as this
/// will probably get completely inlined into predecessor and successor
/// methods.
- const DomainTreeNode<T>*
- abstractSuccessor(typename DomainTreeNode<T>::DomainTreeNodePtr
+ template <typename TT>
+ static TT*
+ abstractSuccessor(TT* node,
+ typename DomainTreeNode<T>::DomainTreeNodePtr
DomainTreeNode<T>::*left,
- typename DomainTreeNode<T>::DomainTreeNodePtr
- DomainTreeNode<T>::*right)
- const;
+ typename DomainTreeNode<T>::DomainTreeNodePtr
+ DomainTreeNode<T>::*right);
/// \name Data to maintain the rbtree structure.
///
@@ -489,6 +551,59 @@ private:
const DomainTreeNode<T>* getRight() const {
return (right_.get());
}
+
+ /// \brief Access grandparent node as bare pointer.
+ ///
+ /// The grandparent node is the parent's parent.
+ ///
+ /// \return the grandparent node if one exists, NULL otherwise.
+ DomainTreeNode<T>* getGrandParent() {
+ DomainTreeNode<T>* parent = getParent();
+ if (parent != NULL) {
+ return (parent->getParent());
+ } else {
+ // If there's no parent, there's no grandparent.
+ return (NULL);
+ }
+ }
+
+ /// \brief Access sibling node as bare pointer.
+ ///
+ /// A sibling node is defined as the parent's other child. It exists
+ /// at the same level as child. Note that child can be NULL here,
+ /// which is why this is a static function.
+ ///
+ /// \return the sibling node if one exists, NULL otherwise.
+ static DomainTreeNode<T>* getSibling(DomainTreeNode<T>* parent,
+ DomainTreeNode<T>* child)
+ {
+ if (!parent) {
+ return (NULL);
+ }
+
+ return ((parent->getLeft() == child) ?
+ parent->getRight() : parent->getLeft());
+ }
+
+ /// \brief Access uncle node as bare pointer.
+ ///
+ /// An uncle node is defined as the parent node's sibling. It exists
+ /// at the same level as the parent.
+ ///
+ /// \return the uncle node if one exists, NULL otherwise.
+ DomainTreeNode<T>* getUncle() {
+ DomainTreeNode<T>* grandparent = getGrandParent();
+ if (grandparent == NULL) {
+ // If there's no grandparent, there's no uncle.
+ return (NULL);
+ }
+ if (getParent() == grandparent->getLeft()) {
+ return (grandparent->getRight());
+ } else {
+ return (grandparent->getLeft());
+ }
+ }
+
//@}
/// \brief The subdomain tree.
@@ -510,6 +625,96 @@ private:
return (down_.get());
}
+ /// \brief Helper method used in many places in code to set parent's
+ /// child links.
+ void connectChild(DomainTreeNode<T>* oldnode,
+ DomainTreeNode<T>* newnode,
+ DomainTreeNodePtr* root_ptr,
+ DomainTreeNode<T>* thisnode = NULL)
+ {
+ if (!thisnode) {
+ thisnode = this;
+ }
+
+ if (getParent() != NULL) {
+ if (getParent()->getLeft() == oldnode) {
+ thisnode->getParent()->left_ = newnode;
+ } else if (getParent()->getRight() == oldnode) {
+ thisnode->getParent()->right_ = newnode;
+ } else {
+ thisnode->getParent()->down_ = newnode;
+ }
+ } else {
+ *root_ptr = newnode;
+ }
+ }
+
+ /// \brief Exchanges the location of two nodes (this and
+ /// lower). Their data remain the same, but their location in the
+ /// tree, colors and sub-tree root status may change. Note that this
+ /// is different from std::swap()-like behavior.
+ ///
+ /// IMPORTANT: A necessary pre-condition is that lower node must be
+ /// at a lower level in the tree than this node. This method is
+ /// primarily used in remove() and this pre-condition is followed
+ /// there.
+ ///
+ /// This method doesn't throw any exceptions.
+ void exchange(DomainTreeNode<T>* lower, DomainTreeNodePtr* root_ptr) {
+ // Swap the pointers first. down should not be swapped as it
+ // belongs to the node's data, and not to its position in the
+ // tree.
+
+ // NOTE: The conditions following the swaps below are
+ // asymmetric. We only need to check this for the lower node, as
+ // it can be a direct child of this node. The reverse is not
+ // possible.
+
+ std::swap(left_, lower->left_);
+ if (lower->getLeft() == lower) {
+ lower->left_ = this;
+ }
+
+ std::swap(right_, lower->right_);
+ if (lower->getRight() == lower) {
+ lower->right_ = this;
+ }
+
+ std::swap(parent_, lower->parent_);
+ if (getParent() == this) {
+ parent_ = lower;
+ }
+
+ // Update FLAG_RED and FLAG_SUBTREE_ROOT as these two are
+ // associated with the node's position.
+ const DomainTreeNodeColor this_color = getColor();
+ const bool this_is_subtree_root = isSubTreeRoot();
+ const DomainTreeNodeColor lower_color = lower->getColor();
+ const bool lower_is_subtree_root = lower->isSubTreeRoot();
+
+ lower->setColor(this_color);
+ setColor(lower_color);
+
+ setSubTreeRoot(lower_is_subtree_root);
+ lower->setSubTreeRoot(this_is_subtree_root);
+
+ lower->connectChild(this, lower, root_ptr);
+
+ if (getParent()->getLeft() == lower) {
+ getParent()->left_ = this;
+ } else if (getParent()->getRight() == lower) {
+ getParent()->right_ = this;
+ }
+
+ if (lower->getRight()) {
+ lower->getRight()->parent_ = lower;
+ }
+
+ if (lower->getLeft()) {
+ lower->getLeft()->parent_ = lower;
+ }
+ }
+
/// \brief Data stored here.
boost::interprocess::offset_ptr<T> data_;
@@ -550,17 +755,36 @@ DomainTreeNode<T>::~DomainTreeNode() {
}
template <typename T>
-const DomainTreeNode<T>*
-DomainTreeNode<T>::getSubTreeRoot() const {
- const DomainTreeNode<T>* current = this;
-
- // current would never be equal to NULL here (in a correct tree
+template <typename TT>
+TT*
+DomainTreeNode<T>::getSubTreeRootImpl(TT* node) {
+ // node would never be equal to NULL here (in a correct tree
// implementation)
- while (!current->isSubTreeRoot()) {
- current = current->getParent();
+ assert(node != NULL);
+
+ while (!node->isSubTreeRoot()) {
+ node = node->getParent();
}
- return (current);
+ return (node);
+}
+
+template <typename T>
+DomainTreeNode<T>*
+DomainTreeNode<T>::getSubTreeRoot() {
+ return (getSubTreeRootImpl<DomainTreeNode<T> >(this));
+}
+
+template <typename T>
+const DomainTreeNode<T>*
+DomainTreeNode<T>::getSubTreeRoot() const {
+ return (getSubTreeRootImpl<const DomainTreeNode<T> >(this));
+}
+
+template <typename T>
+DomainTreeNode<T>*
+DomainTreeNode<T>::getUpperNode() {
+ return (getSubTreeRoot()->getParent());
}
template <typename T>
@@ -595,40 +819,39 @@ DomainTreeNode<T>::getAbsoluteLabels(
}
template <typename T>
-const DomainTreeNode<T>*
-DomainTreeNode<T>::abstractSuccessor(
+template <typename TT>
+TT*
+DomainTreeNode<T>::abstractSuccessor(TT* node,
typename DomainTreeNode<T>::DomainTreeNodePtr DomainTreeNode<T>::*left,
typename DomainTreeNode<T>::DomainTreeNodePtr DomainTreeNode<T>::*right)
- const
{
// This function is written as a successor. It becomes predecessor if
// the left and right pointers are swapped. So in case of predecessor,
// the left pointer points to right and vice versa. Don't get confused
// by the idea, just imagine the pointers look into a mirror.
- const DomainTreeNode<T>* current = this;
// If it has right node, the successor is the left-most node of the right
// subtree.
- if ((current->*right).get() != NULL) {
- current = (current->*right).get();
+ if ((node->*right).get() != NULL) {
+ node = (node->*right).get();
const DomainTreeNode<T>* left_n;
- while ((left_n = (current->*left).get()) != NULL) {
- current = left_n;
+ while ((left_n = (node->*left).get()) != NULL) {
+ node = left_n;
}
- return (current);
+ return (node);
}
// Otherwise go up until we find the first left branch on our path to
// root. If found, the parent of the branch is the successor.
// Otherwise, we return the null node
- const DomainTreeNode<T>* parent = current->getParent();
- while ((!current->isSubTreeRoot()) &&
- (current == (parent->*right).get())) {
- current = parent;
+ const DomainTreeNode<T>* parent = node->getParent();
+ while ((!node->isSubTreeRoot()) &&
+ (node == (parent->*right).get())) {
+ node = parent;
parent = parent->getParent();
}
- if (!current->isSubTreeRoot()) {
+ if (!node->isSubTreeRoot()) {
return (parent);
} else {
return (NULL);
@@ -636,18 +859,33 @@ DomainTreeNode<T>::abstractSuccessor(
}
template <typename T>
+DomainTreeNode<T>*
+DomainTreeNode<T>::successor() {
+ return (abstractSuccessor<DomainTreeNode<T> >
+ (this, &DomainTreeNode<T>::left_, &DomainTreeNode<T>::right_));
+}
+
+template <typename T>
const DomainTreeNode<T>*
DomainTreeNode<T>::successor() const {
- return (abstractSuccessor(&DomainTreeNode<T>::left_,
- &DomainTreeNode<T>::right_));
+ return (abstractSuccessor<const DomainTreeNode<T> >
+ (this, &DomainTreeNode<T>::left_, &DomainTreeNode<T>::right_));
+}
+
+template <typename T>
+DomainTreeNode<T>*
+DomainTreeNode<T>::predecessor() {
+ // Swap the left and right pointers for the abstractSuccessor
+ return (abstractSuccessor<DomainTreeNode<T> >
+ (this, &DomainTreeNode<T>::right_, &DomainTreeNode<T>::left_));
}
template <typename T>
const DomainTreeNode<T>*
DomainTreeNode<T>::predecessor() const {
// Swap the left and right pointers for the abstractSuccessor
- return (abstractSuccessor(&DomainTreeNode<T>::right_,
- &DomainTreeNode<T>::left_));
+ return (abstractSuccessor<const DomainTreeNode<T> >
+ (this, &DomainTreeNode<T>::right_, &DomainTreeNode<T>::left_));
}
/// \brief DomainTreeNodeChain stores detailed information of \c
@@ -705,7 +943,13 @@ public:
// XXX: meaningless initial values:
last_comparison_(0, 0,
isc::dns::NameComparisonResult::EQUAL)
- {}
+ {
+ // To silence cppcheck. We don't really use the values before
+ // initialization, but this is cleaner anyway.
+ for (size_t i = 0; i < RBT_MAX_LEVEL; ++i) {
+ nodes_[i] = NULL;
+ }
+ }
/// \brief Copy constructor.
///
@@ -828,6 +1072,7 @@ private:
/// the top node
///
/// \exception None
+ // cppcheck-suppress unusedPrivateFunction (false positive, it is used)
void pop() {
assert(!isEmpty());
--level_count_;
@@ -840,6 +1085,7 @@ private:
/// otherwise the node should be the root node of DomainTree.
///
/// \exception None
+ // cppcheck-suppress unusedPrivateFunction (false positive, it is used)
void push(const DomainTreeNode<T>* node) {
assert(level_count_ < RBT_MAX_LEVEL);
nodes_[level_count_++] = node;
@@ -1008,7 +1254,7 @@ public:
DomainTree<T>* tree,
DataDeleter deleter)
{
- tree->deleteAllNodes(mem_sgmt, deleter);
+ tree->removeAllNodes(mem_sgmt, deleter);
tree->~DomainTree<T>();
mem_sgmt.deallocate(tree, sizeof(DomainTree<T>));
}
@@ -1084,45 +1330,19 @@ public:
/// of it. In that case, node parameter is left intact.
//@{
- /// \brief Simple find
- ///
- /// Acts as described in the \ref find section.
- Result find(const isc::dns::Name& name,
- const DomainTreeNode<T>** node) const {
- DomainTreeNodeChain<T> node_path;
- const isc::dns::LabelSequence ls(name);
- Result ret = (find<void*>(ls, node, node_path, NULL, NULL));
- return (ret);
- }
-
- /// \brief Simple find, with node_path tracking
- ///
- /// Acts as described in the \ref find section.
- Result find(const isc::dns::Name& name, const DomainTreeNode<T>** node,
- DomainTreeNodeChain<T>& node_path) const
- {
- const isc::dns::LabelSequence ls(name);
- Result ret = (find<void*>(ls, node, node_path, NULL, NULL));
- return (ret);
- }
-
- /// \brief Simple find returning immutable node.
- ///
- /// Acts as described in the \ref find section, but returns immutable
- /// node pointer.
- template <typename CBARG>
- Result find(const isc::dns::Name& name,
- const DomainTreeNode<T>** node,
- DomainTreeNodeChain<T>& node_path,
- bool (*callback)(const DomainTreeNode<T>&, CBARG),
- CBARG callback_arg) const
- {
- const isc::dns::LabelSequence ls(name);
- Result ret = find(ls, node, node_path, callback,
- callback_arg);
- return (ret);
- }
+private:
+ /// \brief Static helper function used by const and non-const
+ /// variants of find() below
+ template <typename TT, typename TTN, typename CBARG>
+ static Result findImpl(TT* tree,
+ const isc::dns::LabelSequence& target_labels_orig,
+ TTN** target,
+ TTN* node,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg);
+public:
/// \brief Find with callback and node chain
/// \anchor callback
///
@@ -1199,10 +1419,79 @@ public:
/// \c true, it returns immediately with the current node.
template <typename CBARG>
Result find(const isc::dns::LabelSequence& target_labels_orig,
+ DomainTreeNode<T>** node,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg);
+
+ /// \brief Find with callback and node chain (const variant)
+ template <typename CBARG>
+ Result find(const isc::dns::LabelSequence& target_labels_orig,
const DomainTreeNode<T>** node,
DomainTreeNodeChain<T>& node_path,
bool (*callback)(const DomainTreeNode<T>&, CBARG),
CBARG callback_arg) const;
+
+ /// \brief Simple find
+ ///
+ /// Acts as described in the \ref find section.
+ Result find(const isc::dns::Name& name,
+ DomainTreeNode<T>** node) {
+ const isc::dns::LabelSequence ls(name);
+ DomainTreeNodeChain<T> node_path;
+ return (find<void*>(ls, node, node_path, NULL, NULL));
+ }
+
+ /// \brief Simple find (const variant)
+ Result find(const isc::dns::Name& name,
+ const DomainTreeNode<T>** node) const {
+ const isc::dns::LabelSequence ls(name);
+ DomainTreeNodeChain<T> node_path;
+ return (find<void*>(ls, node, node_path, NULL, NULL));
+ }
+
+ /// \brief Simple find, with node_path tracking
+ ///
+ /// Acts as described in the \ref find section.
+ Result find(const isc::dns::Name& name, DomainTreeNode<T>** node,
+ DomainTreeNodeChain<T>& node_path)
+ {
+ const isc::dns::LabelSequence ls(name);
+ return (find<void*>(ls, node, node_path, NULL, NULL));
+ }
+
+ /// \brief Simple find, with node_path tracking (const variant)
+ Result find(const isc::dns::Name& name, const DomainTreeNode<T>** node,
+ DomainTreeNodeChain<T>& node_path) const
+ {
+ const isc::dns::LabelSequence ls(name);
+ return (find<void*>(ls, node, node_path, NULL, NULL));
+ }
+
+ /// \brief Simple find with callback
+ template <typename CBARG>
+ Result find(const isc::dns::Name& name,
+ DomainTreeNode<T>** node,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg)
+ {
+ const isc::dns::LabelSequence ls(name);
+ return (find<CBARG>(ls, node, node_path, callback, callback_arg));
+ }
+
+ /// \brief Simple find with callback (const variant)
+ template <typename CBARG>
+ Result find(const isc::dns::Name& name,
+ const DomainTreeNode<T>** node,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg) const
+ {
+ const isc::dns::LabelSequence ls(name);
+ return (find<CBARG>(ls, node, node_path, callback, callback_arg));
+ }
+
//@}
/// \brief return the next bigger node in DNSSEC order from a given node
@@ -1256,12 +1545,24 @@ public:
const DomainTreeNode<T>*
previousNode(DomainTreeNodeChain<T>& node_path) const;
+private:
+ /// \brief Static helper function used by const and non-const
+ /// variants of largestNode()
+ template <typename TT, typename TTN>
+ static TTN*
+ largestNodeImpl(TT* node);
+
+public:
/// \brief return the largest node in the tree of trees.
///
/// \throw none
///
/// \return A \c DomainTreeNode that is the largest node in the
/// tree. If there are no nodes, then \c NULL is returned.
+ DomainTreeNode<T>* largestNode();
+
+ /// \brief return the largest node in the tree of trees (const
+ /// variant).
const DomainTreeNode<T>* largestNode() const;
/// \brief Get the total number of nodes in the tree
@@ -1271,6 +1572,39 @@ public:
/// This function is mainly intended to be used for debugging.
uint32_t getNodeCount() const { return (node_count_); }
+private:
+ /// \brief Helper method for getHeight()
+ size_t getHeightHelper(const DomainTreeNode<T>* node) const;
+
+public:
+ /// \brief Return the maximum height of sub-root nodes found in the
+ /// DomainTree forest.
+ ///
+ /// The height of a node is defined as the number of nodes in the
+ /// longest path from the node to a leaf. For each subtree in the
+ /// DomainTree forest, this method determines the height of its root
+ /// node. Then it returns the maximum such height in the forest.
+ ///
+ /// Note: This method exists for testing purposes. Non-test code
+ /// must not use it.
+ size_t getHeight() const;
+
+private:
+ /// \brief Helper method for checkProperties()
+ bool checkPropertiesHelper(const DomainTreeNode<T>* node) const;
+
+ /// \brief Helper for checkProperties()
+ bool checkBlackDistanceHelper(const DomainTreeNode<T>* node,
+ size_t* distance)
+ const;
+
+public:
+ /// \brief Check red-black properties of the DomainTree.
+ ///
+ /// Note: This method exists for testing purposes. Non-test code
+ /// must not use it.
+ bool checkProperties() const;
+
/// \name Debug function
//@{
/// \brief Print the nodes in the trees.
@@ -1305,15 +1639,24 @@ public:
/// doesn't exist.
///
/// This method normally involves resource allocation. If it fails
- /// the corresponding standard exception will be thrown.
+ /// \c std::bad_alloc will be thrown. Also, depending on details of the
+ /// specific \c MemorySegment, it can propagate the \c MemorySegmentGrown
+ /// exception.
///
/// This method does not provide the strong exception guarantee in its
- /// strict sense; if an exception is thrown in the middle of this
- /// method, the internal structure may change. However, it should
- /// still retain the same property as a mapping container before this
- /// method is called. For example, the result of \c find() should be
- /// the same. This method provides the weak exception guarantee in its
- /// normal sense.
+ /// strict sense; there can be new empty nodes that are superdomains of
+ /// the domain to be inserted as a side effect. However, the tree
+ /// retains internal integrity otherwise, and, in particular, the intended
+ /// insert operation is "resumable": if the \c insert() method is called
+ /// again with the same argument after resolving the cause of the
+ /// exception (possibly multiple times), it should now succeed. Note,
+ /// however, that in case of \c MemorySegmentGrown the address of the
+ /// `DomainTree` object may have been reallocated if it was created with
+ /// the same \c MemorySegment (which will often be the case in practice).
+ /// So the caller may have to re-get the address before calling \c insert
+ /// again. It can be done using the concept of "named addresses" of
+ /// \c MemorySegment, or the direct caller may not have to worry about it
+ /// if this condition is guaranteed at a higher level.
///
/// \param mem_sgmt A \c MemorySegment object for allocating memory of
/// a new node to be inserted. Must be the same segment as that used
@@ -1329,16 +1672,31 @@ public:
Result insert(util::MemorySegment& mem_sgmt, const isc::dns::Name& name,
DomainTreeNode<T>** inserted_node);
+ /// \brief Delete a tree node.
+ ///
+ /// \throw none.
+ ///
+ /// \param mem_sgmt The \c MemorySegment object used to insert the nodes
+ /// (which was also used for creating the tree due to the requirement of
+ /// \c insert()).
+ /// \param node The node to delete.
+ /// \param deleter The \c DataDeleter used to destroy data stored in
+ /// the tree nodes.
+ template <typename DataDeleter>
+ void remove(util::MemorySegment& mem_sgmt, DomainTreeNode<T>* node,
+ DataDeleter deleter);
+
/// \brief Delete all tree nodes.
///
/// \throw none.
///
/// \param mem_sgmt The \c MemorySegment object used to insert the nodes
/// (which was also used for creating the tree due to the requirement of
- /// \c inert()).
- /// \param deleter A deleter functor or function to delete node data.
+ /// \c insert()).
+ /// \param deleter The \c DataDeleter used to destroy data stored in
+ /// the tree nodes.
template <typename DataDeleter>
- void deleteAllNodes(util::MemorySegment& mem_sgmt, DataDeleter deleter);
+ void removeAllNodes(util::MemorySegment& mem_sgmt, DataDeleter deleter);
/// \brief Swaps two tree's contents.
///
@@ -1361,6 +1719,10 @@ private:
insertRebalance(typename DomainTreeNode<T>::DomainTreeNodePtr* root,
DomainTreeNode<T>* node);
+ void
+ removeRebalance(typename DomainTreeNode<T>::DomainTreeNodePtr* root_ptr,
+ DomainTreeNode<T>* child, DomainTreeNode<T>* parent);
+
DomainTreeNode<T>*
rightRotate(typename DomainTreeNode<T>::DomainTreeNodePtr* root,
DomainTreeNode<T>* node);
@@ -1399,6 +1761,7 @@ private:
void nodeFission(util::MemorySegment& mem_sgmt, DomainTreeNode<T>& node,
const isc::dns::LabelSequence& new_prefix,
const isc::dns::LabelSequence& new_suffix);
+
//@}
typename DomainTreeNode<T>::DomainTreeNodePtr root_;
@@ -1463,13 +1826,15 @@ DomainTree<T>::deleteHelper(util::MemorySegment& mem_sgmt,
}
template <typename T>
-template <typename CBARG>
+template <typename TT, typename TTN, typename CBARG>
typename DomainTree<T>::Result
-DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
- const DomainTreeNode<T>** target,
- DomainTreeNodeChain<T>& node_path,
- bool (*callback)(const DomainTreeNode<T>&, CBARG),
- CBARG callback_arg) const
+DomainTree<T>::findImpl(TT* tree,
+ const isc::dns::LabelSequence& target_labels_orig,
+ TTN** target,
+ TTN* node,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg)
{
if (node_path.isEmpty() ^ target_labels_orig.isAbsolute()) {
isc_throw(isc::BadValue,
@@ -1477,17 +1842,6 @@ DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
" and label sequence");
}
- const DomainTreeNode<T>* node;
-
- if (!node_path.isEmpty()) {
- // Get the top node in the node chain
- node = node_path.top();
- // Start searching from its down pointer
- node = node->getDown();
- } else {
- node = root_.get();
- }
-
Result ret = NOTFOUND;
dns::LabelSequence target_labels(target_labels_orig);
@@ -1498,7 +1852,7 @@ DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
node_path.last_comparison_.getRelation();
if (relation == isc::dns::NameComparisonResult::EQUAL) {
- if (needsReturnEmptyNode_ || !node->isEmpty()) {
+ if (tree->needsReturnEmptyNode_ || !node->isEmpty()) {
node_path.push(node);
*target = node;
ret = EXACTMATCH;
@@ -1511,7 +1865,7 @@ DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
node->getLeft() : node->getRight();
} else {
if (relation == isc::dns::NameComparisonResult::SUBDOMAIN) {
- if (needsReturnEmptyNode_ || !node->isEmpty()) {
+ if (tree->needsReturnEmptyNode_ || !node->isEmpty()) {
ret = PARTIALMATCH;
*target = node;
if (callback != NULL &&
@@ -1535,6 +1889,53 @@ DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
}
template <typename T>
+template <typename CBARG>
+typename DomainTree<T>::Result
+DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
+ DomainTreeNode<T>** target,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg)
+{
+ if (!node_path.isEmpty()) {
+ isc_throw(isc::BadValue,
+ "DomainTree::find() non-const method is given "
+ "non-empty node chain");
+ }
+
+ DomainTreeNode<T>* node = root_.get();
+
+ return (findImpl<DomainTree<T>, DomainTreeNode<T>, CBARG >
+ (this, target_labels_orig, target, node, node_path,
+ callback, callback_arg));
+}
+
+template <typename T>
+template <typename CBARG>
+typename DomainTree<T>::Result
+DomainTree<T>::find(const isc::dns::LabelSequence& target_labels_orig,
+ const DomainTreeNode<T>** target,
+ DomainTreeNodeChain<T>& node_path,
+ bool (*callback)(const DomainTreeNode<T>&, CBARG),
+ CBARG callback_arg) const
+{
+ const DomainTreeNode<T>* node;
+
+ if (!node_path.isEmpty()) {
+ // Get the top node in the node chain
+ node = node_path.top();
+ // Start searching from its down pointer
+ node = node->getDown();
+ } else {
+ node = root_.get();
+ }
+
+ return (findImpl<const DomainTree<T>, const DomainTreeNode<T>, CBARG >
+ (this, target_labels_orig, target, node, node_path,
+ callback, callback_arg));
+}
+
+template <typename T>
const DomainTreeNode<T>*
DomainTree<T>::nextNode(DomainTreeNodeChain<T>& node_path) const {
if (node_path.isEmpty()) {
@@ -1717,9 +2118,10 @@ DomainTree<T>::previousNode(DomainTreeNodeChain<T>& node_path) const {
}
template <typename T>
-const DomainTreeNode<T>*
-DomainTree<T>::largestNode() const {
- const DomainTreeNode<T>* node = root_.get();
+template <typename TT, typename TTN>
+TTN*
+DomainTree<T>::largestNodeImpl(TT* tree) {
+ TTN* node = tree->root_.get();
while (node != NULL) {
// We go right first, then down.
if (node->getRight() != NULL) {
@@ -1735,6 +2137,19 @@ DomainTree<T>::largestNode() const {
}
template <typename T>
+DomainTreeNode<T>*
+DomainTree<T>::largestNode() {
+ return (largestNodeImpl<DomainTree<T>, DomainTreeNode<T> >(this));
+}
+
+template <typename T>
+const DomainTreeNode<T>*
+DomainTree<T>::largestNode() const {
+ return (largestNodeImpl<const DomainTree<T>, const DomainTreeNode<T> >
+ (this));
+}
+
+template <typename T>
typename DomainTree<T>::Result
DomainTree<T>::insert(util::MemorySegment& mem_sgmt,
const isc::dns::Name& target_name,
@@ -1802,11 +2217,13 @@ DomainTree<T>::insert(util::MemorySegment& mem_sgmt,
} else if (order < 0) {
node->setSubTreeRoot(false);
parent->left_ = node;
+ insertRebalance(current_root, node);
} else {
node->setSubTreeRoot(false);
parent->right_ = node;
+ insertRebalance(current_root, node);
}
- insertRebalance(current_root, node);
+
if (new_node != NULL) {
*new_node = node;
}
@@ -1818,7 +2235,127 @@ DomainTree<T>::insert(util::MemorySegment& mem_sgmt,
template <typename T>
template <typename DataDeleter>
void
-DomainTree<T>::deleteAllNodes(util::MemorySegment& mem_sgmt,
+DomainTree<T>::remove(util::MemorySegment& mem_sgmt, DomainTreeNode<T>* node,
+ DataDeleter deleter)
+{
+ // If node has a down pointer, we cannot remove this node from the
+ // DomainTree forest. We merely clear its data (destroying the data)
+ // and return.
+ if (node->getDown()) {
+ T* data = node->setData(NULL);
+ if (data) {
+ deleter(data);
+ }
+ return;
+ }
+
+ while (true) {
+ // Save subtree root's parent for use later.
+ DomainTreeNode<T>* upper_node = node->getUpperNode();
+
+ // node points to the node to be deleted in the BST. It first
+ // has to be exchanged with the right-most node in the left
+ // sub-tree or the left-most node in the right sub-tree. (Here,
+ // sub-tree is inside this RB tree itself, not in the
+ // tree-of-trees forest.) The node then ends up having a maximum
+ // of 1 child. Note that this is not an in-place value swap of
+ // node data, but the actual node locations are swapped in
+ // exchange(). Unlike normal BSTs, we have to do this as our
+ // label data is at address (this + 1).
+ if (node->getLeft() && node->getRight()) {
+ DomainTreeNode<T>* rightmost = node->getLeft();
+ while (rightmost->getRight() != NULL) {
+ rightmost = rightmost->getRight();
+ }
+
+ node->exchange(rightmost, &root_);
+ }
+
+ // Now, node has 0 or 1 children, as from above, either its
+ // right or left child definitely doesn't exist (and is NULL).
+ // Pick the child node, or if no children exist, just use NULL.
+ DomainTreeNode<T>* child;
+ if (node->getRight()) {
+ child = node->getRight();
+ } else {
+ child = node->getLeft();
+ }
+
+ // Set it as the node's parent's child, effectively removing
+ // node from the tree.
+ node->connectChild(node, child, &root_);
+
+ // Child can be NULL here if node was a leaf.
+ if (child) {
+ child->parent_ = node->getParent();
+ // Even if node is not a leaf node, we don't always do an
+ // exchange() with another node, so we have to set the
+ // child's FLAG_SUBTREE_ROOT explicitly.
+ if ((!child->getParent()) ||
+ (child->getParent()->getDown() == child))
+ {
+ child->setSubTreeRoot(node->isSubTreeRoot());
+ }
+ }
+
+ // If node is RED, it is a valid red-black tree already as
+ // (node's) child must be BLACK or NULL (which is
+ // BLACK). Deleting (the RED) node will not have any effect on
+ // the number of BLACK nodes through this path (involving node's
+ // parent and its new child). In this case, we can skip the
+ // following block.
+ if (node->isBlack()) {
+ if (child && child->isRed()) {
+ // If node is BLACK and child is RED, removing node
+ // would decrease the number of BLACK nodes through this
+ // path (involving node's parent and its new child). So
+ // we color child to be BLACK to restore the old count
+ // of black nodes through this path. It is now a valid
+ // red-black tree.
+ child->setColor(DomainTreeNode<T>::BLACK);
+ } else {
+ // If node is BLACK and child is also BLACK or NULL
+ // (which is BLACK), we need to do re-balancing to make
+ // it a valid red-black tree again.
+ typename DomainTreeNode<T>::DomainTreeNodePtr* root_ptr =
+ upper_node ? &(upper_node->down_) : &root_;
+ removeRebalance(root_ptr, child, node->getParent());
+ }
+ }
+
+ // Finally, destroy the node.
+ if (node->data_.get()) {
+ deleter(node->data_.get());
+ }
+ DomainTreeNode<T>::destroy(mem_sgmt, node);
+ --node_count_;
+
+ // If the node deletion did not cause the subtree to disappear
+ // completely, return early.
+ if (upper_node->getDown()) {
+ break;
+ }
+
+ // If the upper node is not empty, it cannot be deleted.
+ if (!upper_node->isEmpty()) {
+ break;
+ }
+
+ // If upper node is the root node (.), don't attempt to delete
+ // it. The root node must always exist.
+ if (upper_node == root_.get()) {
+ break;
+ }
+
+ // Ascend up the tree and delete the upper node.
+ node = upper_node;
+ }
+}
+
+template <typename T>
+template <typename DataDeleter>
+void
+DomainTree<T>::removeAllNodes(util::MemorySegment& mem_sgmt,
DataDeleter deleter)
{
deleteHelper(mem_sgmt, root_.get(), deleter);
@@ -1842,17 +2379,8 @@ DomainTree<T>::nodeFission(util::MemorySegment& mem_sgmt,
node.resetLabels(new_prefix);
up_node->parent_ = node.getParent();
- if (node.getParent() != NULL) {
- if (node.getParent()->getLeft() == &node) {
- node.getParent()->left_ = up_node;
- } else if (node.getParent()->getRight() == &node) {
- node.getParent()->right_ = up_node;
- } else {
- node.getParent()->down_ = up_node;
- }
- } else {
- root_ = up_node;
- }
+
+ node.connectChild(&node, up_node, &root_);
up_node->down_ = &node;
node.parent_ = up_node;
@@ -1882,63 +2410,422 @@ DomainTree<T>::nodeFission(util::MemorySegment& mem_sgmt,
}
+/// \brief Fix Red-Black tree properties after an ordinary BST
+/// insertion.
+///
+/// After a normal binary search tree insertion, the Red-Black tree
+/// properties may be violated. This method fixes these properties by
+/// doing tree rotations and recoloring nodes in the tree appropriately.
+///
+/// \param subtree_root The root of the current sub-tree where the node
+/// is being inserted.
+/// \param node The node which was inserted by ordinary BST insertion.
template <typename T>
void
DomainTree<T>::insertRebalance
- (typename DomainTreeNode<T>::DomainTreeNodePtr* root,
+ (typename DomainTreeNode<T>::DomainTreeNodePtr* subtree_root,
DomainTreeNode<T>* node)
{
- DomainTreeNode<T>* uncle;
- DomainTreeNode<T>* parent;
- while (node != (*root).get() &&
- ((parent = node->getParent())->getColor()) ==
- DomainTreeNode<T>::RED) {
- // Here, node->parent_ is not NULL and it is also red, so
- // node->parent_->parent_ is also not NULL.
- if (parent == parent->getParent()->getLeft()) {
- uncle = parent->getParent()->getRight();
-
- if (uncle != NULL && uncle->getColor() ==
- DomainTreeNode<T>::RED) {
- parent->setColor(DomainTreeNode<T>::BLACK);
- uncle->setColor(DomainTreeNode<T>::BLACK);
- parent->getParent()->setColor(DomainTreeNode<T>::RED);
- node = parent->getParent();
- } else {
- if (node == parent->getRight()) {
- node = parent;
- leftRotate(root, node);
- parent = node->getParent();
- }
- parent->setColor(DomainTreeNode<T>::BLACK);
- parent->getParent()->setColor(DomainTreeNode<T>::RED);
- rightRotate(root, parent->getParent());
- }
+ // The node enters this method colored RED. We assume in our
+ // red-black implementation that NULL values in left and right
+ // children are BLACK.
+ //
+ // Case 1. If node is at the subtree root, we don't need to change
+ // its position in the tree. We re-color it BLACK further below
+ // (right before we return).
+ while (node != (*subtree_root).get()) {
+ // Case 2. If the node is not subtree root, but its parent is
+ // colored BLACK, then we're done. This is because the new node
+ // introduces a RED node in the path through it (from its
+ // subtree root to its children colored BLACK) but doesn't
+ // change the red-black properties.
+ DomainTreeNode<T>* parent = node->getParent();
+ if (parent->isBlack()) {
+ break;
+ }
+
+ DomainTreeNode<T>* uncle = node->getUncle();
+ DomainTreeNode<T>* grandparent = node->getGrandParent();
+
+ if ((uncle != NULL) && uncle->isRed()) {
+ // Case 3. Here, the node's parent is colored RED and the
+ // uncle node is also RED. In this case, the grandparent
+ // must be BLACK (due to existing red-black state). We set
+ // both the parent and uncle nodes to BLACK then, change the
+ // grandparent to RED, and iterate the while loop with
+ // node = grandparent. This is the only case that causes
+ // insertion to have a max insertion time of log(n).
+ parent->setColor(DomainTreeNode<T>::BLACK);
+ uncle->setColor(DomainTreeNode<T>::BLACK);
+ grandparent->setColor(DomainTreeNode<T>::RED);
+ node = grandparent;
} else {
- uncle = parent->getParent()->getLeft();
+ // Case 4. Here, the node and node's parent are colored RED,
+ // and the uncle node is BLACK. Only in this case, tree
+ // rotations are necessary.
+
+ /* First we check if we need to convert to a canonical form:
+ *
+ * (a) If the node is the right-child of its parent, and the
+ * node's parent is the left-child of the node's
+ * grandparent, rotate left about the parent so that the old
+ * 'node' becomes the new parent, and the old parent becomes
+ * the new 'node'.
+ *
+ * G(B) G(B)
+ * / \ / \
+ * P(R) U(B) => P*(R) U(B)
+ * \ /
+ * N(R) N*(R)
+ *
+ * (P* is old N, N* is old P)
+ *
+ * (b) If the node is the left-child of its parent, and the
+ * node's parent is the right-child of the node's
+ * grandparent, rotate right about the parent so that the
+ * old 'node' becomes the new parent, and the old parent
+ * becomes the new 'node'.
+ *
+ * G(B) G(B)
+ * / \ / \
+ * U(B) P(R) => U(B) P*(R)
+ * / \
+ * N(R) N*(R)
+ *
+ * (P* is old N, N* is old P)
+ */
+ if ((node == parent->getRight()) &&
+ (parent == grandparent->getLeft())) {
+ node = parent;
+ leftRotate(subtree_root, parent);
+ } else if ((node == parent->getLeft()) &&
+ (parent == grandparent->getRight())) {
+ node = parent;
+ rightRotate(subtree_root, parent);
+ }
- if (uncle != NULL && uncle->getColor() ==
- DomainTreeNode<T>::RED) {
- parent->setColor(DomainTreeNode<T>::BLACK);
- uncle->setColor(DomainTreeNode<T>::BLACK);
- parent->getParent()->setColor(DomainTreeNode<T>::RED);
- node = parent->getParent();
+ // Also adjust the parent variable (node is already adjusted
+ // above). The grandparent variable need not be adjusted.
+ parent = node->getParent();
+
+ /* Here, we're in a canonical form where the uncle node is
+ * BLACK and both the node and its parent are together
+ * either left-children or right-children of their
+ * corresponding parents.
+ *
+ * G(B) or G(B)
+ * / \ / \
+ * P(R) U(B) U(B) P(R)
+ * / \
+ * N(R) N(R)
+ *
+ * We rotate around the grandparent, right or left,
+ * depending on the orientation above, color the old
+ * grandparent RED (it used to be BLACK) and color the
+ * parent BLACK (it used to be RED). This restores the
+ * red-black property that the number of BLACK nodes from
+ * subtree root to the leaves (the NULL children which are
+ * assumed BLACK) are equal, and that every RED node has a
+ * BLACK parent.
+ */
+ parent->setColor(DomainTreeNode<T>::BLACK);
+ grandparent->setColor(DomainTreeNode<T>::RED);
+
+ if (node == parent->getLeft()) {
+ rightRotate(subtree_root, grandparent);
} else {
- if (node == parent->getLeft()) {
- node = parent;
- rightRotate(root, node);
- parent = node->getParent();
- }
- parent->setColor(DomainTreeNode<T>::BLACK);
- parent->getParent()->setColor(DomainTreeNode<T>::RED);
- leftRotate(root, parent->getParent());
+ leftRotate(subtree_root, grandparent);
}
+
+ // In this case, the tree is ready now and we explicitly
+ // break out of the loop here. Even if we continue the loop,
+ // it will exit the loop in case 2 above, but that's not so
+ // obvious.
+ break;
}
}
- (*root)->setColor(DomainTreeNode<T>::BLACK);
+ // Color sub-tree roots black.
+ (*subtree_root)->setColor(DomainTreeNode<T>::BLACK);
}
+template <typename T>
+void
+DomainTree<T>::removeRebalance
+ (typename DomainTreeNode<T>::DomainTreeNodePtr* root_ptr,
+ DomainTreeNode<T>* child, DomainTreeNode<T>* parent)
+{
+ // Case 1. Repeat until we reach the root node of this subtree in
+ // the forest. Note that child can be NULL here, so we can only test
+ // the parent pointer and see if it has escaped to the upper tree.
+ while (&(parent->down_) != root_ptr) {
+ // A sibling node is defined as the parent's other child. It
+ // exists at the same level as child. Note that child can be
+ // NULL here.
+ DomainTreeNode<T>* sibling =
+ DomainTreeNode<T>::getSibling(parent, child);
+
+ // NOTE #1: Understand this clearly. We are here only because in
+ // the path through parent--child, a BLACK node was removed,
+ // i.e., the sibling's side in the path through parent--sibling
+ // is heavier by 1 extra BLACK node in its path. Because this
+ // can be an iterative process up the tree, the key is to
+ // understand this point when entering the block here.
+
+ // NOTE #2: sibling cannot be NULL here as parent--child has
+ // fewer BLACK nodes than parent--sibling.
+ assert(sibling);
+
+ // If sibling is RED, convert the tree to a form where sibling
+ // is BLACK.
+ if (sibling->isRed()) {
+ // Case 2. Here, the sibling is RED. We do a tree rotation
+ // at the parent such that sibling is the new parent, and
+ // the old parent is sibling's child. We also invert the
+ // colors of the two nodes.
+ //
+ // This step is done to convert the tree to a form for
+ // further cases below.
+
+ /* Parent (P) has to be BLACK here as its child sibling (S)
+ * is RED.
+ *
+ * P(B) S(B)
+ * / \ / \
+ * C(?) S(R) => P(R) y(B)
+ * / \ / \ / \
+ * x(B) y(B) C(?) x(B)
+ * / \
+ */
+
+ parent->setColor(DomainTreeNode<T>::RED);
+ sibling->setColor(DomainTreeNode<T>::BLACK);
+
+ if (parent->getLeft() == child) {
+ leftRotate(root_ptr, parent);
+ } else {
+ rightRotate(root_ptr, parent);
+ }
+
+ // Re-compute child's sibling due to the tree adjustment
+ // above.
+ sibling = DomainTreeNode<T>::getSibling(parent, child);
+ }
+
+ // NOTE #3: sibling still cannot be NULL here as parent--child
+ // has fewer BLACK nodes than parent--sibling.
+ assert(sibling);
+
+ // NOTE #4: From above, sibling must be BLACK here.
+ assert(sibling->isBlack());
+
+ // NOTE #5: If a tree rotation happened above, the new sibling's
+ // side through parent--sibling [x(B)] above is still heavier
+ // than parent--child by 1 extra BLACK node in its path.
+
+ // Case 3. If both of sibling's children are BLACK, then set the
+ // sibling's color to RED. This reduces the number of BLACK
+ // nodes in parent--sibling path by 1 and balances the BLACK
+ // nodes count on both sides of parent. But this introduces
+ // another issue which is that the path through one child
+ // (=parent) of parent's parent (child's grandparent) has fewer
+ // BLACK nodes now than the other child (parent's sibling).
+ //
+ // To fix this: (a) if parent is colored RED, we can change its
+ // color to BLACK (to increment the number of black nodes in
+ // grandparent--parent-->path) and we're done with the
+ // rebalancing; (b) if parent is colored BLACK, then we set
+ // child=parent and go back to the beginning of the loop to
+ // repeat the original rebalancing problem 1 node higher up the
+ // tree (see NOTE #1 above).
+
+ /* (a):
+ *
+ * G(?) G(?)
+ * / \ / \
+ * P(R) => P(B) (Rebalancing is complete)
+ * / \ / \
+ * C(?) S(B) C(?) S(R)
+ * / \ / \ / \ / \
+ * ss1(B) ss2(B) ss1(B) ss2(B)
+ *
+ *
+ * (b):
+ *
+ * G(?) G(?) <----------(New parent)
+ * / \ / \
+ * P(B) => P(B) <------------(New child)
+ * / \ / \
+ * C(?) S(B) C(?) S(R)
+ * / \ / \ / \ / \
+ * ss1(B) ss2(B) ss1(B) ss2(B)
+ */
+
+ if ((DomainTreeNode<T>::isBlack(sibling->getLeft()) &&
+ DomainTreeNode<T>::isBlack(sibling->getRight())))
+ {
+ sibling->setColor(DomainTreeNode<T>::RED);
+
+ if (parent->isBlack()) {
+ child = parent;
+ parent = parent->getParent();
+ continue;
+ } else {
+ parent->setColor(DomainTreeNode<T>::BLACK);
+ break;
+ }
+ }
+
+ // NOTE #3 and NOTE #4 asserted above still hold here.
+ assert(sibling);
+ assert(sibling->isBlack());
+
+ // NOTE #6: The path through parent--sibling is still heavier
+ // than parent--child by 1 extra BLACK node in its path. This is
+ // the key point, and this is why we are still doing the
+ // rebalancing.
+
+ // Case 4. Now, one or both of sibling's children are not
+ // BLACK. (a) We consider the case where child is the left-child
+ // of parent, and the left-child of sibling is RED and the
+ // right-child of sibling is BLACK. (b) We also consider its
+ // mirror, arrangement, i.e., the case where child is the
+ // right-child of parent, and the right-child of sibling is RED
+ // and the left-child of sibling is BLACK.
+ //
+ // In both cases, we change sibling's color to RED, the color of
+ // the RED child of sibling to BLACK (so both children of
+ // sibling are BLACK), and we do a tree rotation around sibling
+ // node in the opposite direction of the old RED child of
+ // sibling.
+ //
+ // This step is done to convert the tree to a form for further
+ // cases below.
+
+ /* (a):
+ *
+ * P(?) => P(?)
+ * / \ / \
+ * C(?) S(B) C(?) ss1(B)
+ * / \ / \ / \ / \
+ * ss1(R) ss2(B) x(B) S(R)
+ * / \ / \
+ * x(B) y(B) y(B) ss2(B)
+ *
+ *
+ * (b):
+ *
+ * P(?) => P(?)
+ * / \ / \
+ * S(B) C(?) ss1(B) C(?)
+ * / \ / \ / \ / \
+ * ss2(B) ss1(R) S(R) x(B)
+ * / \ / \
+ * y(B) x(B) ss2(B) y(B)
+ */
+
+ DomainTreeNode<T>* ss1 = sibling->getLeft();
+ DomainTreeNode<T>* ss2 = sibling->getRight();
+ if (parent->getLeft() != child) {
+ // Swap for the mirror arrangement described in case 4 (b)
+ // above.
+ std::swap(ss1, ss2);
+ }
+
+ if (DomainTreeNode<T>::isBlack(ss2)) {
+ // It is implied by ss2 being BLACK that ss1 is RED.
+
+ sibling->setColor(DomainTreeNode<T>::RED);
+ // ss1 cannot be NULL here as it is a RED node.
+ ss1->setColor(DomainTreeNode<T>::BLACK);
+
+ if (parent->getLeft() == child) {
+ rightRotate(root_ptr, sibling);
+ } else {
+ leftRotate(root_ptr, sibling);
+ }
+ // Re-compute child's sibling due to the tree adjustment
+ // above.
+ sibling = DomainTreeNode<T>::getSibling(parent, child);
+ }
+
+ // NOTE #7: sibling cannot be NULL here as even if the sibling
+ // variable was assigned in the node above, it was set to ss1
+ // which was a RED node before (non-NULL).
+ assert(sibling);
+
+ // NOTE #8: sibling is still BLACK, even if the sibling variable
+ // was assigned in the node above, it was set to ss1 which is
+ // now a BLACK node.
+ assert(sibling->isBlack());
+
+ // NOTE #9: The path through parent--sibling is still heavier
+ // than parent--child by 1 extra BLACK node in its path. This is
+ // the key point, and this is why we are still doing the
+ // rebalancing.
+
+ // Case 5. After case 4 above, we are in a canonical form now
+ // where sibling is BLACK, and either (a) if child is the
+ // left-child of parent, the right-child (ss2) of sibling is
+ // definitely RED, or (b) if child is the right-child of parent,
+ // the left-child (ss2) of sibling is definitely RED.
+ //
+ // Here, we set sibling's color to that of the parent, and set
+ // the parent's color to BLACK. We set ss2's color to BLACK (it
+ // was previously RED). Then we do a tree-rotation around parent
+ // in the direction of child to pull in the BLACK parent into
+ // its sub-tree. These steps effectively balances the tree as
+ // you can see from the graph below. Before starting, the graph
+ // on the child's side is lighter by 1 BLACK node than the graph
+ // on the sibling's side. After these steps, both sides of S(x)
+ // have the same number of BLACK nodes in any path through it.
+
+ /* (a):
+ *
+ * P(x) S(x)
+ * / \ / \
+ * C(?) S(B) => P(B) ss2(B)
+ * / \ / \ / \ / \
+ * ss1(?) ss2(R) C(?) ss1(?) (B) (B)
+ * / \ / \
+ * (B) (B)
+ *
+ * (Here, 'x' is the parent's color. In the resulting tree,
+ * it is set as S node's color.)
+ *
+ * (b):
+ * This is just the mirror of above.
+ */
+
+ sibling->setColor(parent->getColor());
+ parent->setColor(DomainTreeNode<T>::BLACK);
+
+ ss1 = sibling->getLeft();
+ ss2 = sibling->getRight();
+ if (parent->getLeft() != child) {
+ // Swap for the mirror arrangement described in case 4 (b)
+ // above.
+ std::swap(ss1, ss2);
+ }
+
+ // ss2 cannot be NULL here as it is a RED node.
+ ss2->setColor(DomainTreeNode<T>::BLACK);
+
+ if (parent->getLeft() == child) {
+ leftRotate(root_ptr, parent);
+ } else {
+ rightRotate(root_ptr, parent);
+ }
+
+ // NOTE #10: Now, sibling is parent's parent. All paths through
+ // sibling have the same number of BLACK nodes.
+
+ // We are completely finished and the tree is now
+ // balanced. Phew. Now let's take a coffee-break.
+
+ break;
+ }
+}
template <typename T>
DomainTreeNode<T>*
@@ -2008,6 +2895,112 @@ DomainTree<T>::rightRotate
return (node);
}
+template <typename T>
+size_t
+DomainTree<T>::getHeightHelper(const DomainTreeNode<T>* node) const {
+ if (node == NULL) {
+ return (0);
+ }
+
+ const size_t dl = getHeightHelper(node->getLeft());
+ const size_t dr = getHeightHelper(node->getRight());
+
+ const size_t this_height = std::max(dl + 1, dr + 1);
+ const size_t down_height = getHeightHelper(node->getDown());
+
+ return (std::max(this_height, down_height));
+}
+
+template <typename T>
+size_t
+DomainTree<T>::getHeight() const {
+ return (getHeightHelper(root_.get()));
+}
+
+template <typename T>
+bool
+DomainTree<T>::checkPropertiesHelper(const DomainTreeNode<T>* node) const {
+ if (node == NULL) {
+ return (true);
+ }
+
+ if (node->isRed()) {
+ // Root nodes must be BLACK.
+ if (node->isSubTreeRoot()) {
+ return (false);
+ }
+
+ // Both children of RED nodes must be BLACK.
+ if (DomainTreeNode<T>::isRed(node->getLeft()) ||
+ DomainTreeNode<T>::isRed(node->getRight()))
+ {
+ return (false);
+ }
+ }
+
+ // If node is assigned to the down_ pointer of its parent, it is a
+ // subtree root and must have the flag set.
+ if (((!node->getParent()) ||
+ (node->getParent()->getDown() == node)) &&
+ (!node->isSubTreeRoot()))
+ {
+ return (false);
+ }
+
+ // Repeat tests with this node's children.
+ return (checkPropertiesHelper(node->getLeft()) &&
+ checkPropertiesHelper(node->getRight()) &&
+ checkPropertiesHelper(node->getDown()));
+}
+
+template <typename T>
+bool
+DomainTree<T>::checkBlackDistanceHelper(const DomainTreeNode<T>* node,
+ size_t* distance) const
+{
+ if (node == NULL) {
+ *distance = 1;
+ return (true);
+ }
+
+ size_t dl, dr, dd;
+ if (!checkBlackDistanceHelper(node->getLeft(), &dl)) {
+ return (false);
+ }
+ if (!checkBlackDistanceHelper(node->getRight(), &dr)) {
+ return (false);
+ }
+ if (!checkBlackDistanceHelper(node->getDown(), &dd)) {
+ return (false);
+ }
+
+ if (dl != dr) {
+ return (false);
+ }
+
+ if (node->isBlack()) {
+ ++dl;
+ }
+
+ *distance = dl;
+
+ return (true);
+}
+
+template <typename T>
+bool
+DomainTree<T>::checkProperties() const {
+ if (!checkPropertiesHelper(root_.get())) {
+ return (false);
+ }
+
+ // Path from a given node to all its leaves must contain the same
+ // number of BLACK child nodes. This is done separately here instead
+ // of inside checkPropertiesHelper() as it would take (n log n)
+ // complexity otherwise.
+ size_t dd;
+ return (checkBlackDistanceHelper(root_.get(), &dd));
+}
template <typename T>
void
@@ -2031,7 +3024,7 @@ DomainTree<T>::dumpTreeHelper(std::ostream& os,
indent(os, depth);
os << node->getLabels() << " ("
- << ((node->getColor() == DomainTreeNode<T>::BLACK) ? "black" : "red")
+ << (node->isBlack() ? "black" : "red")
<< ")";
if (node->isEmpty()) {
os << " [invisible]";
@@ -2095,7 +3088,7 @@ DomainTree<T>::dumpDotHelper(std::ostream& os,
}
os << "\"] [";
- if (node->getColor() == DomainTreeNode<T>::RED) {
+ if (node->isRed()) {
os << "color=red";
} else {
os << "color=black";
diff --git a/src/lib/datasrc/memory/memory_client.cc b/src/lib/datasrc/memory/memory_client.cc
index d49359a..0b0c827 100644
--- a/src/lib/datasrc/memory/memory_client.cc
+++ b/src/lib/datasrc/memory/memory_client.cc
@@ -69,11 +69,11 @@ InMemoryClient::findZone(const isc::dns::Name& zone_name) const {
const ZoneTable::FindResult result(zone_table->findZone(zone_name));
ZoneFinderPtr finder;
- if (result.code != result::NOTFOUND) {
+ if (result.code != result::NOTFOUND && result.zone_data) {
finder.reset(new InMemoryZoneFinder(*result.zone_data, getClass()));
}
- return (DataSourceClient::FindResult(result.code, finder));
+ return (DataSourceClient::FindResult(result.code, finder, result.flags));
}
const ZoneData*
@@ -242,7 +242,12 @@ InMemoryClient::getIterator(const Name& name, bool separate_rrs) const {
const ZoneTable* zone_table = ztable_segment_->getHeader().getTable();
const ZoneTable::FindResult result(zone_table->findZone(name));
if (result.code != result::SUCCESS) {
- isc_throw(DataSourceError, "No such zone: " + name.toText());
+ isc_throw(NoSuchZone, "no such zone for in-memory iterator: "
+ << name.toText());
+ }
+ if (!result.zone_data) {
+ isc_throw(EmptyZone, "empty zone for in-memory iterator: "
+ << name.toText());
}
return (ZoneIteratorPtr(new MemoryIterator(
diff --git a/src/lib/datasrc/memory/memory_messages.mes b/src/lib/datasrc/memory/memory_messages.mes
index cf51706..37539e7 100644
--- a/src/lib/datasrc/memory/memory_messages.mes
+++ b/src/lib/datasrc/memory/memory_messages.mes
@@ -92,6 +92,12 @@ tried).
Debug information. A specific type RRset is requested at a zone origin
of an in-memory zone and it is found.
+% DATASRC_MEMORY_MEM_ADD_EMPTY_ZONE adding an empty zone '%1/%2'
+Debug information. An "empty" zone is being added into the in-memory
+data source. This is conceptual data indicating the state where the
+zone exists but its content isn't available. That would be the case,
+for example, a broken zone specified in the configuration.
+
% DATASRC_MEMORY_MEM_ADD_RRSET adding RRset '%1/%2' into zone '%3'
Debug information. An RRset is being added to the in-memory data source.
diff --git a/src/lib/datasrc/memory/rdataset.cc b/src/lib/datasrc/memory/rdataset.cc
index 7f37f51..b60316c 100644
--- a/src/lib/datasrc/memory/rdataset.cc
+++ b/src/lib/datasrc/memory/rdataset.cc
@@ -12,6 +12,9 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include "rdataset.h"
+#include "rdata_serialization.h"
+
#include <exceptions/exceptions.h>
#include <dns/rdata.h>
@@ -19,11 +22,10 @@
#include <dns/rrclass.h>
#include <dns/rrtype.h>
#include <dns/rrset.h>
-
-#include "rdataset.h"
-#include "rdata_serialization.h"
+#include <util/buffer.h>
#include <boost/static_assert.hpp>
+#include <boost/bind.hpp>
#include <stdint.h>
#include <algorithm>
@@ -74,12 +76,16 @@ lowestTTL(const RdataSet* rdataset, ConstRRsetPtr& rrset,
sig_rrset->getTTL());
}
}
-}
-RdataSet*
-RdataSet::create(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
- ConstRRsetPtr rrset, ConstRRsetPtr sig_rrset,
- const RdataSet* old_rdataset)
+// Do some sanity checks on params of create and substract. Return the
+// target rrclass and rrtype (as they are produced as side effect of the
+// checks).
+//
+// The only reason for this function is to reuse common code of the
+// methods.
+std::pair<RRClass, RRType>
+sanityChecks(const ConstRRsetPtr& rrset, const ConstRRsetPtr &sig_rrset,
+ const RdataSet *old_rdataset)
{
// Check basic validity
if (!rrset && !sig_rrset) {
@@ -91,6 +97,9 @@ RdataSet::create(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
if (sig_rrset && sig_rrset->getRdataCount() == 0) {
isc_throw(BadValue, "Empty SIG RRset");
}
+ if (sig_rrset && sig_rrset->getType() != RRType::RRSIG()) {
+ isc_throw(BadValue, "SIG RRset doesn't have type RRSIG");
+ }
if (rrset && sig_rrset && rrset->getClass() != sig_rrset->getClass()) {
isc_throw(BadValue, "RR class doesn't match between RRset and RRSIG");
}
@@ -98,9 +107,45 @@ RdataSet::create(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
const RRClass rrclass = rrset ? rrset->getClass() : sig_rrset->getClass();
const RRType rrtype = rrset ? rrset->getType() :
getCoveredType(sig_rrset->getRdataIterator()->getCurrent());
+
if (old_rdataset && old_rdataset->type != rrtype) {
- isc_throw(BadValue, "RR type doesn't match for merging RdataSet");
+ isc_throw(BadValue, "RR type doesn't match between RdataSets");
}
+
+ return (std::pair<RRClass, RRType>(rrclass, rrtype));
+}
+
+} // Anonymous namespace
+
+RdataSet*
+RdataSet::packSet(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
+ size_t rdata_count, size_t rrsig_count, const RRType& rrtype,
+ const RRTTL& rrttl)
+{
+ const size_t ext_rrsig_count_len =
+ rrsig_count >= MANY_RRSIG_COUNT ? sizeof(uint16_t) : 0;
+ const size_t data_len = encoder.getStorageLength();
+ void* p = mem_sgmt.allocate(sizeof(RdataSet) + ext_rrsig_count_len +
+ data_len);
+ RdataSet* rdataset = new(p) RdataSet(rrtype, rdata_count, rrsig_count,
+ rrttl);
+ if (rrsig_count >= RdataSet::MANY_RRSIG_COUNT) {
+ *rdataset->getExtSIGCountBuf() = rrsig_count;
+ }
+ encoder.encode(rdataset->getDataBuf(), data_len);
+ return (rdataset);
+}
+
+RdataSet*
+RdataSet::create(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
+ ConstRRsetPtr rrset, ConstRRsetPtr sig_rrset,
+ const RdataSet* old_rdataset)
+{
+ const std::pair<RRClass, RRType>& rrparams =
+ sanityChecks(rrset, sig_rrset, old_rdataset);
+ const RRClass& rrclass = rrparams.first;
+ const RRType& rrtype = rrparams.second;
+
const RRTTL rrttl = lowestTTL(old_rdataset, rrset, sig_rrset);
if (old_rdataset) {
encoder.start(rrclass, rrtype, old_rdataset->getDataBuf(),
@@ -148,18 +193,99 @@ RdataSet::create(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
<< MAX_RRSIG_COUNT);
}
- const size_t ext_rrsig_count_len =
- rrsig_count >= MANY_RRSIG_COUNT ? sizeof(uint16_t) : 0;
- const size_t data_len = encoder.getStorageLength();
- void* p = mem_sgmt.allocate(sizeof(RdataSet) + ext_rrsig_count_len +
- data_len);
- RdataSet* rdataset = new(p) RdataSet(rrtype, rdata_count, rrsig_count,
- rrttl);
- if (rrsig_count >= MANY_RRSIG_COUNT) {
- *rdataset->getExtSIGCountBuf() = rrsig_count;
+ return (packSet(mem_sgmt, encoder, rdata_count, rrsig_count, rrtype,
+ rrttl));
+}
+
+namespace {
+
+void writeName(util::OutputBuffer* buffer, const LabelSequence& name,
+ RdataNameAttributes)
+{
+ size_t len;
+ const uint8_t* data = name.getData(&len);
+ buffer->writeData(data, len);
+}
+
+void writeData(util::OutputBuffer* buffer, const void* data, size_t len) {
+ buffer->writeData(data, len);
+}
+
+size_t subtractIterate(const dns::ConstRRsetPtr& subtract,
+ const RRClass& rrclass, const RRType& rrtype,
+ boost::function<bool ()> iterator,
+ boost::function<void (const Rdata& rdata)> inserter,
+ util::OutputBuffer& buffer)
+{
+ size_t count = 0;
+ while (iterator()) {
+ util::InputBuffer input(buffer.getData(), buffer.getLength());
+ const RdataPtr& rdata(createRdata(rrtype, rrclass, input,
+ buffer.getLength()));
+ buffer.clear();
+
+ bool insert = true;
+ if (subtract) {
+ for (RdataIteratorPtr it = subtract->getRdataIterator();
+ !it->isLast(); it->next()) {
+ if (rdata->compare(it->getCurrent()) == 0) {
+ insert = false;
+ break;
+ }
+ }
+ }
+
+ if (insert) {
+ inserter(*rdata);
+ ++count;
+ }
}
- encoder.encode(rdataset->getDataBuf(), data_len);
- return (rdataset);
+ return (count);
+}
+
+} // Anonymous namespace
+
+RdataSet*
+RdataSet::subtract(util::MemorySegment& mem_sgmt, RdataEncoder& encoder,
+ const dns::ConstRRsetPtr& rrset,
+ const dns::ConstRRsetPtr& sig_rrset,
+ const RdataSet& old_rdataset)
+{
+ const std::pair<RRClass, RRType>& rrparams =
+ sanityChecks(rrset, sig_rrset, &old_rdataset);
+ const RRClass& rrclass = rrparams.first;
+ const RRType& rrtype = rrparams.second;
+
+ // Do the encoding
+ encoder.start(rrclass, rrtype);
+ util::OutputBuffer buffer(1024);
+ RdataReader reader(rrclass, rrtype, old_rdataset.getDataBuf(),
+ old_rdataset.getRdataCount(),
+ old_rdataset.getSigRdataCount(),
+ boost::bind(writeName, &buffer, _1, _2),
+ boost::bind(writeData, &buffer, _1, _2));
+
+ // Copy over the Rdata (except for the subtracted)
+ const size_t rdata_count =
+ subtractIterate(rrset, rrclass, rrtype,
+ boost::bind(&RdataReader::iterateRdata, &reader),
+ boost::bind(&RdataEncoder::addRdata, &encoder, _1),
+ buffer);
+ // Copy over the signatures (except for the subtracted)
+ const size_t rrsig_count =
+ subtractIterate(sig_rrset, rrclass, RRType::RRSIG(),
+ boost::bind(&RdataReader::iterateSingleSig, &reader),
+ boost::bind(&RdataEncoder::addSIGRdata, &encoder, _1),
+ buffer);
+
+ // Note that we don't need to check for overflow, if it fitted before, it
+ // fits after removal of something too.
+
+ if (rdata_count == 0 && rrsig_count == 0) {
+ return (NULL); // It is left empty
+ }
+ return (packSet(mem_sgmt, encoder, rdata_count, rrsig_count, rrtype,
+ restoreTTL(old_rdataset.getTTLData())));
}
void
diff --git a/src/lib/datasrc/memory/rdataset.h b/src/lib/datasrc/memory/rdataset.h
index f852e12..a30ec05 100644
--- a/src/lib/datasrc/memory/rdataset.h
+++ b/src/lib/datasrc/memory/rdataset.h
@@ -176,6 +176,15 @@ public:
/// it cannot contain more than 65535 RRSIGs. If the given RRset(s) fail
/// to meet this condition, an \c RdataSetError exception will be thrown.
///
+ /// This method ensures there'll be no memory leak on exception.
+ /// But addresses allocated from \c mem_sgmt could be relocated if
+ /// \c util::MemorySegmentGrown is thrown; the caller or its upper layer
+ /// must be aware of that possibility and update any such addresses
+ /// accordingly. On successful return, this method ensures there's no
+ /// address relocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw isc::BadValue Given RRset(s) are invalid (see the description)
/// \throw RdataSetError Number of RDATAs exceed the limits
/// \throw std::bad_alloc Memory allocation fails.
@@ -198,6 +207,53 @@ public:
dns::ConstRRsetPtr sig_rrset,
const RdataSet* old_rdataset = NULL);
+ /// \brief Subtract some RDATAs and RRSIGs from an RdataSet
+ ///
+ /// Allocate and construct a new RdataSet that contains all the
+ /// data from the \c old_rdataset except for the ones in rrset
+ /// and sig_rrset.
+ ///
+ /// The interface is almost the same as with \c create, as well
+ /// as the restrictions and internal format. The most significant
+ /// difference in the interface is old_rdataset is mandatory here.
+ ///
+ /// This ignores RDATAs present in rrset and not in old_rdataset
+ /// (similarly for RRSIGs). If an RDATA in rrset and not in
+ /// old_rdataset is an error condition or needs other special
+ /// handling, it is up to the caller to check the old_rdataset
+ /// first.
+ ///
+ /// There'll be no memory leak on exception. However, the memory
+ /// allocated from the mem_sgmt may move when
+ /// \c util::MemorySegmentGrown is thrown. Note that it may happen
+ /// even when subtracting data from the old_rdataset, since a new
+ /// copy is being created.
+ ///
+ /// The old_rdataset is not destroyed and it is up to the caller to
+ /// manage its allocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
+ /// \throw isc::BadValue Given RRset(s) are invalid.
+ /// \throw std::bad_alloc Memory allocation fails.
+ ///
+ /// \param mem_sgmt A \c MemorySegment from which memory for the new
+ /// \c RdataSet is allocated.
+ /// \param encoder The RDATA encoder to encode \c rrset and \c sig_rrset
+ /// with the \c RdataSet is to be created.
+ /// \param rrset A (non-RRSIG) RRset containing the RDATA that are not
+ /// to be present in the result. Can be NULL if sig_rrset is not.
+ /// \param sig_rrset An RRSIG RRset containing the RRSIGs that are not
+ /// to be present in the result. Can be NULL if rrset is not.
+ /// \param old_rdataset The data from which to subtract.
+ ///
+ /// \return A pointer to the created \c RdataSet.
+ static RdataSet* subtract(util::MemorySegment& mem_sgmt,
+ RdataEncoder& encoder,
+ const dns::ConstRRsetPtr& rrset,
+ const dns::ConstRRsetPtr& sig_rrset,
+ const RdataSet& old_rdataset);
+
/// \brief Destruct and deallocate \c RdataSet
///
/// Note that this method needs to know the expected RR class of the
@@ -302,6 +358,12 @@ private:
// field for the real number of RRSIGs. It's 2^3 - 1 = 7.
static const size_t MANY_RRSIG_COUNT = (1 << 3) - 1;
+ // Common code for packing the result in create and subtract.
+ static RdataSet* packSet(util::MemorySegment& mem_sgmt,
+ RdataEncoder& encoder, size_t rdata_count,
+ size_t rrsig_count, const dns::RRType& rrtype,
+ const dns::RRTTL& rrttl);
+
public:
/// \brief Return the bare pointer to the next node.
///
diff --git a/src/lib/datasrc/memory/segment_object_holder.cc b/src/lib/datasrc/memory/segment_object_holder.cc
new file mode 100644
index 0000000..6d47b9d
--- /dev/null
+++ b/src/lib/datasrc/memory/segment_object_holder.cc
@@ -0,0 +1,41 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include "segment_object_holder.h"
+
+#include <boost/lexical_cast.hpp>
+
+#include <cassert>
+#include <stdint.h>
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+namespace detail {
+
+std::string
+getNextHolderName() {
+ static uint64_t index = 0;
+ ++index;
+ // in practice we should be able to assume this, uint64 is large
+ // and should not overflow
+ assert(index != 0);
+ return ("Segment object holder auto name " +
+ boost::lexical_cast<std::string>(index));
+}
+
+}
+}
+}
+}
diff --git a/src/lib/datasrc/memory/segment_object_holder.h b/src/lib/datasrc/memory/segment_object_holder.h
index 384f4ef..a716d4a 100644
--- a/src/lib/datasrc/memory/segment_object_holder.h
+++ b/src/lib/datasrc/memory/segment_object_holder.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,39 +16,99 @@
#define DATASRC_MEMORY_SEGMENT_OBJECT_HOLDER_H 1
#include <util/memory_segment.h>
+#include <string>
+#include <cassert>
namespace isc {
namespace datasrc {
namespace memory {
namespace detail {
+// Internal function to get next yet unused name of segment holder.
+// We need the names of holders to be unique per segment at any given
+// momemnt. This just keeps incrementing number after a prefix with
+// each call, it should be enough (we assert it does not wrap around,
+// but 64bits should be enough).
+//
+// Also, it is not thread safe.
+std::string
+getNextHolderName();
+
// A simple holder to create and use some objects in this implementation
// in an exception safe manner. It works like std::auto_ptr but much
// more simplified.
+//
+// Note, however, that it doesn't take the pointer to hold on construction.
+// This is because the constructor itself can throw or cause address
+// reallocation inside the memory segment. If that happens various
+// undesirable effects can happen, such as memory leak or unintentional access
+// to the pre-reallocated address. To make it safer, we use a separate
+// \c set() method, which is exception free and doesn't cause address
+// reallocation. So the typical usage is to first construct the holder
+// object, then the object to be held, immediately followed by a call to \c
+// set(). Subsequent access to the held address should be done via the \c get()
+// method. get() ensures the address is always valid in the memory segment
+// even if address reallocation happens between set() and get().
+//
// template parameter T is the type of object allocated by mem_sgmt.
// template parameter ARG_T is the type that will be passed to destroy()
// (deleter functor, etc). It must be copyable.
template <typename T, typename ARG_T>
class SegmentObjectHolder {
public:
- SegmentObjectHolder(util::MemorySegment& mem_sgmt, T* obj, ARG_T arg) :
- mem_sgmt_(mem_sgmt), obj_(obj), arg_(arg)
- {}
+ SegmentObjectHolder(util::MemorySegment& mem_sgmt, ARG_T arg) :
+ mem_sgmt_(mem_sgmt), arg_(arg),
+ holder_name_(getNextHolderName()), holding_(true)
+ {
+ if (mem_sgmt_.setNamedAddress(holder_name_.c_str(), NULL)) {
+ // OK. We've grown. The caller might need to be informed, so
+ // we throw. But then, we don't get our destructor, so we
+ // release the memory right away.
+ mem_sgmt_.clearNamedAddress(holder_name_.c_str());
+ isc_throw(isc::util::MemorySegmentGrown,
+ "Segment grown when allocating holder");
+ }
+ }
~SegmentObjectHolder() {
- if (obj_ != NULL) {
- T::destroy(mem_sgmt_, obj_, arg_);
+ if (holding_) {
+ // Use release, as it removes the stored address from segment
+ T* obj = release();
+ if (obj) { // May be NULL if set wasn't called
+ T::destroy(mem_sgmt_, obj, arg_);
+ }
+ }
+ }
+ void set(T* obj) {
+ const bool grown = mem_sgmt_.setNamedAddress(holder_name_.c_str(),
+ obj);
+ // We reserve the space in the constructor, should not grow now
+ assert(!grown);
+ }
+ T* get() {
+ if (holding_) {
+ const util::MemorySegment::NamedAddressResult result =
+ mem_sgmt_.getNamedAddress(holder_name_.c_str());
+ assert(result.first);
+ return (static_cast<T*>(result.second));
+ } else {
+ return (NULL);
}
}
- T* get() { return (obj_); }
T* release() {
- T* ret = obj_;
- obj_ = NULL;
- return (ret);
+ if (holding_) {
+ T* obj = get();
+ mem_sgmt_.clearNamedAddress(holder_name_.c_str());
+ holding_ = false;
+ return (obj);
+ } else {
+ return (NULL);
+ }
}
private:
util::MemorySegment& mem_sgmt_;
- T* obj_;
ARG_T arg_;
+ const std::string holder_name_;
+ bool holding_;
};
} // detail
diff --git a/src/lib/datasrc/memory/treenode_rrset.cc b/src/lib/datasrc/memory/treenode_rrset.cc
index c4e16a6..4734e04 100644
--- a/src/lib/datasrc/memory/treenode_rrset.cc
+++ b/src/lib/datasrc/memory/treenode_rrset.cc
@@ -67,11 +67,6 @@ TreeNodeRRset::getTTL() const {
}
void
-TreeNodeRRset::setName(const Name&) {
- isc_throw(Unexpected, "unexpected method called on TreeNodeRRset");
-}
-
-void
TreeNodeRRset::setTTL(const RRTTL&) {
isc_throw(Unexpected, "unexpected method called on TreeNodeRRset");
}
diff --git a/src/lib/datasrc/memory/treenode_rrset.h b/src/lib/datasrc/memory/treenode_rrset.h
index 295a95a..640c972 100644
--- a/src/lib/datasrc/memory/treenode_rrset.h
+++ b/src/lib/datasrc/memory/treenode_rrset.h
@@ -180,12 +180,7 @@ public:
/// \brief Specialized version of \c getTTL() for \c TreeNodeRRset.
virtual const dns::RRTTL& getTTL() const;
- /// \brief Specialized version of \c setName() for \c TreeNodeRRset.
- ///
- /// It throws \c isc::Unexpected unconditionally.
- virtual void setName(const dns::Name& name);
-
- /// \brief Specialized version of \c setName() for \c TreeNodeRRset.
+ /// \brief Specialized version of \c setTTL() for \c TreeNodeRRset.
///
/// It throws \c isc::Unexpected unconditionally.
virtual void setTTL(const dns::RRTTL& ttl);
diff --git a/src/lib/datasrc/memory/zone_data.cc b/src/lib/datasrc/memory/zone_data.cc
index d32fc87..cdcb683 100644
--- a/src/lib/datasrc/memory/zone_data.cc
+++ b/src/lib/datasrc/memory/zone_data.cc
@@ -38,6 +38,10 @@ namespace isc {
namespace datasrc {
namespace memory {
+// Definition of a class static constant. It's public and its address
+// could be needed by applications, so we need an explicit definition.
+const ZoneNode::Flags ZoneData::DNSSEC_SIGNED;
+
namespace {
void
rdataSetDeleter(RRClass rrclass, util::MemorySegment* mem_sgmt,
@@ -91,8 +95,8 @@ NSEC3Data::create(util::MemorySegment& mem_sgmt,
// (with an assertion check for that).
typedef boost::function<void(RdataSet*)> RdataSetDeleterType;
detail::SegmentObjectHolder<ZoneTree, RdataSetDeleterType> holder(
- mem_sgmt, ZoneTree::create(mem_sgmt, true),
- boost::bind(nullDeleter, _1));
+ mem_sgmt, boost::bind(nullDeleter, _1));
+ holder.set(ZoneTree::create(mem_sgmt, true));
ZoneTree* tree = holder.get();
const ZoneTree::Result result =
@@ -165,8 +169,8 @@ ZoneData::create(util::MemorySegment& mem_sgmt, const Name& zone_origin) {
// NSEC3Data::create().
typedef boost::function<void(RdataSet*)> RdataSetDeleterType;
detail::SegmentObjectHolder<ZoneTree, RdataSetDeleterType> holder(
- mem_sgmt, ZoneTree::create(mem_sgmt, true),
- boost::bind(nullDeleter, _1));
+ mem_sgmt, boost::bind(nullDeleter, _1));
+ holder.set(ZoneTree::create(mem_sgmt, true));
ZoneTree* tree = holder.get();
ZoneNode* origin_node = NULL;
@@ -179,6 +183,13 @@ ZoneData::create(util::MemorySegment& mem_sgmt, const Name& zone_origin) {
return (zone_data);
}
+ZoneData*
+ZoneData::create(util::MemorySegment& mem_sgmt) {
+ ZoneData* zone_data = create(mem_sgmt, Name::ROOT_NAME());
+ zone_data->origin_node_->setFlag(EMPTY_ZONE);
+ return (zone_data);
+}
+
void
ZoneData::destroy(util::MemorySegment& mem_sgmt, ZoneData* zone_data,
RRClass zone_class)
diff --git a/src/lib/datasrc/memory/zone_data.h b/src/lib/datasrc/memory/zone_data.h
index c6b3dcc..b4c65f7 100644
--- a/src/lib/datasrc/memory/zone_data.h
+++ b/src/lib/datasrc/memory/zone_data.h
@@ -86,6 +86,15 @@ public:
/// The NSEC3 parameters are extracted and stored within the created
/// \c NSEC3Data object.
///
+ /// This method ensures there'll be no memory leak on exception.
+ /// But addresses allocated from \c mem_sgmt could be relocated if
+ /// \c util::MemorySegmentGrown is thrown; the caller or its upper layer
+ /// must be aware of that possibility and update any such addresses
+ /// accordingly. On successful return, this method ensures there's no
+ /// address relocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw std::bad_alloc Memory allocation fails.
///
/// \param mem_sgmt A \c MemorySegment from which memory for the new
@@ -102,6 +111,15 @@ public:
/// The NSEC3 hash parameters are extracted and stored within the created
/// \c NSEC3Data object.
///
+ /// This method ensures there'll be no memory leak on exception.
+ /// But addresses allocated from \c mem_sgmt could be relocated if
+ /// \c util::MemorySegmentGrown is thrown; the caller or its upper layer
+ /// must be aware of that possibility and update any such addresses
+ /// accordingly. On successful return, this method ensures there's no
+ /// address relocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw std::bad_alloc Memory allocation fails.
///
/// \param mem_sgmt A \c MemorySegment from which memory for the new
@@ -360,21 +378,40 @@ private:
/// It never throws an exception.
ZoneData(ZoneTree* zone_tree, ZoneNode* origin_node);
- // Zone node flags.
+ // Zone node flags. When adding a new flag, it's generally advisable to
+ // keep existing values so the binary image of the data is as much
+ // backward compatible as possible. And it can be helpful in practice
+ // for file-mapped data.
private:
// Set in the origin node (which always exists at the same address)
// to indicate whether the zone is signed or not. Internal use,
// so defined as private.
static const ZoneNode::Flags DNSSEC_SIGNED = ZoneNode::FLAG_USER1;
+
public:
/// \brief Node flag indicating it is at a "wildcard level"
///
/// This means one of the node's immediate children is a wildcard.
static const ZoneNode::Flags WILDCARD_NODE = ZoneNode::FLAG_USER2;
+private:
+ // Also set in the origin node, indicating this is a special "empty zone",
+ // that could be created only by the corresponding create() method to be
+ // used for some kind of sentinel data.
+ static const ZoneNode::Flags EMPTY_ZONE = ZoneNode::FLAG_USER3;
+
public:
/// \brief Allocate and construct \c ZoneData.
///
+ /// This method ensures there'll be no memory leak on exception.
+ /// But addresses allocated from \c mem_sgmt could be relocated if
+ /// \c util::MemorySegmentGrown is thrown; the caller or its upper layer
+ /// must be aware of that possibility and update any such addresses
+ /// accordingly. On successful return, this method ensures there's no
+ /// address relocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw std::bad_alloc Memory allocation fails.
///
/// \param mem_sgmt A \c MemorySegment from which memory for the new
@@ -383,6 +420,23 @@ public:
static ZoneData* create(util::MemorySegment& mem_sgmt,
const dns::Name& zone_origin);
+ /// \brief Allocate and construct a special "empty" \c ZoneData.
+ ///
+ /// A ZoneData object created this way holds all internal integrity
+ /// that those created by the other \c create() method have, but is not
+ /// publicly associated with any actual zone data. It's intended to be
+ /// used as a kind of sentinel data to representing the concept such as
+ /// "broken zone".
+ ///
+ /// Methods calls on empty \c ZoneData object except \c destroy() and
+ /// \c isEmpty() are meaningless, while they shouldn't cause disruption.
+ /// It's caller's responsibility to use empty zone data objects in the
+ /// intended way.
+ ///
+ /// \param mem_sgmt A \c MemorySegment from which memory for the new
+ /// \c ZoneData is allocated.
+ static ZoneData* create(util::MemorySegment& mem_sgmt);
+
/// \brief Destruct and deallocate \c ZoneData.
///
/// It releases all resource allocated in the internal storage NSEC3 for
@@ -455,6 +509,13 @@ public:
/// \throw none
bool isNSEC3Signed() const { return (nsec3_data_); }
+ /// \brief Return whether or not the zone data is "empty".
+ ///
+ /// See the description of \c create() for the concept of empty zone data.
+ ///
+ /// \throw None
+ bool isEmpty() const { return (origin_node_->getFlag(EMPTY_ZONE)); }
+
/// \brief Return NSEC3Data of the zone.
///
/// This method returns non-NULL valid pointer to \c NSEC3Data object
diff --git a/src/lib/datasrc/memory/zone_data_loader.cc b/src/lib/datasrc/memory/zone_data_loader.cc
index d66fb3b..e796dd4 100644
--- a/src/lib/datasrc/memory/zone_data_loader.cc
+++ b/src/lib/datasrc/memory/zone_data_loader.cc
@@ -131,7 +131,7 @@ ZoneDataLoader::flushNodeRRsets() {
}
// Normally rrsigsets map should be empty at this point, but it's still
- // possible that an RRSIG that don't has covered RRset is added; they
+ // possible that an RRSIG that doesn't have covered RRset is added; they
// still remain in the map. We add them to the zone separately.
BOOST_FOREACH(NodeRRsetsVal val, node_rrsigsets_) {
updater_.add(ConstRRsetPtr(), val.second);
@@ -182,35 +182,47 @@ loadZoneDataInternal(util::MemorySegment& mem_sgmt,
const Name& zone_name,
boost::function<void(LoadCallback)> rrset_installer)
{
- SegmentObjectHolder<ZoneData, RRClass> holder(
- mem_sgmt, ZoneData::create(mem_sgmt, zone_name), rrclass);
-
- ZoneDataLoader loader(mem_sgmt, rrclass, zone_name, *holder.get());
- rrset_installer(boost::bind(&ZoneDataLoader::addFromLoad, &loader, _1));
- // Add any last RRsets that were left
- loader.flushNodeRRsets();
-
- const ZoneNode* origin_node = holder.get()->getOriginNode();
- const RdataSet* rdataset = origin_node->getData();
- // If the zone is NSEC3-signed, check if it has NSEC3PARAM
- if (holder.get()->isNSEC3Signed()) {
- if (RdataSet::find(rdataset, RRType::NSEC3PARAM()) == NULL) {
- LOG_WARN(logger, DATASRC_MEMORY_MEM_NO_NSEC3PARAM).
- arg(zone_name).arg(rrclass);
+ while (true) { // Try as long as it takes to load and grow the segment
+ bool created = false;
+ try {
+ SegmentObjectHolder<ZoneData, RRClass> holder(mem_sgmt, rrclass);
+ holder.set(ZoneData::create(mem_sgmt, zone_name));
+
+ // Nothing from this point on should throw MemorySegmentGrown.
+ // It is handled inside here.
+ created = true;
+
+ ZoneDataLoader loader(mem_sgmt, rrclass, zone_name, *holder.get());
+ rrset_installer(boost::bind(&ZoneDataLoader::addFromLoad, &loader,
+ _1));
+ // Add any last RRsets that were left
+ loader.flushNodeRRsets();
+
+ const ZoneNode* origin_node = holder.get()->getOriginNode();
+ const RdataSet* rdataset = origin_node->getData();
+ // If the zone is NSEC3-signed, check if it has NSEC3PARAM
+ if (holder.get()->isNSEC3Signed()) {
+ if (RdataSet::find(rdataset, RRType::NSEC3PARAM()) == NULL) {
+ LOG_WARN(logger, DATASRC_MEMORY_MEM_NO_NSEC3PARAM).
+ arg(zone_name).arg(rrclass);
+ }
+ }
+
+ RRsetCollection collection(*(holder.get()), rrclass);
+ const dns::ZoneCheckerCallbacks
+ callbacks(boost::bind(&logError, &zone_name, &rrclass, _1),
+ boost::bind(&logWarning, &zone_name, &rrclass, _1));
+ if (!dns::checkZone(zone_name, rrclass, collection, callbacks)) {
+ isc_throw(ZoneValidationError,
+ "Errors found when validating zone: "
+ << zone_name << "/" << rrclass);
+ }
+
+ return (holder.release());
+ } catch (const util::MemorySegmentGrown&) {
+ assert(!created);
}
}
-
- RRsetCollection collection(*(holder.get()), rrclass);
- const dns::ZoneCheckerCallbacks
- callbacks(boost::bind(&logError, &zone_name, &rrclass, _1),
- boost::bind(&logWarning, &zone_name, &rrclass, _1));
- if (!dns::checkZone(zone_name, rrclass, collection, callbacks)) {
- isc_throw(ZoneValidationError,
- "Errors found when validating zone: "
- << zone_name << "/" << rrclass);
- }
-
- return (holder.release());
}
// A wrapper for dns::MasterLoader used by loadZoneData() below. Essentially
@@ -256,7 +268,7 @@ loadZoneData(util::MemorySegment& mem_sgmt,
LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_MEMORY_MEM_LOAD_FROM_FILE).
arg(zone_name).arg(rrclass).arg(zone_file);
- return (loadZoneDataInternal(mem_sgmt, rrclass, zone_name,
+ return (loadZoneDataInternal(mem_sgmt, rrclass, zone_name,
boost::bind(masterLoaderWrapper,
zone_file.c_str(),
zone_name, rrclass,
diff --git a/src/lib/datasrc/memory/zone_data_loader.h b/src/lib/datasrc/memory/zone_data_loader.h
index 32ed58b..56e1ada 100644
--- a/src/lib/datasrc/memory/zone_data_loader.h
+++ b/src/lib/datasrc/memory/zone_data_loader.h
@@ -57,7 +57,7 @@ ZoneData* loadZoneData(util::MemorySegment& mem_sgmt,
/// \c iterator.
///
/// Throws \c ZoneDataUpdater::AddError if invalid or inconsistent data
-/// is present in the \c zone_file. Throws \c isc::Unexpected if empty
+/// is present in the \c iterator. Throws \c isc::Unexpected if empty
/// RRsets are passed by the zone iterator. Throws \c EmptyZone if an
/// empty zone would be created due to the \c loadZoneData().
///
diff --git a/src/lib/datasrc/memory/zone_data_updater.cc b/src/lib/datasrc/memory/zone_data_updater.cc
index 4d7e7e0..a8a88e6 100644
--- a/src/lib/datasrc/memory/zone_data_updater.cc
+++ b/src/lib/datasrc/memory/zone_data_updater.cc
@@ -49,14 +49,14 @@ ZoneDataUpdater::addWildcards(const Name& name) {
// Ensure a separate level exists for the "wildcarding"
// name, and mark the node as "wild".
ZoneNode* node;
- zone_data_.insertName(mem_sgmt_, wname.split(1), &node);
+ zone_data_->insertName(mem_sgmt_, wname.split(1), &node);
node->setFlag(ZoneData::WILDCARD_NODE);
// Ensure a separate level exists for the wildcard name.
// Note: for 'name' itself we do this later anyway, but the
// overhead should be marginal because wildcard names should
// be rare.
- zone_data_.insertName(mem_sgmt_, wname, &node);
+ zone_data_->insertName(mem_sgmt_, wname, &node);
}
}
}
@@ -210,7 +210,7 @@ ZoneDataUpdater::validate(const isc::dns::ConstRRsetPtr rrset) const {
const NSEC3Hash*
ZoneDataUpdater::getNSEC3Hash() {
if (hash_ == NULL) {
- NSEC3Data* nsec3_data = zone_data_.getNSEC3Data();
+ NSEC3Data* nsec3_data = zone_data_->getNSEC3Data();
// This should never be NULL in this codepath.
assert(nsec3_data != NULL);
@@ -231,11 +231,11 @@ ZoneDataUpdater::setupNSEC3(const ConstRRsetPtr rrset) {
dynamic_cast<const T&>(
rrset->getRdataIterator()->getCurrent());
- NSEC3Data* nsec3_data = zone_data_.getNSEC3Data();
+ NSEC3Data* nsec3_data = zone_data_->getNSEC3Data();
if (nsec3_data == NULL) {
nsec3_data = NSEC3Data::create(mem_sgmt_, zone_name_, nsec3_rdata);
- zone_data_.setNSEC3Data(nsec3_data);
- zone_data_.setSigned(true);
+ zone_data_->setNSEC3Data(nsec3_data);
+ zone_data_->setSigned(true);
} else {
const NSEC3Hash* hash = getNSEC3Hash();
if (!hash->match(nsec3_rdata)) {
@@ -247,14 +247,14 @@ ZoneDataUpdater::setupNSEC3(const ConstRRsetPtr rrset) {
}
void
-ZoneDataUpdater::addNSEC3(const Name& name, const ConstRRsetPtr rrset,
- const ConstRRsetPtr rrsig)
+ZoneDataUpdater::addNSEC3(const Name& name, const ConstRRsetPtr& rrset,
+ const ConstRRsetPtr& rrsig)
{
if (rrset) {
setupNSEC3<generic::NSEC3>(rrset);
}
- NSEC3Data* nsec3_data = zone_data_.getNSEC3Data();
+ NSEC3Data* nsec3_data = zone_data_->getNSEC3Data();
if (nsec3_data == NULL) {
// This is some tricky case: an RRSIG for NSEC3 is given without the
// covered NSEC3, and we don't even know any NSEC3 related data.
@@ -283,14 +283,14 @@ ZoneDataUpdater::addNSEC3(const Name& name, const ConstRRsetPtr rrset,
void
ZoneDataUpdater::addRdataSet(const Name& name, const RRType& rrtype,
- const ConstRRsetPtr rrset,
- const ConstRRsetPtr rrsig)
+ const ConstRRsetPtr& rrset,
+ const ConstRRsetPtr& rrsig)
{
if (rrtype == RRType::NSEC3()) {
addNSEC3(name, rrset, rrsig);
} else {
ZoneNode* node;
- zone_data_.insertName(mem_sgmt_, name, &node);
+ zone_data_->insertName(mem_sgmt_, name, &node);
RdataSet* rdataset_head = node->getData();
@@ -334,7 +334,7 @@ ZoneDataUpdater::addRdataSet(const Name& name, const RRType& rrtype,
// Ok, we just put it in.
// Convenient (and more efficient) shortcut to check RRsets at origin
- const bool is_origin = (node == zone_data_.getOriginNode());
+ const bool is_origin = (node == zone_data_->getOriginNode());
// If this RRset creates a zone cut at this node, mark the node
// indicating the need for callback in find(). Note that we do this
@@ -356,7 +356,7 @@ ZoneDataUpdater::addRdataSet(const Name& name, const RRType& rrtype,
// (conceptually "signed" is a broader notion but our
// current zone finder implementation regards "signed" as
// "NSEC signed")
- zone_data_.setSigned(true);
+ zone_data_->setSigned(true);
}
// If we are adding a new SOA at the origin, update zone's min TTL.
@@ -365,7 +365,7 @@ ZoneDataUpdater::addRdataSet(const Name& name, const RRType& rrtype,
// this should be only once in normal cases) update the TTL.
if (rrset && rrtype == RRType::SOA() && is_origin) {
// Our own validation ensures the RRset is not empty.
- zone_data_.setMinTTL(
+ zone_data_->setMinTTL(
dynamic_cast<const generic::SOA&>(
rrset->getRdataIterator()->getCurrent()).getMinimum());
}
@@ -373,6 +373,24 @@ ZoneDataUpdater::addRdataSet(const Name& name, const RRType& rrtype,
}
void
+ZoneDataUpdater::addInternal(const isc::dns::Name& name,
+ const isc::dns::RRType& rrtype,
+ const isc::dns::ConstRRsetPtr& rrset,
+ const isc::dns::ConstRRsetPtr& rrsig)
+{
+ // Add wildcards possibly contained in the owner name to the domain
+ // tree. This can only happen for the normal (non-NSEC3) tree.
+ // Note: this can throw an exception, breaking strong exception
+ // guarantee. (see also the note for the call to contextCheck()
+ // above).
+ if (rrtype != RRType::NSEC3()) {
+ addWildcards(name);
+ }
+
+ addRdataSet(name, rrtype, rrset, rrsig);
+}
+
+void
ZoneDataUpdater::add(const ConstRRsetPtr& rrset,
const ConstRRsetPtr& sig_rrset)
{
@@ -397,16 +415,22 @@ ZoneDataUpdater::add(const ConstRRsetPtr& rrset,
arg(rrset ? rrtype.toText() : "RRSIG(" + rrtype.toText() + ")").
arg(zone_name_);
- // Add wildcards possibly contained in the owner name to the domain
- // tree. This can only happen for the normal (non-NSEC3) tree.
- // Note: this can throw an exception, breaking strong exception
- // guarantee. (see also the note for the call to contextCheck()
- // above).
- if (rrtype != RRType::NSEC3()) {
- addWildcards(name);
- }
-
- addRdataSet(name, rrtype, rrset, sig_rrset);
+ // Store the address, it may change during growth and the address inside
+ // would get updated.
+ bool added = false;
+ do {
+ try {
+ addInternal(name, rrtype, rrset, sig_rrset);
+ added = true;
+ } catch (const isc::util::MemorySegmentGrown&) {
+ // The segment has grown. So, we update the base pointer (because
+ // the data may have been remapped somewhere else in the process).
+ zone_data_ =
+ static_cast<ZoneData*>(
+ mem_sgmt_.getNamedAddress("updater_zone_data").second);
+ }
+ // Retry if it didn't add due to the growth
+ } while (!added);
}
} // namespace memory
diff --git a/src/lib/datasrc/memory/zone_data_updater.h b/src/lib/datasrc/memory/zone_data_updater.h
index 8d67b95..e8826bd 100644
--- a/src/lib/datasrc/memory/zone_data_updater.h
+++ b/src/lib/datasrc/memory/zone_data_updater.h
@@ -57,14 +57,15 @@ public:
/// The constructor.
///
- /// \throw none
- ///
/// \param mem_sgmt The memory segment used for the zone data.
/// \param rrclass The RRclass of the zone data.
/// \param zone_name The Name of the zone under which records will be
/// added.
- /// \param zone_data The ZoneData object which is populated with
- /// record data.
+ /// \param zone_data The ZoneData object which is populated with
+ /// record data.
+ /// \throw InvalidOperation if there's already a zone data updater
+ /// on the given memory segment. Currently, at most one zone data
+ /// updater may exist on the same memory segment.
ZoneDataUpdater(util::MemorySegment& mem_sgmt,
isc::dns::RRClass rrclass,
const isc::dns::Name& zone_name,
@@ -72,12 +73,25 @@ public:
mem_sgmt_(mem_sgmt),
rrclass_(rrclass),
zone_name_(zone_name),
- zone_data_(zone_data),
- hash_(NULL)
- {}
+ hash_(NULL),
+ zone_data_(&zone_data)
+ {
+ if (mem_sgmt_.getNamedAddress("updater_zone_data").first) {
+ isc_throw(isc::InvalidOperation, "A ZoneDataUpdater already exists"
+ " on this memory segment. Destroy it first.");
+ }
+ if (mem_sgmt_.setNamedAddress("updater_zone_data", zone_data_)) {
+ // It might have relocated during the set
+ zone_data_ =
+ static_cast<ZoneData*>(mem_sgmt_.getNamedAddress(
+ "updater_zone_data").second);
+ }
+ assert(zone_data_);
+ }
/// The destructor.
~ZoneDataUpdater() {
+ mem_sgmt_.clearNamedAddress("updater_zone_data");
delete hash_;
}
@@ -159,6 +173,11 @@ private:
// contained in 'name' (e.g., '*.foo.example' in 'bar.*.foo.example').
void addWildcards(const isc::dns::Name& name);
+ void addInternal(const isc::dns::Name& name,
+ const isc::dns::RRType& rrtype,
+ const isc::dns::ConstRRsetPtr& rrset,
+ const isc::dns::ConstRRsetPtr& rrsig);
+
// Does some checks in context of the data that are already in the
// zone. Currently checks for forbidden combinations of RRsets in
// the same domain (CNAME+anything, DNAME+NS). If such condition is
@@ -175,19 +194,19 @@ private:
template <typename T>
void setupNSEC3(const isc::dns::ConstRRsetPtr rrset);
void addNSEC3(const isc::dns::Name& name,
- const isc::dns::ConstRRsetPtr rrset,
- const isc::dns::ConstRRsetPtr rrsig);
+ const isc::dns::ConstRRsetPtr& rrset,
+ const isc::dns::ConstRRsetPtr& rrsig);
void addRdataSet(const isc::dns::Name& name,
const isc::dns::RRType& rrtype,
- const isc::dns::ConstRRsetPtr rrset,
- const isc::dns::ConstRRsetPtr rrsig);
+ const isc::dns::ConstRRsetPtr& rrset,
+ const isc::dns::ConstRRsetPtr& rrsig);
util::MemorySegment& mem_sgmt_;
const isc::dns::RRClass rrclass_;
const isc::dns::Name& zone_name_;
- ZoneData& zone_data_;
RdataEncoder encoder_;
const isc::dns::NSEC3Hash* hash_;
+ ZoneData* zone_data_;
};
} // namespace memory
diff --git a/src/lib/datasrc/memory/zone_finder.cc b/src/lib/datasrc/memory/zone_finder.cc
index 3f61b89..a6c601b 100644
--- a/src/lib/datasrc/memory/zone_finder.cc
+++ b/src/lib/datasrc/memory/zone_finder.cc
@@ -772,7 +772,7 @@ InMemoryZoneFinder::findAll(const isc::dns::Name& name,
// the case of CNAME can be eliminated (these should be guaranteed at the load
// or update time, but even if they miss a corner case and allows a CNAME to
// be added at origin, the zone is broken anyway, so we'd just let this
-// method return garbage, too). As a result, there can be only too cases
+// method return garbage, too). As a result, there can be only two cases
// for the result codes: SUCCESS if the requested type of RR exists; NXRRSET
// otherwise. Due to its simplicity we implement it separately, rather than
// sharing the code with findInternal.
diff --git a/src/lib/datasrc/memory/zone_table.cc b/src/lib/datasrc/memory/zone_table.cc
index 77071f4..454a0aa 100644
--- a/src/lib/datasrc/memory/zone_table.cc
+++ b/src/lib/datasrc/memory/zone_table.cc
@@ -18,6 +18,8 @@
#include <datasrc/memory/segment_object_holder.h>
#include <datasrc/memory/logger.h>
+#include <exceptions/exceptions.h>
+
#include <util/memory_segment.h>
#include <dns/name.h>
@@ -40,7 +42,10 @@ void
deleteZoneData(util::MemorySegment* mem_sgmt, ZoneData* zone_data,
RRClass rrclass)
{
- if (zone_data != NULL) {
+ // We shouldn't delete empty zone data here; the only empty zone
+ // that can be passed here is the placeholder for broken zones maintained
+ // in the zone table. It will stay there until the table is destroyed.
+ if (zone_data && !zone_data->isEmpty()) {
ZoneData::destroy(*mem_sgmt, zone_data, rrclass);
}
}
@@ -49,36 +54,63 @@ typedef boost::function<void(ZoneData*)> ZoneDataDeleterType;
ZoneTable*
ZoneTable::create(util::MemorySegment& mem_sgmt, const RRClass& zone_class) {
- SegmentObjectHolder<ZoneTableTree, ZoneDataDeleterType> holder(
- mem_sgmt, ZoneTableTree::create(mem_sgmt),
- boost::bind(deleteZoneData, &mem_sgmt, _1, zone_class));
+ // Create a placeholder "null" zone data
+ SegmentObjectHolder<ZoneData, RRClass> zdholder(mem_sgmt, zone_class);
+ zdholder.set(ZoneData::create(mem_sgmt));
+
+ // create and setup the tree for the table.
+ SegmentObjectHolder<ZoneTableTree, ZoneDataDeleterType> tree_holder(
+ mem_sgmt, boost::bind(deleteZoneData, &mem_sgmt, _1, zone_class));
+ tree_holder.set(ZoneTableTree::create(mem_sgmt));
void* p = mem_sgmt.allocate(sizeof(ZoneTable));
- ZoneTable* zone_table = new(p) ZoneTable(zone_class, holder.get());
- holder.release();
+
+ // Build zone table with the created objects. Its constructor doesn't
+ // throw, so we can release them from the holder at this point.
+ ZoneTable* zone_table = new(p) ZoneTable(zone_class, tree_holder.release(),
+ zdholder.release());
return (zone_table);
}
void
-ZoneTable::destroy(util::MemorySegment& mem_sgmt, ZoneTable* ztable)
+ZoneTable::destroy(util::MemorySegment& mem_sgmt, ZoneTable* ztable, int)
{
ZoneTableTree::destroy(mem_sgmt, ztable->zones_.get(),
boost::bind(deleteZoneData, &mem_sgmt, _1,
ztable->rrclass_));
+ ZoneData::destroy(mem_sgmt, ztable->null_zone_data_.get(),
+ ztable->rrclass_);
mem_sgmt.deallocate(ztable, sizeof(ZoneTable));
}
ZoneTable::AddResult
-ZoneTable::addZone(util::MemorySegment& mem_sgmt, RRClass zone_class,
+ZoneTable::addZone(util::MemorySegment& mem_sgmt,
const Name& zone_name, ZoneData* content)
{
LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_MEMORY_MEM_ADD_ZONE).
arg(zone_name).arg(rrclass_);
- if (content == NULL) {
- isc_throw(isc::BadValue, "Zone content must not be NULL");
+ if (!content || content->isEmpty()) {
+ isc_throw(InvalidParameter,
+ (content ? "empty data" : "NULL") <<
+ " is passed to Zone::addZone");
}
- SegmentObjectHolder<ZoneData, RRClass> holder(mem_sgmt, content,
- zone_class);
+
+ return (addZoneInternal(mem_sgmt, zone_name, content));
+}
+
+ZoneTable::AddResult
+ZoneTable::addEmptyZone(util::MemorySegment& mem_sgmt, const Name& zone_name) {
+ LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_MEMORY_MEM_ADD_EMPTY_ZONE).
+ arg(zone_name).arg(rrclass_);
+
+ return (addZoneInternal(mem_sgmt, zone_name, null_zone_data_.get()));
+}
+
+ZoneTable::AddResult
+ZoneTable::addZoneInternal(util::MemorySegment& mem_sgmt,
+ const dns::Name& zone_name,
+ ZoneData* content)
+{
// Get the node where we put the zone
ZoneTableNode* node(NULL);
switch (zones_->insert(mem_sgmt, zone_name, &node)) {
@@ -93,10 +125,9 @@ ZoneTable::addZone(util::MemorySegment& mem_sgmt, RRClass zone_class,
// Can Not Happen
assert(node != NULL);
- // We can release now, setData never throws
- ZoneData* old = node->setData(holder.release());
+ ZoneData* old = node->setData(content);
if (old != NULL) {
- return (AddResult(result::EXIST, old));
+ return (AddResult(result::EXIST, old->isEmpty() ? NULL : old));
} else {
++zone_count_;
return (AddResult(result::SUCCESS, NULL));
@@ -126,10 +157,17 @@ ZoneTable::findZone(const Name& name) const {
return (FindResult(result::NOTFOUND, NULL));
}
- // Can Not Happen (remember, NOTFOUND is handled)
+ // Can Not Happen (remember, NOTFOUND is handled). node should also have
+ // data because the tree is constructed in the way empty nodes would
+ // be "invisible" for find().
assert(node != NULL);
- return (FindResult(my_result, node->getData()));
+ const ZoneData* zone_data = node->getData();
+ assert(zone_data);
+ const result::ResultFlags flags =
+ zone_data->isEmpty() ? result::ZONE_EMPTY : result::FLAGS_DEFAULT;
+ return (FindResult(my_result, zone_data->isEmpty() ? NULL : zone_data,
+ flags));
}
} // end of namespace memory
diff --git a/src/lib/datasrc/memory/zone_table.h b/src/lib/datasrc/memory/zone_table.h
index 7147fde..dd1c721 100644
--- a/src/lib/datasrc/memory/zone_table.h
+++ b/src/lib/datasrc/memory/zone_table.h
@@ -84,12 +84,16 @@ public:
};
/// \brief Result data of findZone() method.
+ ///
+ /// See \c findZone() about the semantics of the members.
struct FindResult {
FindResult(result::Result param_code,
- const ZoneData* param_zone_data) :
- code(param_code), zone_data(param_zone_data)
+ const ZoneData* param_zone_data,
+ result::ResultFlags param_flags = result::FLAGS_DEFAULT) :
+ code(param_code), flags(param_flags), zone_data(param_zone_data)
{}
const result::Result code;
+ const result::ResultFlags flags;
const ZoneData* const zone_data;
};
@@ -99,13 +103,13 @@ private:
/// An object of this class is always expected to be created by the
/// allocator (\c create()), so the constructor is hidden as private.
///
- /// This constructor internally involves resource allocation, and if
- /// it fails, a corresponding standard exception will be thrown.
- /// It never throws an exception otherwise.
- ZoneTable(const dns::RRClass& rrclass, ZoneTableTree* zones) :
+ /// This constructor never throws.
+ ZoneTable(const dns::RRClass& rrclass, ZoneTableTree* zones,
+ ZoneData* null_zone_data) :
rrclass_(rrclass),
zone_count_(0),
- zones_(zones)
+ zones_(zones),
+ null_zone_data_(null_zone_data)
{}
public:
@@ -115,6 +119,15 @@ public:
/// from the given memory segment, constructs the object, and returns
/// a pointer to it.
///
+ /// This method ensures there'll be no memory leak on exception.
+ /// But addresses allocated from \c mem_sgmt could be relocated if
+ /// \c util::MemorySegmentGrown is thrown; the caller or its upper layer
+ /// must be aware of that possibility and update any such addresses
+ /// accordingly. On successful return, this method ensures there's no
+ /// address relocation.
+ ///
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw std::bad_alloc Memory allocation fails.
///
/// \param mem_sgmt A \c MemorySegment from which memory for the new
@@ -136,40 +149,83 @@ public:
/// \param ztable A non NULL pointer to a valid \c ZoneTable object
/// that was originally created by the \c create() method (the behavior
/// is undefined if this condition isn't met).
- static void destroy(util::MemorySegment& mem_sgmt, ZoneTable* ztable);
+ /// \param unused Ununsed parameter, provided so it's compatible to
+ /// SegmentObjectHolder.
+ static void destroy(util::MemorySegment& mem_sgmt, ZoneTable* ztable,
+ int unused = 0);
/// \brief Return the number of zones contained in the zone table.
///
/// \throw None.
size_t getZoneCount() const { return (zone_count_); }
- /// Add a new zone to the \c ZoneTable.
+ /// \brief Add a new zone to the \c ZoneTable.
///
/// This method adds a given zone data to the internal table.
///
+ /// On successful completion (i.e., the method returns without an
+ /// exception), the ownership of \c content will be transferred to
+ /// the \c ZoneTable: the caller should not use the \c content hereafter;
+ /// the \c ZoneTable will be responsible to destroy it when the table
+ /// itself is destroyed.
+ ///
+ /// If this method throws, the caller is responsible to take care of
+ /// the passed \c content, whether to destroy it or use for different
+ /// purposes. Note that addresses allocated from \c mem_sgmt could be
+ /// relocated if \c util::MemorySegmentGrown is thrown; the caller or its
+ /// upper layer must be aware of that possibility and update any such
+ /// addresses accordingly. This applies to \c content, as it's expected
+ /// to be created using \c mem_sgmt.
+ ///
+ /// On successful return, this method ensures there's no address
+ /// relocation.
+ ///
+ /// \throw InvalidParameter content is NULL or empty.
+ /// \throw util::MemorySegmentGrown The memory segment has grown, possibly
+ /// relocating data.
/// \throw std::bad_alloc Internal resource allocation fails.
///
/// \param mem_sgmt The \c MemorySegment to allocate zone data to be
/// created. It must be the same segment that was used to create
/// the zone table at the time of create().
/// \param zone_name The name of the zone to be added.
- /// \param zone_class The RR class of the zone. It must be the RR class
- /// that is supposed to be associated to the zone table.
/// \param content This one should hold the zone content (the ZoneData).
- /// The ownership is passed onto the zone table. Must not be null.
- /// Must correspond to the name and class and must be allocated from
- /// mem_sgmt.
+ /// The ownership is passed onto the zone table. Must not be null or
+ /// empty. Must correspond to the name and class and must be allocated
+ /// from mem_sgmt.
/// \return \c result::SUCCESS If the zone is successfully
/// added to the zone table.
/// \return \c result::EXIST The zone table already contained
/// zone of the same origin. The old data is replaced and returned
- /// inside the result.
+ /// inside the result unless it's empty; if the zone was previously
+ /// added by \c addEmptyZone(), the data returned is NULL.
AddResult addZone(util::MemorySegment& mem_sgmt,
- dns::RRClass zone_class,
const dns::Name& zone_name,
ZoneData* content);
- /// Find a zone that best matches the given name in the \c ZoneTable.
+ /// \brief Add an empty zone to the \c ZoneTable.
+ ///
+ /// This method is similar to \c addZone(), but adds a conceptual "empty"
+ /// zone of the given zone name to the table. The added empty zone
+ /// affects subsequent calls to \c addZone() (and \c addEmptyZone() itself)
+ /// and \c findZone() as described for these methods.
+ ///
+ /// The intended meaning of an empty zone in the table is that the zone
+ /// is somehow broken, such as configured to be loaded but loading failed.
+ /// But this class is not aware of such interpretation; it's up to the
+ /// user of the class how to use the concept of empty zones.
+ ///
+ /// It returns an \c AddResult object as described for \c addZone().
+ ///
+ /// The same notes on exception safety as that for \c addZone() applies.
+ ///
+ /// \param mem_sgmt Same as addZone().
+ /// \param zone_name Same as addZone().
+ AddResult addEmptyZone(util::MemorySegment& mem_sgmt,
+ const dns::Name& zone_name);
+
+ /// \brief Find a zone that best matches the given name in the
+ /// \c ZoneTable.
///
/// It searches the internal storage for a zone that gives the
/// longest match against \c name, and returns the result in the
@@ -180,8 +236,11 @@ public:
/// - \c result::PARTIALMATCH: A zone whose origin is a
/// super domain of \c name is found (but there is no exact match)
/// - \c result::NOTFOUND: For all other cases.
- /// - \c zone_data: corresponding zone data of the found zone; NULL if
- /// no matching zone is found.
+ /// - \c flags If the zone is empty (added by \c addEmptyZone()),
+ /// result::ZONE_EMPTY is set.
+ /// - \c zone_data: corresponding zone data of the found zone if found and
+ /// non empty; NULL if no matching zone is found or the found zone is
+ /// empty.
///
/// \throw none
///
@@ -193,6 +252,18 @@ private:
const dns::RRClass rrclass_;
size_t zone_count_;
boost::interprocess::offset_ptr<ZoneTableTree> zones_;
+
+ // this is a shared placeholder for broken zones
+ boost::interprocess::offset_ptr<ZoneData> null_zone_data_;
+
+ // Common routine for addZone and addEmptyZone. This method can throw
+ // util::MemorySegmentGrown, in which case addresses from mem_sgmt
+ // can be relocated. The caller is responsible for destroying content
+ // on exception, if it needs to be destroyed. On successful return it
+ // ensures there's been no address relocation.
+ AddResult addZoneInternal(util::MemorySegment& mem_sgmt,
+ const dns::Name& zone_name,
+ ZoneData* content);
};
}
}
diff --git a/src/lib/datasrc/memory/zone_table_segment.cc b/src/lib/datasrc/memory/zone_table_segment.cc
index 1253102..2e1a1dc 100644
--- a/src/lib/datasrc/memory/zone_table_segment.cc
+++ b/src/lib/datasrc/memory/zone_table_segment.cc
@@ -12,8 +12,14 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include "config.h"
+
#include <datasrc/memory/zone_table_segment.h>
#include <datasrc/memory/zone_table_segment_local.h>
+#ifdef USE_SHARED_MEMORY
+#include <datasrc/memory/zone_table_segment_mapped.h>
+#endif
+#include <datasrc/memory/zone_writer.h>
#include <string>
@@ -30,6 +36,10 @@ ZoneTableSegment::create(const RRClass& rrclass, const std::string& type) {
// Until that it becomes a real issue we won't be too smart.
if (type == "local") {
return (new ZoneTableSegmentLocal(rrclass));
+#ifdef USE_SHARED_MEMORY
+ } else if (type == "mapped") {
+ return (new ZoneTableSegmentMapped(rrclass));
+#endif
}
isc_throw(UnknownSegmentType, "Zone table segment type not supported: "
<< type);
diff --git a/src/lib/datasrc/memory/zone_table_segment.h b/src/lib/datasrc/memory/zone_table_segment.h
index ade1221..0f1942e 100644
--- a/src/lib/datasrc/memory/zone_table_segment.h
+++ b/src/lib/datasrc/memory/zone_table_segment.h
@@ -40,8 +40,8 @@ namespace datasrc {
namespace memory {
class ZoneWriter;
-/// \brief Exception thrown when unknown or unsupported type of zone table
-/// segment is specified.
+/// \brief Exception thrown when unknown or unsupported type of
+/// ZoneTableSegment is asked to be created.
class UnknownSegmentType : public Exception {
public:
UnknownSegmentType(const char* file, size_t line, const char* what) :
@@ -49,12 +49,36 @@ public:
{}
};
+/// \brief Exception thrown when a \c reset() on a \c ZoneTableSegment
+/// fails (due to various reasons). When this exception is thrown, a
+/// strong exception safety guarantee is provided, and the
+/// \c ZoneTableSegment is usable as before.
+class ResetFailed : public isc::Exception {
+public:
+ ResetFailed(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what)
+ {}
+};
+
+/// \brief Exception thrown when a \c reset() on a \c ZoneTableSegment
+/// fails (due to various reasons), and it was not able to preserve the
+/// state of the \c ZoneTableSegment. When this exception is thrown,
+/// only basic exception safety guarantee is provided and the
+/// \c ZoneTableSegment must be expected as cleared.
+class ResetFailedAndSegmentCleared : public isc::Exception {
+public:
+ ResetFailedAndSegmentCleared(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what)
+ {}
+};
+
/// \brief Memory-management independent entry point that contains a
/// pointer to a zone table in memory.
///
-/// An instance of this type lives inside a ZoneTableSegment
-/// implementation. It contains an offset pointer to the zone table (a
-/// map from domain names to zone locators) in memory.
+/// An instance of this type lives inside a \c ZoneTableSegment
+/// implementation. It contains an offset pointer to the \c ZoneTable (a
+/// map from domain names to zone locators) in the \c ZoneTableSegment.
struct ZoneTableHeader {
public:
ZoneTableHeader(ZoneTable* zone_table) :
@@ -74,14 +98,19 @@ private:
boost::interprocess::offset_ptr<ZoneTable> table_;
};
-/// \brief Manages a ZoneTableHeader, an entry point into a table of
+/// \brief Manages a \c ZoneTableHeader, an entry point into a table of
/// zones
///
/// This class specifies an interface for derived implementations which
-/// return a pointer to an object of type ZoneTableHeader, an entry
+/// return a pointer to an object of type \c ZoneTableHeader, an entry
/// point into a table of zones regardless of the underlying memory
-/// management implementation. Derived classes would implement the
-/// interface for specific memory-implementation behavior.
+/// management implementation. Derived classes implement the interface
+/// for the specific memory-implementation behavior.
+///
+/// Note: At some point in the future, methods such as \c reset(),
+/// \c clear(), \c getHeader(), \c isWritable(), \c isUsable() may
+/// become non-virtual methods. Such a change should not affect any code
+/// that uses this class, but please be aware of such plans.
class ZoneTableSegment {
protected:
/// \brief Protected constructor
@@ -89,24 +118,67 @@ protected:
/// An instance implementing this interface is expected to be
/// created by the factory method (\c create()), so this constructor
/// is protected.
- ZoneTableSegment(isc::dns::RRClass)
+ ZoneTableSegment(const isc::dns::RRClass&)
{}
public:
/// \brief Destructor
virtual ~ZoneTableSegment() {}
- /// \brief Return the ZoneTableHeader for the zone table segment.
+ /// \brief Return a string name for the \c ZoneTableSegment
+ /// implementation.
+ ///
+ /// Implementations of this method should ensure that the returned
+ /// string is identical to the corresponding string passed to
+ /// \c ZoneTableSegment::create() for that implementation.
+ ///
+ /// \throw None This method's implementations must be
+ /// exception-free.
+ virtual const std::string& getImplType() const = 0;
+
+ /// \brief Return the \c ZoneTableHeader for the zone table segment.
+ ///
+ /// As long as \c isUsable() returns true, this method must always
+ /// succeed without throwing an exception. If \c isUsable() returns
+ /// false, a derived class implementation can throw
+ /// \c isc::InvalidOperation depending on its implementation
+ /// details. Applications are generally expected to call this
+ /// method only when \c isUsable() returns true (either by making
+ /// sure explicitly or by some other indirect means).
+ ///
+ /// \throw isc::InvalidOperation may be thrown by some
+ /// implementations if this method is called without calling
+ /// \c reset() successfully first.
virtual ZoneTableHeader& getHeader() = 0;
- /// \brief const version of \c getHeader().
+ /// \brief \c const version of \c getHeader().
+ ///
+ /// See the non- \c const version for documentation.
virtual const ZoneTableHeader& getHeader() const = 0;
/// \brief Return the MemorySegment for the zone table segment.
+ ///
+ /// \throw isc::InvalidOperation may be thrown by some
+ /// implementations if this method is called without calling
+ /// \c reset() successfully first.
virtual isc::util::MemorySegment& getMemorySegment() = 0;
- /// \brief Create an instance depending on the memory segment model
+ /// \brief Return true if the segment is writable.
///
- /// This is a factory method to create a derived ZoneTableSegment
+ /// The user of the zone table segment will load or update zones
+ /// into the segment only for writable ones. The precise definition
+ /// of "writability" differs in different derived classes (see
+ /// derived class documentation). In general, however, the user
+ /// should only rely on this interface rather than assume a specific
+ /// definition for a specific type of segment.
+ ///
+ /// \throw None This method's implementations must be
+ /// exception-free.
+ virtual bool isWritable() const = 0;
+
+ /// \brief Create an instance depending on the requested memory
+ /// segment implementation type.
+ ///
+ /// This is a factory method to create a derived \c ZoneTableSegment
/// object based on the \c config passed. The method returns a
/// dynamically-allocated object. The caller is responsible for
/// destroying it with \c ZoneTableSegment::destroy().
@@ -115,33 +187,156 @@ public:
/// \c config is not known or not supported in this implementation.
///
/// \param rrclass The RR class of the zones to be maintained in the table.
- /// \param type The memory segment type used for the zone table segment.
- /// \return Returns a ZoneTableSegment object of the specified type.
+ /// \param type The memory segment type to be used.
+ /// \return Returns a \c ZoneTableSegment object of the specified type.
static ZoneTableSegment* create(const isc::dns::RRClass& rrclass,
const std::string& type);
- /// \brief Destroy a ZoneTableSegment
+ /// \brief Destroy a \c ZoneTableSegment
///
- /// This method destroys the passed ZoneTableSegment. It must be
- /// passed a segment previously created by \c ZoneTableSegment::create().
+ /// This method destroys the passed \c ZoneTableSegment. It must be
+ /// passed a segment previously created by
+ /// \c ZoneTableSegment::create().
///
/// \param segment The segment to destroy.
static void destroy(ZoneTableSegment* segment);
- /// \brief Create a zone write corresponding to this segment
+ /// \brief The mode using which to create a MemorySegment.
+ ///
+ /// Here, a \c MemorySegment (see its class documentation) is an
+ /// interface to a storage area, and provides operations to allocate
+ /// and deallocate from that storage area, and also to look up
+ /// addresses in that area. The storage area can be a buffer in
+ /// memory, a file on disk, or some kind of shared memory depending
+ /// on the \c MemorySegment implementation being used. In every
+ /// case in the documentation below, when we mention \c
+ /// MemorySegment, we mean both the \c MemorySegment object which
+ /// interfaces to the storage area and the contents of the
+ /// associated storage area.
+ ///
+ /// - CREATE: If the \c MemorySegment's storage area doesn't exist,
+ /// create it. If it exists, overwrite it with a new
+ /// storage area (which does not remember old data). In
+ /// both cases, create a \c MemorySegment for it in
+ /// read+write mode.
+ ///
+ /// - READ_WRITE: If the \c MemorySegment's storage area doesn't
+ /// exist, create it. If it exists, use the existing
+ /// storage area as-is (keeping the existing data
+ /// intact). In both cases, create a \c MemorySegment
+ /// for it in read+write mode.
+ ///
+ /// - READ_ONLY: If the \c MemorySegment's storage area doesn't
+ /// exist, throw an exception. If it exists, create a
+ /// \c MemorySegment for it in read-only mode.
+ enum MemorySegmentOpenMode {
+ CREATE,
+ READ_WRITE,
+ READ_ONLY
+ };
+
+ /// \brief Close the current \c MemorySegment (if open) and open the
+ /// requested one.
+ ///
+ /// When we talk about "opening" a \c MemorySegment, it means to
+ /// construct a usable \c MemorySegment object that interfaces to
+ /// the actual memory storage area. "Closing" is the opposite
+ /// operation of opening.
+ ///
+ /// In case opening the new \c MemorySegment fails for some reason,
+ /// one of the following documented (further below) exceptions may
+ /// be thrown. In case failures occur, implementations of this
+ /// method must strictly provide the associated behavior as follows
+ /// and in the exception documentation below. Code that uses
+ /// \c ZoneTableSegment would depend on such assurances.
+ ///
+ /// First, in case a \c ZoneTableSegment was reset successfully
+ /// before and is currently usable (\c isUsable() returns true), and
+ /// an invalid configuration is passed in \c params to \c reset(),
+ /// the isc::InvalidParameter exception must be thrown. In this
+ /// case, a strong exception safety guarantee must be provided, and
+ /// the \c ZoneTableSegment must be usable as before.
+ ///
+ /// In case a \c ZoneTableSegment was reset successfully before and
+ /// is currently usable (\c isUsable() returns true), and the attempt
+ /// to reset to a different \c MemorySegment storage area fails,
+ /// the \c ResetFailed exception must be thrown. In this
+ /// case, a strong exception safety guarantee must be provided, and
+ /// the \c ZoneTableSegment must be usable as before.
+ ///
+ /// In case a \c ZoneTableSegment was reset successfully before and
+ /// is currently usable (\c isUsable() returns true), and the attempt
+ /// to reset to the same \c MemorySegment storage area fails, the
+ /// \c ResetFailedAndSegmentCleared exception must be thrown. In
+ /// this case, only basic exception safety guarantee is provided and
+ /// the \c ZoneTableSegment must be expected as cleared.
+ ///
+ /// In case a \c ZoneTableSegment was not reset successfully before
+ /// and is currently not usable (\c isUsable() returns false), and
+ /// the attempt to reset fails, the \c ResetFailed exception must be
+ /// thrown. In this unique case, a strong exception safety guarantee
+ /// is provided by default, as the \c ZoneTableSegment was clear
+ /// previously, and remains cleared.
+ ///
+ /// In all other cases, \c ZoneTableSegment contents can be expected
+ /// as reset.
+ ///
+ /// See \c MemorySegmentOpenMode for a definition of "storage area"
+ /// and the various modes in which a \c MemorySegment can be opened.
///
- /// This creates a new write that can be used to update zones
- /// inside this zone table segment.
+ /// \c params should contain an implementation-defined
+ /// configuration. See the specific \c ZoneTableSegment
+ /// implementation class for details of what to pass in this
+ /// argument.
+ ///
+ /// \throw isc::InvalidParameter if the configuration in \c params
+ /// has incorrect syntax, but there is a strong exception safety
+ /// guarantee and the \c ZoneTableSegment is usable or unusable as
+ /// before.
+ ///
+ /// \throw ResetFailed if there was a problem in opening the new
+ /// memory store, but there is a strong exception safety guarantee
+ /// and the \c ZoneTableSegment is usable or unusable as before.
+ ///
+ /// \throw ResetFailedAndSegmentCleared if there was a problem in
+ /// opening the new memory store, but there is only a basic
+ /// exception safety guarantee and the \c ZoneTableSegment is not
+ /// usable without a further successful \c reset().
+ ///
+ /// \throw isc::NotImplemented Some implementations may choose to
+ /// not implement this method. In this case, there must be a strong
+ /// exception safety guarantee and the \c ZoneTableSegment is usable
+ /// or unusable as before.
+ ///
+ /// \param mode The open mode (see the MemorySegmentOpenMode
+ /// documentation).
+ /// \param params An element containing implementation-specific
+ /// config (see the description).
+ virtual void reset(MemorySegmentOpenMode mode,
+ isc::data::ConstElementPtr params) = 0;
+
+ /// \brief Close the currently configured \c MemorySegment (if
+ /// open).
+ ///
+ /// See the \c reset() method's documentation for a definition of
+ /// "open" and "close".
+ ///
+ /// Implementations of this method should close any currently
+ /// configured \c MemorySegment and clear the `ZoneTableSegment` to
+ /// a freshly constructed state.
+ ///
+ /// \throw isc::NotImplemented Some implementations may choose to
+ /// not implement this method. In this case, there must be a strong
+ /// exception safety guarantee and the \c ZoneTableSegment is usable
+ /// or unusable as before.
+ virtual void clear() = 0;
+
+ /// \brief Return true if the \c ZoneTableSegment has been
+ /// successfully \c reset().
///
- /// \param load_action Callback to provide the actual data.
- /// \param origin The origin of the zone to reload.
- /// \param rrclass The class of the zone to reload.
- /// \return New instance of a zone writer. The ownership is passed
- /// onto the caller and the caller needs to \c delete it when
- /// it's done with the writer.
- virtual ZoneWriter* getZoneWriter(const LoadAction& load_action,
- const dns::Name& origin,
- const dns::RRClass& rrclass) = 0;
+ /// Note that after calling \c clear(), this method will return
+ /// false until the segment is reset successfully again.
+ virtual bool isUsable() const = 0;
};
} // namespace memory
diff --git a/src/lib/datasrc/memory/zone_table_segment_local.cc b/src/lib/datasrc/memory/zone_table_segment_local.cc
index fdaf678..e0ee369 100644
--- a/src/lib/datasrc/memory/zone_table_segment_local.cc
+++ b/src/lib/datasrc/memory/zone_table_segment_local.cc
@@ -13,7 +13,6 @@
// PERFORMANCE OF THIS SOFTWARE.
#include <datasrc/memory/zone_table_segment_local.h>
-#include "zone_writer_local.h"
using namespace isc::dns;
using namespace isc::util;
@@ -24,6 +23,7 @@ namespace memory {
ZoneTableSegmentLocal::ZoneTableSegmentLocal(const RRClass& rrclass) :
ZoneTableSegment(rrclass),
+ impl_type_("local"),
header_(ZoneTable::create(mem_sgmt_, rrclass))
{
}
@@ -38,6 +38,28 @@ ZoneTableSegmentLocal::~ZoneTableSegmentLocal() {
assert(mem_sgmt_.allMemoryDeallocated());
}
+const std::string&
+ZoneTableSegmentLocal::getImplType() const {
+ return (impl_type_);
+}
+
+void
+ZoneTableSegmentLocal::reset(MemorySegmentOpenMode,
+ isc::data::ConstElementPtr)
+{
+ isc_throw(isc::NotImplemented,
+ "ZoneTableSegmentLocal::reset() is not implemented and "
+ "should not be used.");
+}
+
+void
+ZoneTableSegmentLocal::clear()
+{
+ isc_throw(isc::NotImplemented,
+ "ZoneTableSegmentLocal::clear() is not implemented and "
+ "should not be used.");
+}
+
// After more methods' definitions are added here, it would be a good
// idea to move getHeader() and getMemorySegment() definitions to the
// header file.
@@ -56,14 +78,6 @@ ZoneTableSegmentLocal::getMemorySegment() {
return (mem_sgmt_);
}
-ZoneWriter*
-ZoneTableSegmentLocal::getZoneWriter(const LoadAction& load_action,
- const dns::Name& name,
- const dns::RRClass& rrclass)
-{
- return (new ZoneWriterLocal(this, load_action, name, rrclass));
-}
-
} // namespace memory
} // namespace datasrc
} // namespace isc
diff --git a/src/lib/datasrc/memory/zone_table_segment_local.h b/src/lib/datasrc/memory/zone_table_segment_local.h
index e08ca39..c2312fa 100644
--- a/src/lib/datasrc/memory/zone_table_segment_local.h
+++ b/src/lib/datasrc/memory/zone_table_segment_local.h
@@ -18,19 +18,22 @@
#include <datasrc/memory/zone_table_segment.h>
#include <util/memory_segment_local.h>
+#include <string>
+
namespace isc {
namespace datasrc {
namespace memory {
-/// \brief Local implementation of ZoneTableSegment class
+/// \brief Local implementation of \c ZoneTableSegment class
///
/// This class specifies a concrete implementation for a
-/// MemorySegmentLocal based ZoneTableSegment. Please see the
-/// ZoneTableSegment class documentation for usage.
+/// \c MemorySegmentLocal -based \c ZoneTableSegment. Please see the
+/// \c ZoneTableSegment class documentation for usage.
class ZoneTableSegmentLocal : public ZoneTableSegment {
- // This is so that ZoneTableSegmentLocal can be instantiated from
- // ZoneTableSegment::create().
+ // This is so that \c ZoneTableSegmentLocal can be instantiated from
+ // \c ZoneTableSegment::create().
friend class ZoneTableSegment;
+
protected:
/// \brief Protected constructor
///
@@ -38,26 +41,60 @@ protected:
/// (\c ZoneTableSegment::create()), so this constructor is
/// protected.
ZoneTableSegmentLocal(const isc::dns::RRClass& rrclass);
+
public:
/// \brief Destructor
virtual ~ZoneTableSegmentLocal();
- /// \brief Return the ZoneTableHeader for the local zone table
- /// segment implementation.
+ /// \brief Returns "local" as the implementation type.
+ virtual const std::string& getImplType() const;
+
+ /// \brief Return the \c ZoneTableHeader for this local zone table
+ /// segment.
virtual ZoneTableHeader& getHeader();
- /// \brief const version of \c getHeader().
+ /// \brief \c const version of \c getHeader().
virtual const ZoneTableHeader& getHeader() const;
- /// \brief Return the MemorySegment for the local zone table segment
- /// implementation (a MemorySegmentLocal instance).
+ /// \brief Return the \c MemorySegment for the local zone table
+ /// segment implementation (a \c MemorySegmentLocal instance).
virtual isc::util::MemorySegment& getMemorySegment();
- /// \brief Concrete implementation of ZoneTableSegment::getZoneWriter
- virtual ZoneWriter* getZoneWriter(const LoadAction& load_action,
- const dns::Name& origin,
- const dns::RRClass& rrclass);
+ /// \brief Return true if the segment is writable.
+ ///
+ /// Local segments are always writable. This implementation always
+ /// returns true.
+ virtual bool isWritable() const {
+ return (true);
+ }
+
+ /// \brief This method is not implemented.
+ ///
+ /// Resetting a local \c ZoneTableSegment is not supported at this
+ /// time.
+ ///
+ /// \throw isc::NotImplemented
+ virtual void reset(MemorySegmentOpenMode mode,
+ isc::data::ConstElementPtr params);
+
+ /// \brief This method is not implemented.
+ ///
+ /// Clearing a local \c ZoneTableSegment is not supported at this
+ /// time.
+ ///
+ /// \throw isc::NotImplemented
+ virtual void clear();
+
+ /// \brief Return true if the segment is usable.
+ ///
+ /// Local segments are always usable. This implementation always
+ /// returns true.
+ virtual bool isUsable() const {
+ return (true);
+ }
+
private:
+ std::string impl_type_;
isc::util::MemorySegmentLocal mem_sgmt_;
ZoneTableHeader header_;
};
diff --git a/src/lib/datasrc/memory/zone_table_segment_mapped.cc b/src/lib/datasrc/memory/zone_table_segment_mapped.cc
new file mode 100644
index 0000000..330107f
--- /dev/null
+++ b/src/lib/datasrc/memory/zone_table_segment_mapped.cc
@@ -0,0 +1,392 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <datasrc/memory/zone_table_segment_mapped.h>
+#include <datasrc/memory/zone_table.h>
+#include <datasrc/memory/segment_object_holder.h>
+
+#include <memory>
+
+using namespace isc::data;
+using namespace isc::dns;
+using namespace isc::util;
+using isc::datasrc::memory::detail::SegmentObjectHolder;
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+
+namespace { // unnamed namespace
+
+// The name with which the zone table checksum is associated in the segment.
+const char* const ZONE_TABLE_CHECKSUM_NAME = "zone_table_checksum";
+
+// The name with which the zone table header is associated in the segment.
+const char* const ZONE_TABLE_HEADER_NAME = "zone_table_header";
+
+} // end of unnamed namespace
+
+ZoneTableSegmentMapped::ZoneTableSegmentMapped(const RRClass& rrclass) :
+ ZoneTableSegment(rrclass),
+ impl_type_("mapped"),
+ rrclass_(rrclass)
+{
+}
+
+ZoneTableSegmentMapped::~ZoneTableSegmentMapped() {
+ sync();
+}
+
+const std::string&
+ZoneTableSegmentMapped::getImplType() const {
+ return (impl_type_);
+}
+
+bool
+ZoneTableSegmentMapped::processChecksum(MemorySegmentMapped& segment,
+ bool create, bool has_allocations,
+ std::string& error_msg)
+{
+ const MemorySegment::NamedAddressResult result =
+ segment.getNamedAddress(ZONE_TABLE_CHECKSUM_NAME);
+ if (result.first) {
+ if (create) {
+ // There must be no previously saved checksum.
+ error_msg = "There is already a saved checksum in the segment "
+ "opened in create mode";
+ return (false);
+ } else {
+ // The segment was already shrunk when it was last
+ // closed. Check that its checksum is consistent.
+ assert(result.second);
+ size_t* checksum = static_cast<size_t*>(result.second);
+ const size_t saved_checksum = *checksum;
+ // First, clear the checksum so that getCheckSum() returns a
+ // consistent value.
+ *checksum = 0;
+ const size_t new_checksum = segment.getCheckSum();
+ if (saved_checksum != new_checksum) {
+ error_msg = "Saved checksum doesn't match segment data";
+ return (false);
+ }
+ }
+ } else {
+ if ((!create) && has_allocations) {
+ // If we are resetting in READ_WRITE mode, and some memory
+ // was already allocated but there is no checksum name, that
+ // indicates that the segment is corrupted.
+ error_msg = "Existing segment is missing a checksum name";
+ return (false);
+ }
+
+ // Allocate space for a checksum (which is saved during close).
+ void* checksum = NULL;
+ while (!checksum) {
+ try {
+ checksum = segment.allocate(sizeof(size_t));
+ } catch (const MemorySegmentGrown&) {
+ // Do nothing and try again.
+ }
+ }
+ *static_cast<size_t*>(checksum) = 0;
+ segment.setNamedAddress(ZONE_TABLE_CHECKSUM_NAME, checksum);
+ }
+
+ return (true);
+}
+
+bool
+ZoneTableSegmentMapped::processHeader(MemorySegmentMapped& segment,
+ bool create, bool has_allocations,
+ std::string& error_msg)
+{
+ const MemorySegment::NamedAddressResult result =
+ segment.getNamedAddress(ZONE_TABLE_HEADER_NAME);
+ if (result.first) {
+ if (create) {
+ // There must be no previously saved header.
+ error_msg = "There is already a saved ZoneTableHeader in the "
+ "segment opened in create mode";
+ return (false);
+ } else {
+ assert(result.second);
+ }
+ } else {
+ if ((!create) && has_allocations) {
+ // If we are resetting in READ_WRITE mode, and some memory
+ // was already allocated but there is no header name, that
+ // indicates that the segment is corrupted.
+ error_msg = "Existing segment is missing a ZoneTableHeader name";
+ return (false);
+ }
+
+ while (true) {
+ try {
+ SegmentObjectHolder<ZoneTable, int> zt_holder(segment, 0);
+ zt_holder.set(ZoneTable::create(segment, rrclass_));
+ void* ptr = segment.allocate(sizeof(ZoneTableHeader));
+ ZoneTableHeader* new_header = new(ptr)
+ ZoneTableHeader(zt_holder.release());
+ segment.setNamedAddress(ZONE_TABLE_HEADER_NAME, new_header);
+ break;
+ } catch (const MemorySegmentGrown&) {}
+ }
+ }
+
+ return (true);
+}
+
+MemorySegmentMapped*
+ZoneTableSegmentMapped::openReadWrite(const std::string& filename,
+ bool create)
+{
+ const MemorySegmentMapped::OpenMode mode = create ?
+ MemorySegmentMapped::CREATE_ONLY :
+ MemorySegmentMapped::OPEN_OR_CREATE;
+ // In case there is a problem, we throw. We want the segment to be
+ // automatically destroyed then.
+ std::auto_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(filename, mode));
+
+ // This flag is used inside processCheckSum() and processHeader(),
+ // and must be initialized before we make any further allocations.
+ const bool has_allocations = !segment->allMemoryDeallocated();
+
+ std::string error_msg;
+ if ((!processChecksum(*segment, create, has_allocations, error_msg)) ||
+ (!processHeader(*segment, create, has_allocations, error_msg))) {
+ if (mem_sgmt_) {
+ isc_throw(ResetFailed,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ } else {
+ isc_throw(ResetFailedAndSegmentCleared,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ }
+ }
+
+ return (segment.release());
+}
+
+MemorySegmentMapped*
+ZoneTableSegmentMapped::openReadOnly(const std::string& filename) {
+ // In case the checksum or table header is missing, we throw. We
+ // want the segment to be automatically destroyed then.
+ std::auto_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(filename));
+ // There must be a previously saved checksum.
+ MemorySegment::NamedAddressResult result =
+ segment->getNamedAddress(ZONE_TABLE_CHECKSUM_NAME);
+ if (!result.first) {
+ const std::string error_msg =
+ "There is no previously saved checksum in a "
+ "mapped segment opened in read-only mode";
+ if (mem_sgmt_) {
+ isc_throw(ResetFailed,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ } else {
+ isc_throw(ResetFailedAndSegmentCleared,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ }
+ }
+
+ // We can't verify the checksum here as we can't set the checksum to
+ // 0 for checksum calculation in a read-only segment. So we continue
+ // without verifying the checksum.
+
+ // There must be a previously saved ZoneTableHeader.
+ result = segment->getNamedAddress(ZONE_TABLE_HEADER_NAME);
+ if (result.first) {
+ assert(result.second);
+ } else {
+ const std::string error_msg =
+ "There is no previously saved ZoneTableHeader in a "
+ "mapped segment opened in read-only mode.";
+ if (mem_sgmt_) {
+ isc_throw(ResetFailed,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ } else {
+ isc_throw(ResetFailedAndSegmentCleared,
+ "Error in resetting zone table segment to use "
+ << filename << ": " << error_msg);
+ }
+ }
+
+ return (segment.release());
+}
+
+void
+ZoneTableSegmentMapped::reset(MemorySegmentOpenMode mode,
+ isc::data::ConstElementPtr params)
+{
+ if (!params || params->getType() != Element::map) {
+ isc_throw(isc::InvalidParameter,
+ "Configuration does not contain a map");
+ }
+
+ if (!params->contains("mapped-file")) {
+ isc_throw(isc::InvalidParameter,
+ "Configuration does not contain a \"mapped-file\" key");
+ }
+
+ ConstElementPtr mapped_file = params->get("mapped-file");
+ if ((!mapped_file) || (mapped_file->getType() != Element::string)) {
+ isc_throw(isc::InvalidParameter,
+ "Value of \"mapped-file\" is not a string");
+ }
+
+ const std::string filename = mapped_file->stringValue();
+
+ if (mem_sgmt_ && (filename == current_filename_)) {
+ // This reset() is an attempt to re-open the currently open
+ // mapped file. We cannot do this in many mode combinations
+ // unless we close the existing mapped file. So just close it.
+ clear();
+ } else {
+ sync();
+ }
+
+ // In case current_filename_ below fails, we want the segment to be
+ // automatically destroyed.
+ std::auto_ptr<MemorySegmentMapped> segment;
+
+ switch (mode) {
+ case CREATE:
+ segment.reset(openReadWrite(filename, true));
+ break;
+
+ case READ_WRITE:
+ segment.reset(openReadWrite(filename, false));
+ break;
+
+ case READ_ONLY:
+ segment.reset(openReadOnly(filename));
+ break;
+
+ default:
+ isc_throw(isc::InvalidParameter,
+ "Invalid MemorySegmentOpenMode passed to reset()");
+ }
+
+ current_filename_ = filename;
+ current_mode_ = mode;
+ mem_sgmt_.reset(segment.release());
+
+ if (!isWritable()) {
+ // Given what we setup above, the following must not throw at
+ // this point. If it does, all bets are off.
+ cached_ro_header_ = getHeaderHelper<ZoneTableHeader>(true);
+ }
+}
+
+void
+ZoneTableSegmentMapped::sync() {
+ // Synchronize checksum, etc.
+ if (mem_sgmt_ && isWritable()) {
+ // If there is a previously opened segment, and it was opened in
+ // read-write mode, update its checksum.
+ mem_sgmt_->shrinkToFit();
+ const MemorySegment::NamedAddressResult result =
+ mem_sgmt_->getNamedAddress(ZONE_TABLE_CHECKSUM_NAME);
+ assert(result.first);
+ assert(result.second);
+ size_t* checksum = static_cast<size_t*>(result.second);
+ // First, clear the checksum so that getCheckSum() returns a
+ // consistent value.
+ *checksum = 0;
+ const size_t new_checksum = mem_sgmt_->getCheckSum();
+ // Now, update it into place.
+ *checksum = new_checksum;
+ }
+}
+
+void
+ZoneTableSegmentMapped::clear() {
+ if (mem_sgmt_) {
+ sync();
+ mem_sgmt_.reset();
+ }
+}
+
+template<typename T>
+T*
+ZoneTableSegmentMapped::getHeaderHelper(bool initial) const {
+ if (!isUsable()) {
+ isc_throw(isc::InvalidOperation,
+ "getHeader() called without calling reset() first");
+ }
+
+ if (!isWritable() && !initial) {
+ // The header address would not have changed since reset() for
+ // READ_ONLY segments.
+ return (cached_ro_header_);
+ }
+
+ const MemorySegment::NamedAddressResult result =
+ mem_sgmt_->getNamedAddress(ZONE_TABLE_HEADER_NAME);
+ if (!result.first) {
+ isc_throw(isc::Unexpected,
+ "Unable to look up the address of the table header in "
+ "getHeader()");
+ }
+
+ T* header = static_cast<ZoneTableHeader*>(result.second);
+ assert(header);
+ return (header);
+}
+
+ZoneTableHeader&
+ZoneTableSegmentMapped::getHeader() {
+ return (*getHeaderHelper<ZoneTableHeader>(false));
+}
+
+const ZoneTableHeader&
+ZoneTableSegmentMapped::getHeader() const {
+ return (*getHeaderHelper<const ZoneTableHeader>(false));
+}
+
+MemorySegment&
+ZoneTableSegmentMapped::getMemorySegment() {
+ if (!isUsable()) {
+ isc_throw(isc::InvalidOperation,
+ "getMemorySegment() called without calling reset() first");
+ }
+ return (*mem_sgmt_);
+}
+
+bool
+ZoneTableSegmentMapped::isUsable() const {
+ // If mem_sgmt_ is not empty, then it is usable.
+ return (mem_sgmt_);
+}
+
+bool
+ZoneTableSegmentMapped::isWritable() const {
+ if (!isUsable()) {
+ // If reset() was never performed for this segment, or if the
+ // most recent reset() had failed, or if the segment had been
+ // cleared, then the segment is not writable.
+ return (false);
+ }
+
+ return ((current_mode_ == CREATE) || (current_mode_ == READ_WRITE));
+}
+
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
diff --git a/src/lib/datasrc/memory/zone_table_segment_mapped.h b/src/lib/datasrc/memory/zone_table_segment_mapped.h
new file mode 100644
index 0000000..1776314
--- /dev/null
+++ b/src/lib/datasrc/memory/zone_table_segment_mapped.h
@@ -0,0 +1,144 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef ZONE_TABLE_SEGMENT_MAPPED_H
+#define ZONE_TABLE_SEGMENT_MAPPED_H
+
+#include <datasrc/memory/zone_table_segment.h>
+#include <util/memory_segment_mapped.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <string>
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+
+/// \brief Mapped-file based implementation of \c ZoneTableSegment class
+///
+/// This class specifies a concrete implementation for a memory-mapped
+/// \c ZoneTableSegment. Please see the \c ZoneTableSegment class
+/// documentation for usage.
+class ZoneTableSegmentMapped : public ZoneTableSegment {
+ // This is so that \c ZoneTableSegmentMapped can be instantiated
+ // from \c ZoneTableSegment::create().
+ friend class ZoneTableSegment;
+
+protected:
+ /// \brief Protected constructor
+ ///
+ /// Instances are expected to be created by the factory method
+ /// (\c ZoneTableSegment::create()), so this constructor is
+ /// protected.
+ ZoneTableSegmentMapped(const isc::dns::RRClass& rrclass);
+
+public:
+ /// \brief Destructor
+ virtual ~ZoneTableSegmentMapped();
+
+ /// \brief Returns "mapped" as the implementation type.
+ virtual const std::string& getImplType() const;
+
+ /// \brief Return the \c ZoneTableHeader for this mapped zone table
+ /// segment.
+ ///
+ /// \throws isc::InvalidOperation if this method is called without a
+ /// successful \c reset() call first.
+ virtual ZoneTableHeader& getHeader();
+
+ /// \brief const version of \c getHeader().
+ virtual const ZoneTableHeader& getHeader() const;
+
+ /// \brief Return the \c MemorySegment for the memory-mapped zone
+ /// table segment implementation (a \c MemorySegmentMapped
+ /// instance).
+ ///
+ /// \throws isc::InvalidOperation if this method is called without a
+ /// successful \c reset() call first.
+ virtual isc::util::MemorySegment& getMemorySegment();
+
+ /// \brief Returns if the segment is writable.
+ ///
+ /// Segments successfully opened in CREATE or READ_WRITE modes are
+ /// writable. Segments opened in READ_ONLY mode are not writable.
+ /// If the \c ZoneTableSegment was cleared for some reason, it is
+ /// not writable until it is reset successfully.
+ virtual bool isWritable() const;
+
+ /// \brief Close the current \c MemorySegment (if open) and open the
+ /// requested one.
+ ///
+ /// See \c MemorySegmentOpenMode for a definition of "storage area"
+ /// and the various modes in which a \c MemorySegment can be opened.
+ ///
+ /// \c params should be a map containing a "mapped-file" key that
+ /// points to a string value containing the filename of a mapped
+ /// file. E.g.,
+ ///
+ /// {"mapped-file": "/var/bind10/mapped-files/zone-sqlite3.mapped.0"}
+ ///
+ /// Please see the \c ZoneTableSegment API documentation for the
+ /// behavior in case of exceptions.
+ ///
+ /// \throws isc::Unexpected when it's unable to lookup a named
+ /// address that it expected to be present. This is extremely
+ /// unlikely, and it points to corruption.
+ ///
+ /// \param mode The open mode (see the \c MemorySegmentOpenMode
+ /// documentation in \c ZoneTableSegment class).
+ /// \param params An element containing config for the mapped file
+ /// (see the description).
+ virtual void reset(MemorySegmentOpenMode mode,
+ isc::data::ConstElementPtr params);
+
+ /// \brief Close the currently configured \c MemorySegment (if
+ /// open). See the base class for a definition of "open" and
+ /// "close".
+ virtual void clear();
+
+ /// \brief Return true if the segment is usable.
+ ///
+ /// See the base class for the description.
+ virtual bool isUsable() const;
+
+private:
+ void sync();
+
+ bool processChecksum(isc::util::MemorySegmentMapped& segment, bool create,
+ bool has_allocations, std::string& error_msg);
+ bool processHeader(isc::util::MemorySegmentMapped& segment, bool create,
+ bool has_allocations, std::string& error_msg);
+
+ isc::util::MemorySegmentMapped* openReadWrite(const std::string& filename,
+ bool create);
+ isc::util::MemorySegmentMapped* openReadOnly(const std::string& filename);
+
+ template<typename T> T* getHeaderHelper(bool initial) const;
+
+private:
+ std::string impl_type_;
+ isc::dns::RRClass rrclass_;
+ MemorySegmentOpenMode current_mode_;
+ std::string current_filename_;
+ // Internally holds a MemorySegmentMapped. This is NULL on
+ // construction, and is set by the \c reset() method.
+ boost::scoped_ptr<isc::util::MemorySegmentMapped> mem_sgmt_;
+ ZoneTableHeader* cached_ro_header_;
+};
+
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
+
+#endif // ZONE_TABLE_SEGMENT_MAPPED_H
diff --git a/src/lib/datasrc/memory/zone_writer.cc b/src/lib/datasrc/memory/zone_writer.cc
new file mode 100644
index 0000000..ea70bb9
--- /dev/null
+++ b/src/lib/datasrc/memory/zone_writer.cc
@@ -0,0 +1,175 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <datasrc/memory/zone_writer.h>
+#include <datasrc/memory/zone_data.h>
+#include <datasrc/memory/zone_table_segment.h>
+#include <datasrc/memory/segment_object_holder.h>
+
+#include <boost/scoped_ptr.hpp>
+
+#include <dns/rrclass.h>
+
+#include <datasrc/exceptions.h>
+
+#include <memory>
+
+using std::auto_ptr;
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+
+ZoneTableSegment&
+checkZoneTableSegment(ZoneTableSegment& segment) {
+ if (!segment.isWritable()) {
+ isc_throw(isc::InvalidOperation,
+ "Attempt to construct ZoneWriter for a read-only segment");
+ }
+ return (segment);
+}
+
+struct ZoneWriter::Impl {
+ Impl(ZoneTableSegment& segment, const LoadAction& load_action,
+ const dns::Name& origin, const dns::RRClass& rrclass,
+ bool throw_on_load_error) :
+ // We validate segment first so we can use it to initialize
+ // data_holder_ safely.
+ segment_(checkZoneTableSegment(segment)),
+ load_action_(load_action),
+ origin_(origin),
+ rrclass_(rrclass),
+ state_(ZW_UNUSED),
+ catch_load_error_(throw_on_load_error)
+ {
+ while (true) {
+ try {
+ data_holder_.reset(
+ new ZoneDataHolder(segment.getMemorySegment(), rrclass_));
+ break;
+ } catch (const isc::util::MemorySegmentGrown&) {}
+ }
+ }
+
+ ZoneTableSegment& segment_;
+ const LoadAction load_action_;
+ const dns::Name origin_;
+ const dns::RRClass rrclass_;
+ enum State {
+ ZW_UNUSED,
+ ZW_LOADED,
+ ZW_INSTALLED,
+ ZW_CLEANED
+ };
+ State state_;
+ const bool catch_load_error_;
+ typedef detail::SegmentObjectHolder<ZoneData, dns::RRClass> ZoneDataHolder;
+ boost::scoped_ptr<ZoneDataHolder> data_holder_;
+};
+
+ZoneWriter::ZoneWriter(ZoneTableSegment& segment,
+ const LoadAction& load_action,
+ const dns::Name& origin,
+ const dns::RRClass& rrclass,
+ bool throw_on_load_error) :
+ impl_(new Impl(segment, load_action, origin, rrclass, throw_on_load_error))
+{
+}
+
+ZoneWriter::~ZoneWriter() {
+ // Clean up everything there might be left if someone forgot, just
+ // in case.
+ cleanup();
+ delete impl_;
+}
+
+void
+ZoneWriter::load(std::string* error_msg) {
+ if (impl_->state_ != Impl::ZW_UNUSED) {
+ isc_throw(isc::InvalidOperation, "Trying to load twice");
+ }
+
+ try {
+ ZoneData* zone_data =
+ impl_->load_action_(impl_->segment_.getMemorySegment());
+
+ if (!zone_data) {
+ // Bug inside impl_->load_action_.
+ isc_throw(isc::InvalidOperation,
+ "No data returned from load action");
+ }
+
+ impl_->data_holder_->set(zone_data);
+
+ } catch (const ZoneLoaderException& ex) {
+ if (!impl_->catch_load_error_) {
+ throw;
+ }
+ if (error_msg) {
+ *error_msg = ex.what();
+ }
+ }
+
+ impl_->state_ = Impl::ZW_LOADED;
+}
+
+void
+ZoneWriter::install() {
+ if (impl_->state_ != Impl::ZW_LOADED) {
+ isc_throw(isc::InvalidOperation, "No data to install");
+ }
+
+ // Check the internal integrity assumption: we should have non NULL
+ // zone data or we've allowed load error to create an empty zone.
+ assert(impl_->data_holder_.get() || impl_->catch_load_error_);
+
+ while (impl_->state_ != Impl::ZW_INSTALLED) {
+ try {
+ ZoneTableHeader& header = impl_->segment_.getHeader();
+ ZoneTable* table(header.getTable());
+ if (!table) {
+ isc_throw(isc::Unexpected, "No zone table present");
+ }
+ // We still need to hold the zone data until we return from
+ // addZone in case it throws, but we then need to immediately
+ // release it as the ownership is transferred to the zone table.
+ // we release this by (re)set it to the old data; that way we can
+ // use the holder for the final cleanup.
+ const ZoneTable::AddResult result(
+ impl_->data_holder_->get() ?
+ table->addZone(impl_->segment_.getMemorySegment(),
+ impl_->origin_, impl_->data_holder_->get()) :
+ table->addEmptyZone(impl_->segment_.getMemorySegment(),
+ impl_->origin_));
+ impl_->data_holder_->set(result.zone_data);
+ impl_->state_ = Impl::ZW_INSTALLED;
+ } catch (const isc::util::MemorySegmentGrown&) {}
+ }
+}
+
+void
+ZoneWriter::cleanup() {
+ // We eat the data (if any) now.
+
+ ZoneData* zone_data = impl_->data_holder_->release();
+ if (zone_data) {
+ ZoneData::destroy(impl_->segment_.getMemorySegment(), zone_data,
+ impl_->rrclass_);
+ impl_->state_ = Impl::ZW_CLEANED;
+ }
+}
+
+}
+}
+}
diff --git a/src/lib/datasrc/memory/zone_writer.h b/src/lib/datasrc/memory/zone_writer.h
index 0e8f285..bdd350c 100644
--- a/src/lib/datasrc/memory/zone_writer.h
+++ b/src/lib/datasrc/memory/zone_writer.h
@@ -15,30 +15,58 @@
#ifndef MEM_ZONE_WRITER_H
#define MEM_ZONE_WRITER_H
-#include "load_action.h"
+#include <datasrc/memory/load_action.h>
+
+#include <boost/noncopyable.hpp>
+
+#include <dns/dns_fwd.h>
namespace isc {
namespace datasrc {
namespace memory {
+class ZoneTableSegment;
/// \brief Does an update to a zone.
///
-/// This abstract base class represents the work of a reload of a zone.
-/// The work is divided into three stages -- load(), install() and cleanup().
-/// They should be called in this order for the effect to take place.
+/// This represents the work of a (re)load of a zone. The work is divided
+/// into three stages -- load(), install() and cleanup(). They should
+/// be called in this order for the effect to take place.
///
/// We divide them so the update of zone data can be done asynchronously,
/// in a different thread. The install() operation is the only one that needs
/// to be done in a critical section.
///
-/// Each derived class implementation must provide the strong exception
-/// guarantee for each public method. That is, when any of the methods
-/// throws, the entire state should stay the same as before the call
-/// (how to achieve that may be implementation dependant).
-class ZoneWriter {
+/// This class provides strong exception guarantee for each public
+/// method. That is, when any of the methods throws, the entire state
+/// stays the same as before the call.
+class ZoneWriter : boost::noncopyable {
public:
- /// \brief Virtual destructor.
- virtual ~ZoneWriter() {};
+ /// \brief Constructor
+ ///
+ /// If \c catch_load_error is set to true, the \c load() method will
+ /// internally catch load related errors reported as a DataSourceError
+ /// exception, and subsequent \c install() method will add a special
+ /// empty zone to the zone table segment. If it's set to false, \c load()
+ /// will simply propagate the exception. This parameter would normally
+ /// be set to false as it's not desirable to install a broken zone;
+ /// however, it would be better to be set to true at the initial loading
+ /// so the zone table recognizes the existence of the zone (and being
+ /// aware that it's broken).
+ ///
+ /// \throw isc::InvalidOperation if \c segment is read-only.
+ ///
+ /// \param segment The zone table segment to store the zone into.
+ /// \param load_action The callback used to load data.
+ /// \param name The name of the zone.
+ /// \param rrclass The class of the zone.
+ /// \param catch_load_error true if loading errors are to be caught
+ /// internally; false otherwise.
+ ZoneWriter(ZoneTableSegment& segment,
+ const LoadAction& load_action, const dns::Name& name,
+ const dns::RRClass& rrclass, bool catch_load_error);
+
+ /// \brief Destructor.
+ ~ZoneWriter();
/// \brief Get the zone data into memory.
///
@@ -49,6 +77,12 @@ public:
/// This is the first method you should call on the object. Never call it
/// multiple times.
///
+ /// If the optional parameter \c error_msg is given and non NULL, and
+ /// if the writer object was constructed with \c catch_load_error being
+ /// true, then error_msg will be filled with text indicating the reason
+ /// for the error in case a load error happens. In other cases any
+ /// passed non NULL error_msg will be intact.
+ ///
/// \note As this contains reading of files or other data sources, or with
/// some other source of the data to load, it may throw quite anything.
/// If it throws, do not call any other methods on the object and
@@ -56,24 +90,33 @@ public:
/// \note After successful load(), you have to call cleanup() some time
/// later.
/// \throw isc::InvalidOperation if called second time.
- virtual void load() = 0;
+ /// \throw DataSourceError load related error (not thrown if constructed
+ /// with catch_load_error being \c true).
+ ///
+ /// \param error_msg If non NULL, used as a placeholder to store load error
+ /// messages.
+ void load(std::string* error_msg = NULL);
/// \brief Put the changes to effect.
///
/// This replaces the old version of zone with the one previously prepared
/// by load(). It takes ownership of the old zone data, if any.
///
- /// You may call it only after successful load() and at most once.
+ /// You may call it only after successful load() and at most once. It
+ /// includes the case the writer is constructed with catch_load_error
+ /// being true and load() encountered and caught a DataSourceError
+ /// exception. In this case this method installs a special empty zone
+ /// to the table.
///
/// The operation is expected to be fast and is meant to be used inside
/// a critical section.
///
- /// This may throw in rare cases, depending on the concrete implementation.
- /// If it throws, you still need to call cleanup().
+ /// This may throw in rare cases. If it throws, you still need to
+ /// call cleanup().
///
/// \throw isc::InvalidOperation if called without previous load() or for
/// the second time or cleanup() was called already.
- virtual void install() = 0;
+ void install();
/// \brief Clean up resources.
///
@@ -81,12 +124,22 @@ public:
/// one loaded by load() in case install() was not called or was not
/// successful, or the one replaced in install().
///
- /// Generally, this should never throw.
- virtual void cleanup() = 0;
+ /// \throw none
+ void cleanup();
+
+private:
+ // We hide details as this class will be used by various applications
+ // and we use some internal data structures in the implementation.
+ struct Impl;
+ Impl* impl_;
};
}
}
}
-#endif
+#endif // MEM_ZONE_WRITER_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/datasrc/memory/zone_writer_local.cc b/src/lib/datasrc/memory/zone_writer_local.cc
deleted file mode 100644
index 0cd9587..0000000
--- a/src/lib/datasrc/memory/zone_writer_local.cc
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include "zone_writer_local.h"
-#include "zone_data.h"
-#include "zone_table_segment_local.h"
-
-#include <memory>
-
-using std::auto_ptr;
-
-namespace isc {
-namespace datasrc {
-namespace memory {
-
-ZoneWriterLocal::ZoneWriterLocal(ZoneTableSegmentLocal* segment,
- const LoadAction& load_action,
- const dns::Name& origin,
- const dns::RRClass& rrclass) :
- segment_(segment),
- load_action_(load_action),
- origin_(origin),
- rrclass_(rrclass),
- zone_data_(NULL),
- state_(ZW_UNUSED)
-{}
-
-ZoneWriterLocal::~ZoneWriterLocal() {
- // Clean up everything there might be left if someone forgot, just
- // in case.
- cleanup();
-}
-
-void
-ZoneWriterLocal::load() {
- if (state_ != ZW_UNUSED) {
- isc_throw(isc::InvalidOperation, "Trying to load twice");
- }
-
- zone_data_ = load_action_(segment_->getMemorySegment());
-
- if (zone_data_ == NULL) {
- // Bug inside load_action_.
- isc_throw(isc::InvalidOperation, "No data returned from load action");
- }
-
- state_ = ZW_LOADED;
-}
-
-void
-ZoneWriterLocal::install() {
- if (state_ != ZW_LOADED) {
- isc_throw(isc::InvalidOperation, "No data to install");
- }
-
-
- ZoneTable* table(segment_->getHeader().getTable());
- if (table == NULL) {
- isc_throw(isc::Unexpected, "No zone table present");
- }
- const ZoneTable::AddResult result(table->addZone(
- segment_->getMemorySegment(),
- rrclass_, origin_, zone_data_));
-
- state_ = ZW_INSTALLED;
- zone_data_ = result.zone_data;
-}
-
-void
-ZoneWriterLocal::cleanup() {
- // We eat the data (if any) now.
-
- if (zone_data_ != NULL) {
- ZoneData::destroy(segment_->getMemorySegment(), zone_data_, rrclass_);
- zone_data_ = NULL;
- state_ = ZW_CLEANED;
- }
-}
-
-}
-}
-}
diff --git a/src/lib/datasrc/result.h b/src/lib/datasrc/result.h
index 5a28d08..7a042cb 100644
--- a/src/lib/datasrc/result.h
+++ b/src/lib/datasrc/result.h
@@ -18,13 +18,10 @@
namespace isc {
namespace datasrc {
namespace result {
-/// Result codes of various public methods of in memory data source
+/// \brief Result codes of various public methods of DataSourceClient.
///
/// The detailed semantics may differ in different methods.
/// See the description of specific methods for more details.
-///
-/// Note: this is intended to be used from other data sources eventually,
-/// but for now it's specific to in memory data source and its backend.
enum Result {
SUCCESS, ///< The operation is successful.
EXIST, ///< The search key is already stored.
@@ -32,8 +29,26 @@ enum Result {
PARTIALMATCH ///< Only a partial match is found.
};
+/// \brief Flags for supplemental information along with the \c Result
+///
+/// Initially there's only one flag defined, but several flags will be added
+/// later. One likely case is to indicate a flag that is listed in in-memory
+/// but its content is served in the underlying data source. This will help
+/// when only a subset of zones are cached in-memory so the lookup code can
+/// efficiently detect whether it doesn't exist or is not just cached.
+/// When more flags are added, the logical-or operation should be allowed
+/// (by defining \c operator|) on these flags.
+enum ResultFlags {
+ FLAGS_DEFAULT = 0, // no flags
+ ZONE_EMPTY = 1 ///< The zone found is empty, normally meaning it's broken
+};
+
}
}
}
-#endif
+#endif // DATASRC_RESULT_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/datasrc/sqlite3_accessor.cc b/src/lib/datasrc/sqlite3_accessor.cc
index 9321bed..02b55e9 100644
--- a/src/lib/datasrc/sqlite3_accessor.cc
+++ b/src/lib/datasrc/sqlite3_accessor.cc
@@ -566,7 +566,8 @@ SQLite3Accessor::open(const std::string& name) {
}
SQLite3Accessor::~SQLite3Accessor() {
- LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_SQLITE_DROPCONN);
+ LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_SQLITE_DROPCONN)
+ .arg(database_name_);
if (dbparameters_->db_ != NULL) {
close();
}
diff --git a/src/lib/datasrc/sqlite3_datasrc_messages.mes b/src/lib/datasrc/sqlite3_datasrc_messages.mes
index a462d43..c3984d2 100644
--- a/src/lib/datasrc/sqlite3_datasrc_messages.mes
+++ b/src/lib/datasrc/sqlite3_datasrc_messages.mes
@@ -42,8 +42,9 @@ Debug information. An instance of SQLite data source is being created.
% DATASRC_SQLITE_DESTROY SQLite data source destroyed
Debug information. An instance of SQLite data source is being destroyed.
-% DATASRC_SQLITE_DROPCONN SQLite3Database is being deinitialized
-The object around a database connection is being destroyed.
+% DATASRC_SQLITE_DROPCONN SQLite3 database '%1' is being closed
+The object around a database connection is being destroyed. If the
+SQLite3 database file was open, it will be closed now.
% DATASRC_SQLITE_ENCLOSURE looking for zone containing '%1'
Debug information. The SQLite data source is trying to identify which zone
diff --git a/src/lib/datasrc/static.zone.pre b/src/lib/datasrc/static.zone.pre
index 13c0c9d..8046410 100644
--- a/src/lib/datasrc/static.zone.pre
+++ b/src/lib/datasrc/static.zone.pre
@@ -1,10 +1,10 @@
-;; This is the content of the BIND./CH zone. It contains the version and
-;; authors (called VERSION.BIND. and AUTHORS.BIND.). You can add more or
-;; modify the zone. Then you can reload the zone by issuing the command
+;; This file contains records for the BIND./CH zone. It contains version
+;; (VERSION.BIND.) and authors (AUTHORS.BIND.) information. You can add
+;; more records or modify this zone file like any other zone file. If
+;; you modify this file, you can reload the zone by issuing the
+;; following command in the bindctl program:
;;
-;; loadzone CH BIND
-;;
-;; in the bindctl.
+;; Auth loadzone CH BIND
;; This is here mostly for technical reasons.
BIND. 0 CH SOA bind. authors.bind. 0 28800 7200 604800 86400
diff --git a/src/lib/datasrc/tests/cache_config_unittest.cc b/src/lib/datasrc/tests/cache_config_unittest.cc
index e73b06d..34dd5d1 100644
--- a/src/lib/datasrc/tests/cache_config_unittest.cc
+++ b/src/lib/datasrc/tests/cache_config_unittest.cc
@@ -281,7 +281,7 @@ TEST_F(CacheConfigTest, getLoadActionWithMock) {
// Zone configured for the cache but doesn't exist in the underling data
// source.
EXPECT_THROW(cache_conf.getLoadAction(RRClass::IN(), Name("example.net")),
- DataSourceError);
+ NoSuchZone);
// buggy data source client: it returns a null pointer from getIterator.
EXPECT_THROW(cache_conf.getLoadAction(RRClass::IN(), Name("null.org")),
diff --git a/src/lib/datasrc/tests/client_list_unittest.cc b/src/lib/datasrc/tests/client_list_unittest.cc
index 8013f01..7de5cda 100644
--- a/src/lib/datasrc/tests/client_list_unittest.cc
+++ b/src/lib/datasrc/tests/client_list_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -12,8 +12,11 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <config.h>
+
#include <datasrc/client_list.h>
#include <datasrc/client.h>
+#include <datasrc/factory.h>
#include <datasrc/cache_config.h>
#include <datasrc/zone_iterator.h>
#include <datasrc/exceptions.h>
@@ -30,7 +33,9 @@
#include <gtest/gtest.h>
+#include <boost/format.hpp>
#include <boost/shared_ptr.hpp>
+#include <boost/interprocess/file_mapping.hpp>
#include <set>
#include <fstream>
@@ -42,9 +47,8 @@ using isc::datasrc::memory::ZoneTableSegment;
using isc::datasrc::memory::InMemoryZoneFinder;
using namespace isc::data;
using namespace isc::dns;
-// don't import the entire boost namespace. It will unexpectedly hide uintXX_t
-// for some systems.
-using boost::shared_ptr;
+// note: don't use 'using [namespace]' for shared_ptr. It would conflict with
+// C++ std:: definitions.
using namespace std;
namespace {
@@ -67,10 +71,14 @@ public:
if (type == "error") {
isc_throw(DataSourceError, "The error data source type");
}
+ if (type == "library_error") {
+ isc_throw(DataSourceLibraryError,
+ "The library error data source type");
+ }
if (type == "MasterFiles") {
return (DataSourcePair(0, DataSourceClientContainerPtr()));
}
- shared_ptr<MockDataSourceClient>
+ boost::shared_ptr<MockDataSourceClient>
ds(new MockDataSourceClient(type, configuration));
// Make sure it is deleted when the test list is deleted.
to_delete_.push_back(ds);
@@ -79,7 +87,7 @@ public:
private:
// Hold list of data sources created internally, so they are preserved
// until the end of the test and then deleted.
- vector<shared_ptr<MockDataSourceClient> > to_delete_;
+ vector<boost::shared_ptr<MockDataSourceClient> > to_delete_;
};
const char* ds_zones[][3] = {
@@ -103,12 +111,25 @@ const char* ds_zones[][3] = {
const size_t ds_count = (sizeof(ds_zones) / sizeof(*ds_zones));
-class ListTest : public ::testing::Test {
+class SegmentType {
+public:
+ virtual ~SegmentType() {}
+ virtual ConstElementPtr getCacheConfig(bool enabled, const Name& zone)
+ const = 0;
+ virtual void reset(ConfigurableClientList& list,
+ const std::string& datasrc_name,
+ ZoneTableSegment::MemorySegmentOpenMode mode,
+ ConstElementPtr config_params) = 0;
+ virtual std::string getType() = 0;
+};
+
+class ListTest : public ::testing::TestWithParam<SegmentType*> {
public:
ListTest() :
rrclass_(RRClass::IN()),
// The empty list corresponds to a list with no elements inside
list_(new TestedList(rrclass_)),
+ negative_result_(),
config_elem_(Element::fromJSON("["
"{"
" \"type\": \"test_type\","
@@ -119,11 +140,10 @@ public:
" \"type\": \"test_type\","
" \"params\": [\"example.org\", \"example.com\", "
" \"noiter.org\", \"null.org\"]"
- "}]")),
- ztable_segment_(ZoneTableSegment::create(rrclass_, "local"))
+ "}]"))
{
for (size_t i(0); i < ds_count; ++ i) {
- shared_ptr<MockDataSourceClient>
+ boost::shared_ptr<MockDataSourceClient>
ds(new MockDataSourceClient(ds_zones[i]));
ds_.push_back(ds);
ds_info_.push_back(ConfigurableClientList::DataSourceInfo(
@@ -133,6 +153,24 @@ public:
}
}
+ ~ListTest() {
+ ds_info_.clear();
+ for (size_t i(0); i < ds_count; ++ i) {
+ ds_[i].reset();
+ }
+ ds_.clear();
+
+ for (size_t i(0); i < ds_count; ++ i) {
+ boost::interprocess::file_mapping::remove(
+ getMappedFilename(i).c_str());
+ }
+ }
+
+ static std::string getMappedFilename(size_t index) {
+ return (boost::str(boost::format(TEST_DATA_BUILDDIR "/test%d.mapped")
+ % index));
+ }
+
// Install a "fake" cached zone using a temporary underlying data source
// client. If 'enabled' is set to false, emulate a disabled cache, in
// which case there will be no data in memory.
@@ -150,11 +188,7 @@ public:
// Build new cache config to load the specified zone, and replace
// the data source info with the new config.
ConstElementPtr cache_conf_elem =
- Element::fromJSON("{\"type\": \"mock\","
- " \"cache-enable\": " +
- string(enabled ? "true," : "false,") +
- " \"cache-zones\": "
- " [\"" + zone.toText() + "\"]}");
+ GetParam()->getCacheConfig(enabled, zone);
boost::shared_ptr<internal::CacheConfig> cache_conf(
new internal::CacheConfig("mock", mock_client, *cache_conf_elem,
true));
@@ -165,13 +199,27 @@ public:
// Load the data into the zone table.
if (enabled) {
- boost::scoped_ptr<memory::ZoneWriter> writer(
- dsrc_info.ztable_segment_->getZoneWriter(
- cache_conf->getLoadAction(rrclass_, zone),
- zone, rrclass_));
- writer->load();
- writer->install();
- writer->cleanup(); // not absolutely necessary, but just in case
+ const ConstElementPtr config_ztable_segment(
+ Element::fromJSON("{\"mapped-file\": \"" +
+ getMappedFilename(index) +
+ "\"}"));
+
+ GetParam()->reset(*list_, dsrc_info.name_,
+ memory::ZoneTableSegment::CREATE,
+ config_ztable_segment);
+
+ const ConfigurableClientList::ZoneWriterPair result =
+ list_->getCachedZoneWriter(zone, false, dsrc_info.name_);
+
+ ASSERT_EQ(ConfigurableClientList::ZONE_SUCCESS, result.first);
+ result.second->load();
+ result.second->install();
+ // not absolutely necessary, but just in case
+ result.second->cleanup();
+
+ GetParam()->reset(*list_, dsrc_info.name_,
+ memory::ZoneTableSegment::READ_WRITE,
+ config_ztable_segment);
}
// On completion of load revert to the previous state of underlying
@@ -181,7 +229,7 @@ public:
}
// Check the positive result is as we expect it.
void positiveResult(const ClientList::FindResult& result,
- const shared_ptr<MockDataSourceClient>& dsrc,
+ const boost::shared_ptr<MockDataSourceClient>& dsrc,
const Name& name, bool exact,
const char* test, bool from_cache = false)
{
@@ -193,10 +241,10 @@ public:
// alive, even when we don't know what it is.
// Any better idea how to test it actually keeps the thing
// alive?
- EXPECT_NE(shared_ptr<ClientList::FindResult::LifeKeeper>(),
+ EXPECT_NE(boost::shared_ptr<ClientList::FindResult::LifeKeeper>(),
result.life_keeper_);
if (from_cache) {
- EXPECT_NE(shared_ptr<InMemoryZoneFinder>(),
+ EXPECT_NE(boost::shared_ptr<InMemoryZoneFinder>(),
boost::dynamic_pointer_cast<InMemoryZoneFinder>(
result.finder_)) << "Finder is not from cache";
EXPECT_TRUE(NULL !=
@@ -205,6 +253,18 @@ public:
EXPECT_EQ(dsrc.get(), result.dsrc_client_);
}
}
+
+ // check the result with empty (broken) zones. Right now this can only
+ // happen for in-memory caches.
+ void emptyResult(const ClientList::FindResult& result, bool exact,
+ const char* trace_txt)
+ {
+ SCOPED_TRACE(trace_txt);
+ ASSERT_FALSE(result.finder_);
+ EXPECT_EQ(exact, result.exact_match_);
+ EXPECT_TRUE(dynamic_cast<InMemoryClient*>(result.dsrc_client_));
+ }
+
// Configure the list with multiple data sources, according to
// some configuration. It uses the index as parameter, to be able to
// loop through the configurations.
@@ -247,19 +307,91 @@ public:
EXPECT_EQ(type, ds->type_);
EXPECT_TRUE(Element::fromJSON(params)->equals(*ds->configuration_));
EXPECT_EQ(cache, list_->getDataSources()[index].cache_ !=
- shared_ptr<InMemoryClient>());
+ boost::shared_ptr<InMemoryClient>());
}
+ ConfigurableClientList::CacheStatus doReload(
+ const Name& origin, const string& datasrc_name = "");
+ void accessorIterate(const ConstZoneTableAccessorPtr& accessor,
+ int numZones, const string& zoneName);
+
const RRClass rrclass_;
- shared_ptr<TestedList> list_;
+ boost::shared_ptr<TestedList> list_;
const ClientList::FindResult negative_result_;
- vector<shared_ptr<MockDataSourceClient> > ds_;
+ vector<boost::shared_ptr<MockDataSourceClient> > ds_;
vector<ConfigurableClientList::DataSourceInfo> ds_info_;
const ConstElementPtr config_elem_, config_elem_zones_;
- shared_ptr<ZoneTableSegment> ztable_segment_;
};
+class LocalSegmentType : public SegmentType {
+public:
+ virtual ConstElementPtr getCacheConfig(bool enabled, const Name& zone)
+ const
+ {
+ return (Element::fromJSON("{\"type\": \"mock\","
+ " \"cache-enable\": " +
+ string(enabled ? "true," : "false,") +
+ " \"cache-zones\": "
+ " [\"" + zone.toText() + "\"]}"));
+ }
+ virtual void reset(ConfigurableClientList&, const std::string&,
+ ZoneTableSegment::MemorySegmentOpenMode,
+ ConstElementPtr) {
+ // We must not call reset on local ZoneTableSegments.
+ }
+ virtual std::string getType() {
+ return ("local");
+ }
+};
+
+LocalSegmentType local_segment_type;
+
+INSTANTIATE_TEST_CASE_P(ListTestLocal, ListTest,
+ ::testing::Values(static_cast<SegmentType*>(
+ &local_segment_type)));
+
+#ifdef USE_SHARED_MEMORY
+
+class MappedSegmentType : public SegmentType {
+public:
+ virtual ConstElementPtr getCacheConfig(bool enabled, const Name& zone)
+ const
+ {
+ return (Element::fromJSON("{\"type\": \"mock\","
+ " \"cache-enable\": " +
+ string(enabled ? "true," : "false,") +
+ " \"cache-type\": \"mapped\"," +
+ " \"cache-zones\": "
+ " [\"" + zone.toText() + "\"]}"));
+ }
+ virtual void reset(ConfigurableClientList& list,
+ const std::string& datasrc_name,
+ ZoneTableSegment::MemorySegmentOpenMode mode,
+ ConstElementPtr config_params) {
+ EXPECT_TRUE(list.resetMemorySegment(datasrc_name, mode,
+ config_params));
+ }
+ virtual std::string getType() {
+ return ("mapped");
+ }
+};
+
+MappedSegmentType mapped_segment_type;
+
+INSTANTIATE_TEST_CASE_P(ListTestMapped, ListTest,
+ ::testing::Values(static_cast<SegmentType*>(
+ &mapped_segment_type)));
+
+#endif
+
+// Calling reset on empty list finds no data and returns false.
+TEST_P(ListTest, emptyReset) {
+ EXPECT_FALSE(list_->resetMemorySegment("Something",
+ memory::ZoneTableSegment::CREATE,
+ Element::create()));
+}
+
// Test the test itself
-TEST_F(ListTest, selfTest) {
+TEST_P(ListTest, selfTest) {
EXPECT_EQ(result::SUCCESS, ds_[0]->findZone(Name("example.org")).code);
EXPECT_EQ(result::PARTIALMATCH,
ds_[0]->findZone(Name("sub.example.org")).code);
@@ -268,19 +400,19 @@ TEST_F(ListTest, selfTest) {
EXPECT_EQ(result::NOTFOUND, ds_[0]->findZone(Name("aaa")).code);
EXPECT_EQ(result::NOTFOUND, ds_[0]->findZone(Name("zzz")).code);
// Nothing to keep alive here.
- EXPECT_EQ(shared_ptr<ClientList::FindResult::LifeKeeper>(),
+ EXPECT_EQ(boost::shared_ptr<ClientList::FindResult::LifeKeeper>(),
negative_result_.life_keeper_);
}
// Test the list we create with empty configuration is, in fact, empty
-TEST_F(ListTest, emptyList) {
+TEST_P(ListTest, emptyList) {
EXPECT_TRUE(list_->getDataSources().empty());
}
// Check the values returned by a find on an empty list. It should be
// a negative answer (nothing found) no matter if we want an exact or inexact
// match.
-TEST_F(ListTest, emptySearch) {
+TEST_P(ListTest, emptySearch) {
// No matter what we try, we don't get an answer.
// Note: we don't have operator<< for the result class, so we cannot use
@@ -297,7 +429,7 @@ TEST_F(ListTest, emptySearch) {
// Put a single data source inside the list and check it can find an
// exact match if there's one.
-TEST_F(ListTest, singleDSExactMatch) {
+TEST_P(ListTest, singleDSExactMatch) {
list_->getDataSources().push_back(ds_info_[0]);
// This zone is not there
EXPECT_TRUE(negative_result_ == list_->find(Name("org."), true));
@@ -311,7 +443,7 @@ TEST_F(ListTest, singleDSExactMatch) {
}
// When asking for a partial match, we get all that the exact one, but more.
-TEST_F(ListTest, singleDSBestMatch) {
+TEST_P(ListTest, singleDSBestMatch) {
list_->getDataSources().push_back(ds_info_[0]);
// This zone is not there
EXPECT_TRUE(negative_result_ == list_->find(Name("org.")));
@@ -331,7 +463,7 @@ const char* const test_names[] = {
"With a duplicity"
};
-TEST_F(ListTest, multiExactMatch) {
+TEST_P(ListTest, multiExactMatch) {
// Run through all the multi-configurations
for (size_t i(0); i < sizeof(test_names) / sizeof(*test_names); ++i) {
SCOPED_TRACE(test_names[i]);
@@ -350,7 +482,7 @@ TEST_F(ListTest, multiExactMatch) {
}
}
-TEST_F(ListTest, multiBestMatch) {
+TEST_P(ListTest, multiBestMatch) {
// Run through all the multi-configurations
for (size_t i(0); i < 4; ++ i) {
SCOPED_TRACE(test_names[i]);
@@ -371,7 +503,7 @@ TEST_F(ListTest, multiBestMatch) {
}
// Check the configuration is empty when the list is empty
-TEST_F(ListTest, configureEmpty) {
+TEST_P(ListTest, configureEmpty) {
const ConstElementPtr elem(new ListElement);
list_->configure(elem, true);
EXPECT_TRUE(list_->getDataSources().empty());
@@ -380,7 +512,7 @@ TEST_F(ListTest, configureEmpty) {
}
// Check we can get multiple data sources and they are in the right order.
-TEST_F(ListTest, configureMulti) {
+TEST_P(ListTest, configureMulti) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"type1\","
@@ -402,7 +534,7 @@ TEST_F(ListTest, configureMulti) {
}
// Check we can pass whatever we want to the params
-TEST_F(ListTest, configureParams) {
+TEST_P(ListTest, configureParams) {
const char* params[] = {
"true",
"false",
@@ -427,7 +559,7 @@ TEST_F(ListTest, configureParams) {
}
}
-TEST_F(ListTest, status) {
+TEST_P(ListTest, status) {
EXPECT_TRUE(list_->getStatus().empty());
const ConstElementPtr elem(Element::fromJSON("["
"{"
@@ -454,7 +586,7 @@ TEST_F(ListTest, status) {
EXPECT_EQ("local", statuses[1].getSegmentType());
}
-TEST_F(ListTest, wrongConfig) {
+TEST_P(ListTest, wrongConfig) {
const char* configs[] = {
// A lot of stuff missing from there
"[{\"type\": \"test_type\", \"params\": 13}, {}]",
@@ -552,7 +684,7 @@ TEST_F(ListTest, wrongConfig) {
}
// The param thing defaults to null. Cache is not used yet.
-TEST_F(ListTest, defaults) {
+TEST_P(ListTest, defaults) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"type1\""
@@ -563,7 +695,7 @@ TEST_F(ListTest, defaults) {
}
// Check we can call the configure multiple times, to change the configuration
-TEST_F(ListTest, reconfigure) {
+TEST_P(ListTest, reconfigure) {
const ConstElementPtr empty(new ListElement);
list_->configure(config_elem_, true);
checkDS(0, "test_type", "{}", false);
@@ -574,7 +706,7 @@ TEST_F(ListTest, reconfigure) {
}
// Make sure the data source error exception from the factory is propagated
-TEST_F(ListTest, dataSrcError) {
+TEST_P(ListTest, dataSrcError) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"error\""
@@ -585,8 +717,37 @@ TEST_F(ListTest, dataSrcError) {
checkDS(0, "test_type", "{}", false);
}
+// In case of library errors, the rest of the data sources should be
+// unaffected.
+TEST_P(ListTest, dataSrcLibraryError) {
+ EXPECT_EQ(0, list_->getDataSources().size());
+ const ConstElementPtr elem(Element::fromJSON("["
+ "{"
+ " \"type\": \"type1\","
+ " \"cache-enable\": false,"
+ " \"params\": {}"
+ "},"
+ "{"
+ " \"type\": \"library_error\","
+ " \"cache-enable\": false,"
+ " \"params\": {}"
+ "},"
+ "{"
+ " \"type\": \"type2\","
+ " \"cache-enable\": false,"
+ " \"params\": {}"
+ "}]"
+ ));
+ list_->configure(elem, true);
+ EXPECT_EQ(2, list_->getDataSources().size());
+ checkDS(0, "type1", "{}", false);
+ checkDS(1, "type2", "{}", false);
+ // Check the exact configuration is preserved
+ EXPECT_EQ(elem, list_->getConfiguration());
+}
+
// Check we can get the cache
-TEST_F(ListTest, configureCacheEmpty) {
+TEST_P(ListTest, configureCacheEmpty) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"type1\","
@@ -608,7 +769,7 @@ TEST_F(ListTest, configureCacheEmpty) {
}
// But no cache if we disallow it globally
-TEST_F(ListTest, configureCacheDisabled) {
+TEST_P(ListTest, configureCacheDisabled) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"type1\","
@@ -630,7 +791,7 @@ TEST_F(ListTest, configureCacheDisabled) {
}
// Put some zones into the cache
-TEST_F(ListTest, cacheZones) {
+TEST_P(ListTest, cacheZones) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"type1\","
@@ -642,7 +803,8 @@ TEST_F(ListTest, cacheZones) {
checkDS(0, "type1", "[\"example.org\", \"example.com\", \"exmaple.cz\"]",
true);
- const shared_ptr<InMemoryClient> cache(list_->getDataSources()[0].cache_);
+ const boost::shared_ptr<InMemoryClient> cache(
+ list_->getDataSources()[0].cache_);
EXPECT_EQ(2, cache->getZoneCount());
EXPECT_EQ(result::SUCCESS, cache->findZone(Name("example.org")).code);
@@ -664,20 +826,24 @@ TEST_F(ListTest, cacheZones) {
// Check the caching handles misbehaviour from the data source and
// misconfiguration gracefully
-TEST_F(ListTest, badCache) {
+TEST_P(ListTest, badCache) {
list_->configure(config_elem_, true);
checkDS(0, "test_type", "{}", false);
- // First, the zone is not in the data source
+ // First, the zone is not in the data source. configure() should still
+ // succeed, and the existence zone should be cached.
const ConstElementPtr elem1(Element::fromJSON("["
"{"
- " \"type\": \"type1\","
+ " \"type\": \"test_type\","
" \"cache-enable\": true,"
- " \"cache-zones\": [\"example.org\"],"
- " \"params\": []"
+ " \"cache-zones\": [\"example.org\", \"example.com\"],"
+ " \"params\": [\"example.org\"]"
"}]"));
- EXPECT_THROW(list_->configure(elem1, true),
- ConfigurableClientList::ConfigurationError);
- checkDS(0, "test_type", "{}", false);
+ list_->configure(elem1, true); // shouldn't cause disruption
+ checkDS(0, "test_type", "[\"example.org\"]", true);
+ const boost::shared_ptr<InMemoryClient> cache(
+ list_->getDataSources()[0].cache_);
+ EXPECT_EQ(1, cache->getZoneCount());
+ EXPECT_EQ(result::SUCCESS, cache->findZone(Name("example.org")).code);
// Now, the zone doesn't give an iterator
const ConstElementPtr elem2(Element::fromJSON("["
"{"
@@ -687,7 +853,7 @@ TEST_F(ListTest, badCache) {
" \"params\": [\"noiter.org\"]"
"}]"));
EXPECT_THROW(list_->configure(elem2, true), isc::NotImplemented);
- checkDS(0, "test_type", "{}", false);
+ checkDS(0, "test_type", "[\"example.org\"]", true);
// Now, the zone returns NULL iterator
const ConstElementPtr elem3(Element::fromJSON("["
"{"
@@ -697,7 +863,7 @@ TEST_F(ListTest, badCache) {
" \"params\": [\"null.org\"]"
"}]"));
EXPECT_THROW(list_->configure(elem3, true), isc::Unexpected);
- checkDS(0, "test_type", "{}", false);
+ checkDS(0, "test_type", "[\"example.org\"]", true);
// The autodetection of zones is not enabled
const ConstElementPtr elem4(Element::fromJSON("["
"{"
@@ -706,10 +872,39 @@ TEST_F(ListTest, badCache) {
" \"params\": [\"example.org\"]"
"}]"));
EXPECT_THROW(list_->configure(elem4, true), isc::NotImplemented);
- checkDS(0, "test_type", "{}", false);
+ checkDS(0, "test_type", "[\"example.org\"]", true);
}
-TEST_F(ListTest, masterFiles) {
+// This test relies on the property of mapped type of cache.
+TEST_P(ListTest,
+#ifdef USE_SHARED_MEMORY
+ cacheInNonWritableSegment
+#else
+ DISABLED_cacheInNonWritableSegment
+#endif
+ )
+{
+ // Initializing data source with non writable zone table memory segment
+ // is possible. Loading is just postponed
+ const ConstElementPtr elem(Element::fromJSON("["
+ "{"
+ " \"type\": \"test_type\","
+ " \"cache-enable\": true,"
+ " \"cache-type\": \"mapped\","
+ " \"cache-zones\": [\"example.org\"],"
+ " \"params\": [\"example.org\"]"
+ "}]"));
+ list_->configure(elem, true); // no disruption
+ checkDS(0, "test_type", "[\"example.org\"]", true);
+ const boost::shared_ptr<InMemoryClient> cache(
+ list_->getDataSources()[0].cache_);
+
+ // Likewise, reload attempt will fail.
+ EXPECT_EQ(ConfigurableClientList::CACHE_NOT_WRITABLE,
+ doReload(Name("example.org")));
+}
+
+TEST_P(ListTest, masterFiles) {
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"MasterFiles\","
@@ -725,7 +920,7 @@ TEST_F(ListTest, masterFiles) {
list_->getDataSources()[0].data_src_client_);
// And it can search
- positiveResult(list_->find(Name(".")), ds_[0], Name("."), true, "com",
+ positiveResult(list_->find(Name(".")), ds_[0], Name("."), true, "root",
true);
// If cache is not enabled, nothing is loaded
@@ -734,7 +929,7 @@ TEST_F(ListTest, masterFiles) {
}
// Test the names are set correctly and collission is detected.
-TEST_F(ListTest, names) {
+TEST_P(ListTest, names) {
// Explicit name
const ConstElementPtr elem1(Element::fromJSON("["
"{"
@@ -781,9 +976,10 @@ TEST_F(ListTest, names) {
ConfigurableClientList::ConfigurationError);
}
-TEST_F(ListTest, BadMasterFile) {
+TEST_P(ListTest, BadMasterFile) {
// Configuration should succeed, and the good zones in the list
- // below should be loaded. No bad zones should be loaded.
+ // below should be loaded. Bad zones won't be "loaded" in its usual sense,
+ // but are still recognized with conceptual "empty" data.
const ConstElementPtr elem(Element::fromJSON("["
"{"
" \"type\": \"MasterFiles\","
@@ -819,40 +1015,25 @@ TEST_F(ListTest, BadMasterFile) {
positiveResult(list_->find(Name("example.com."), true), ds_[0],
Name("example.com."), true, "example.com", true);
- EXPECT_TRUE(negative_result_ == list_->find(Name("example.org."), true));
- EXPECT_TRUE(negative_result_ == list_->find(Name("foo.bar"), true));
- EXPECT_TRUE(negative_result_ == list_->find(Name("example.net."), true));
- EXPECT_TRUE(negative_result_ == list_->find(Name("example.edu."), true));
- EXPECT_TRUE(negative_result_ == list_->find(Name("example.info."), true));
+ // Bad cases: should result in "empty zone", whether the match is exact
+ // or partial.
+ emptyResult(list_->find(Name("foo.bar"), true), true, "foo.bar");
+ emptyResult(list_->find(Name("example.net."), true), true, "example.net");
+ emptyResult(list_->find(Name("example.edu."), true), true, "example.edu");
+ emptyResult(list_->find(Name("example.info."), true), true,
+ "example.info");
+ emptyResult(list_->find(Name("www.example.edu."), false), false,
+ "example.edu, partial");
positiveResult(list_->find(Name(".")), ds_[0], Name("."), true, "root",
true);
+ // This one simply doesn't exist.
+ EXPECT_TRUE(list_->find(Name("example.org."), true) == negative_result_);
}
-// This allows us to test two versions of the reloading code
-// (One by calling reload(), one by obtaining a ZoneWriter and
-// playing with that). Once we deprecate reload(), we should revert this
-// change and not use typed tests any more.
-template<class UpdateType>
-class ReloadTest : public ListTest {
-public:
- ConfigurableClientList::ReloadResult doReload(const Name& origin);
-};
-
-// Version with calling reload()
-class ReloadUpdateType {};
-template<>
-ConfigurableClientList::ReloadResult
-ReloadTest<ReloadUpdateType>::doReload(const Name& origin) {
- return (list_->reload(origin));
-};
-
-// Version with the ZoneWriter
-class WriterUpdateType {};
-template<>
-ConfigurableClientList::ReloadResult
-ReloadTest<WriterUpdateType>::doReload(const Name& origin) {
+ConfigurableClientList::CacheStatus
+ListTest::doReload(const Name& origin, const string& datasrc_name) {
ConfigurableClientList::ZoneWriterPair
- result(list_->getCachedZoneWriter(origin));
+ result(list_->getCachedZoneWriter(origin, false, datasrc_name));
if (result.first == ConfigurableClientList::ZONE_SUCCESS) {
// Can't use ASSERT_NE here, it would want to return(), which
// it can't in non-void function.
@@ -865,157 +1046,241 @@ ReloadTest<WriterUpdateType>::doReload(const Name& origin) {
"but the writer is NULL";
}
} else {
- EXPECT_EQ(static_cast<memory::ZoneWriter*>(NULL),
- result.second.get());
+ EXPECT_EQ(static_cast<memory::ZoneWriter*>(NULL), result.second.get());
}
return (result.first);
}
-// Typedefs for the GTEST guts to make it work
-typedef ::testing::Types<ReloadUpdateType, WriterUpdateType> UpdateTypes;
-TYPED_TEST_CASE(ReloadTest, UpdateTypes);
+// Check that ZoneWriter doesn't throw when asked not to
+TEST_P(ListTest, checkZoneWriterCatchesExceptions) {
+ const ConstElementPtr config_elem_zones_(Element::fromJSON("["
+ "{"
+ " \"type\": \"MasterFiles\","
+ " \"params\": {"
+ " \"example.edu\": \"" TEST_DATA_DIR "example.edu-broken\""
+ " },"
+ " \"cache-enable\": true"
+ "}]"));
+
+ list_->configure(config_elem_zones_, true);
+ ConfigurableClientList::ZoneWriterPair
+ result(list_->getCachedZoneWriter(Name("example.edu"), true));
+ ASSERT_EQ(ConfigurableClientList::ZONE_SUCCESS, result.first);
+ ASSERT_TRUE(result.second);
+
+ std::string error_msg;
+ // Because of the way we called getCachedZoneWriter() with
+ // catch_load_error=true, the following should not throw and must
+ // return an error message in error_msg.
+ EXPECT_NO_THROW(result.second->load(&error_msg));
+ EXPECT_FALSE(error_msg.empty());
+ result.second->cleanup();
+}
+
+// Check that ZoneWriter throws when asked to
+TEST_P(ListTest, checkZoneWriterThrows) {
+ const ConstElementPtr config_elem_zones_(Element::fromJSON("["
+ "{"
+ " \"type\": \"MasterFiles\","
+ " \"params\": {"
+ " \"example.edu\": \"" TEST_DATA_DIR "example.edu-broken\""
+ " },"
+ " \"cache-enable\": true"
+ "}]"));
+
+ list_->configure(config_elem_zones_, true);
+ ConfigurableClientList::ZoneWriterPair
+ result(list_->getCachedZoneWriter(Name("example.edu"), false));
+ ASSERT_EQ(ConfigurableClientList::ZONE_SUCCESS, result.first);
+ ASSERT_TRUE(result.second);
+
+ std::string error_msg;
+ // Because of the way we called getCachedZoneWriter() with
+ // catch_load_error=false, the following should throw and must not
+ // modify error_msg.
+ EXPECT_THROW(result.second->load(&error_msg),
+ isc::datasrc::ZoneLoaderException);
+ EXPECT_TRUE(error_msg.empty());
+ result.second->cleanup();
+}
// Test we can reload a zone
-TYPED_TEST(ReloadTest, reloadSuccess) {
- this->list_->configure(this->config_elem_zones_, true);
+TEST_P(ListTest, reloadSuccess) {
+ list_->configure(config_elem_zones_, true);
+
+ const vector<DataSourceStatus> statii_before(list_->getStatus());
+ ASSERT_EQ(1, statii_before.size());
+ EXPECT_EQ("test_type", statii_before[0].getName());
+ EXPECT_EQ(SEGMENT_UNUSED, statii_before[0].getSegmentState());
+ EXPECT_THROW(statii_before[0].getSegmentType(), isc::InvalidOperation);
+
const Name name("example.org");
- this->prepareCache(0, name);
+ prepareCache(0, name);
// The cache currently contains a tweaked version of zone, which
// doesn't have "tstzonedata" A record. So the lookup should result
// in NXDOMAIN.
EXPECT_EQ(ZoneFinder::NXDOMAIN,
- this->list_->find(name).finder_->
+ list_->find(name).finder_->
find(Name("tstzonedata").concatenate(name),
RRType::A())->code);
// Now reload the full zone. It should be there now.
- EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS, this->doReload(name));
+ EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS, doReload(name));
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->
+ list_->find(name).finder_->
find(Name("tstzonedata").concatenate(name),
RRType::A())->code);
+
+ const vector<DataSourceStatus> statii_after(list_->getStatus());
+ ASSERT_EQ(1, statii_after.size());
+ EXPECT_EQ("test_type", statii_after[0].getName());
+ EXPECT_EQ(SEGMENT_INUSE, statii_after[0].getSegmentState());
+ EXPECT_EQ(GetParam()->getType(), statii_after[0].getSegmentType());
}
// The cache is not enabled. The load should be rejected.
-TYPED_TEST(ReloadTest, reloadNotAllowed) {
- this->list_->configure(this->config_elem_zones_, false);
+//
+// FIXME: This test is broken by #2853 and needs to be fixed or
+// removed. Please see #2991 for details.
+TEST_P(ListTest, DISABLED_reloadNotAllowed) {
+ list_->configure(config_elem_zones_, false);
const Name name("example.org");
// We put the cache in even when not enabled. This won't confuse the thing.
- this->prepareCache(0, name);
+ prepareCache(0, name);
// See the reloadSuccess test. This should result in NXDOMAIN.
EXPECT_EQ(ZoneFinder::NXDOMAIN,
- this->list_->find(name).finder_->
+ list_->find(name).finder_->
find(Name("tstzonedata").concatenate(name),
RRType::A())->code);
// Now reload. It should reject it.
- EXPECT_EQ(ConfigurableClientList::CACHE_DISABLED, this->doReload(name));
+ EXPECT_EQ(ConfigurableClientList::CACHE_DISABLED, doReload(name));
// Nothing changed here
EXPECT_EQ(ZoneFinder::NXDOMAIN,
- this->list_->find(name).finder_->
+ list_->find(name).finder_->
find(Name("tstzonedata").concatenate(name),
RRType::A())->code);
}
// Similar to the previous case, but the cache is disabled in config.
-TYPED_TEST(ReloadTest, reloadNotEnabled) {
- this->list_->configure(this->config_elem_zones_, true);
+TEST_P(ListTest, reloadNotEnabled) {
+ list_->configure(config_elem_zones_, true);
const Name name("example.org");
// We put the cache, actually disabling it.
- this->prepareCache(0, name, false);
+ prepareCache(0, name, false);
// In this case we cannot really look up due to the limitation of
// the mock implementation. We only check reload fails.
- EXPECT_EQ(ConfigurableClientList::ZONE_NOT_CACHED, this->doReload(name));
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_CACHED, doReload(name));
}
// Test several cases when the zone does not exist
-TYPED_TEST(ReloadTest, reloadNoSuchZone) {
- this->list_->configure(this->config_elem_zones_, true);
+TEST_P(ListTest, reloadNoSuchZone) {
+ list_->configure(config_elem_zones_, true);
const Name name("example.org");
// We put the cache in even when not enabled. This won't confuse the
// reload method, as that one looks at the real state of things, not
// at the configuration.
- this->prepareCache(0, Name("example.com"));
+ prepareCache(0, Name("example.com"));
// Not in the data sources
EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
- this->doReload(Name("exmaple.cz")));
- // Not cached
- EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND, this->doReload(name));
+ doReload(Name("exmaple.cz")));
+ // If it's not configured to be cached, it won't be reloaded.
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_CACHED, doReload(name));
// Partial match
EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
- this->doReload(Name("sub.example.com")));
+ doReload(Name("sub.example.com")));
// Nothing changed here - these zones don't exist
EXPECT_EQ(static_cast<isc::datasrc::DataSourceClient*>(NULL),
- this->list_->find(name).dsrc_client_);
+ list_->find(name).dsrc_client_);
EXPECT_EQ(static_cast<isc::datasrc::DataSourceClient*>(NULL),
- this->list_->find(Name("example.cz")).dsrc_client_);
+ list_->find(Name("example.cz")).dsrc_client_);
EXPECT_EQ(static_cast<isc::datasrc::DataSourceClient*>(NULL),
- this->list_->find(Name("sub.example.com"), true).dsrc_client_);
+ list_->find(Name("sub.example.com"), true).dsrc_client_);
// Not reloaded, so A record shouldn't be visible yet.
EXPECT_EQ(ZoneFinder::NXDOMAIN,
- this->list_->find(Name("example.com")).finder_->
+ list_->find(Name("example.com")).finder_->
find(Name("tstzonedata.example.com"),
RRType::A())->code);
}
-// Check we gracefuly throw an exception when a zone disappeared in
-// the underlying data source when we want to reload it
-TYPED_TEST(ReloadTest, reloadZoneGone) {
- this->list_->configure(this->config_elem_zones_, true);
+// Check we gracefully reject reloading (i.e. no exception) when a zone
+// disappeared in the underlying data source when we want to reload it
+TEST_P(ListTest, reloadZoneGone) {
+ list_->configure(config_elem_zones_, true);
const Name name("example.org");
// We put in a cache for non-existent zone. This emulates being loaded
// and then the zone disappearing. We prefill the cache, so we can check
// it.
- this->prepareCache(0, name);
+ prepareCache(0, name);
// The (cached) zone contains zone's SOA
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
// Remove the zone from the data source.
static_cast<MockDataSourceClient*>(
- this->list_->getDataSources()[0].data_src_client_)->eraseZone(name);
+ list_->getDataSources()[0].data_src_client_)->eraseZone(name);
- // The zone is not there, so abort the reload.
- EXPECT_THROW(this->doReload(name), DataSourceError);
+ // The zone is not there, so reload doesn't take place.
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND, doReload(name));
// The (cached) zone is not hurt.
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
+}
+
+TEST_P(ListTest, reloadNewZone) {
+ // Test the case where a zone to be cached originally doesn't exist
+ // in the underlying data source and is added later. reload() will
+ // succeed once it's available in the data source.
+ const ConstElementPtr elem(Element::fromJSON("["
+ "{"
+ " \"type\": \"test_type\","
+ " \"cache-enable\": true,"
+ " \"cache-zones\": [\"example.org\", \"example.com\"],"
+ " \"params\": [\"example.org\"]"
+ "}]"));
+ list_->configure(elem, true);
+ checkDS(0, "test_type", "[\"example.org\"]", true); // no example.com
+
+ // We can't reload it either
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
+ doReload(Name("example.com")));
+
+ // If we add the zone, we can now reload it
+ EXPECT_TRUE(static_cast<MockDataSourceClient*>(
+ list_->getDataSources()[0].data_src_client_)->
+ insertZone(Name("example.com")));
+ EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS,
+ doReload(Name("example.com")));
}
// The underlying data source throws. Check we don't modify the state.
-TYPED_TEST(ReloadTest, reloadZoneThrow) {
- this->list_->configure(this->config_elem_zones_, true);
+TEST_P(ListTest, reloadZoneThrow) {
+ list_->configure(config_elem_zones_, true);
const Name name("noiter.org");
- this->prepareCache(0, name);
+ prepareCache(0, name);
// The zone contains stuff now
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
// The iterator throws, so abort the reload.
- EXPECT_THROW(this->doReload(name), isc::NotImplemented);
+ EXPECT_THROW(doReload(name), isc::NotImplemented);
// The zone is not hurt.
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
}
-TYPED_TEST(ReloadTest, reloadNullIterator) {
- this->list_->configure(this->config_elem_zones_, true);
+TEST_P(ListTest, reloadNullIterator) {
+ list_->configure(config_elem_zones_, true);
const Name name("null.org");
- this->prepareCache(0, name);
+ prepareCache(0, name);
// The zone contains stuff now
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
// The iterator throws, so abort the reload.
- EXPECT_THROW(this->doReload(name), isc::Unexpected);
+ EXPECT_THROW(doReload(name), isc::Unexpected);
// The zone is not hurt.
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(name).finder_->find(name,
- RRType::SOA())->code);
+ list_->find(name).finder_->find(name, RRType::SOA())->code);
}
// Test we can reload the master files too (special-cased)
-TYPED_TEST(ReloadTest, reloadMasterFile) {
+TEST_P(ListTest, reloadMasterFile) {
const char* const install_cmd = INSTALL_PROG " -c " TEST_DATA_DIR
"/root.zone " TEST_DATA_BUILDDIR "/root.zone.copied";
if (system(install_cmd) != 0) {
@@ -1033,21 +1298,155 @@ TYPED_TEST(ReloadTest, reloadMasterFile) {
" \".\": \"" TEST_DATA_BUILDDIR "/root.zone.copied\""
" }"
"}]"));
- this->list_->configure(elem, true);
+ list_->configure(elem, true);
// Add a record that is not in the zone
EXPECT_EQ(ZoneFinder::NXDOMAIN,
- this->list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
- RRType::TXT())->code);
+ list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
+ RRType::TXT())->code);
ofstream f;
f.open(TEST_DATA_BUILDDIR "/root.zone.copied", ios::out | ios::app);
f << "nosuchdomain.\t\t3600\tIN\tTXT\ttest" << std::endl;
f.close();
// Do the reload.
- EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS, this->doReload(Name(".")));
+ EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS, doReload(Name(".")));
// It is here now.
EXPECT_EQ(ZoneFinder::SUCCESS,
- this->list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
- RRType::TXT())->code);
+ list_->find(Name(".")).finder_->find(Name("nosuchdomain"),
+ RRType::TXT())->code);
+}
+
+TEST_P(ListTest, reloadByDataSourceName) {
+ // We use three data sources (and their clients). 2nd and 3rd have
+ // the same name of the zones.
+ const ConstElementPtr config_elem = Element::fromJSON(
+ "[{\"type\": \"test_type1\", \"params\": [\"example.org\"]},"
+ " {\"type\": \"test_type2\", \"params\": [\"example.com\"]},"
+ " {\"type\": \"test_type3\", \"params\": [\"example.com\"]}]");
+ list_->configure(config_elem, true);
+ // Prepare in-memory cache for the 1st and 2nd data sources.
+ prepareCache(0, Name("example.org"));
+ prepareCache(1, Name("example.com"));
+
+ // Normal case: both zone name and data source name matches.
+ // See the reloadSuccess test about the NXDOMAIN/SUCCESS checks.
+ EXPECT_EQ(ZoneFinder::NXDOMAIN,
+ list_->find(Name("tstzonedata.example.com")).finder_->
+ find(Name("tstzonedata.example.com"), RRType::A())->code);
+ EXPECT_EQ(ConfigurableClientList::ZONE_SUCCESS,
+ doReload(Name("example.com"), "test_type2"));
+ EXPECT_EQ(ZoneFinder::SUCCESS,
+ list_->find(Name("tstzonedata.example.com")).finder_->
+ find(Name("tstzonedata.example.com"), RRType::A())->code);
+
+ // The specified zone exists in the first entry of the list, but a
+ // different data source name is specified (in which the specified zone
+ // doesn't exist), so reloading should fail, and the cache status of the
+ // first data source shouldn't change.
+ EXPECT_EQ(ZoneFinder::NXDOMAIN,
+ list_->find(Name("tstzonedata.example.org")).finder_->
+ find(Name("tstzonedata.example.org"), RRType::A())->code);
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_FOUND,
+ doReload(Name("example.org"), "test_type2"));
+ EXPECT_EQ(ZoneFinder::NXDOMAIN,
+ list_->find(Name("tstzonedata.example.org")).finder_->
+ find(Name("tstzonedata.example.org"), RRType::A())->code);
+
+ // Likewise, if a specific data source is given, normal name matching
+ // isn't suppressed and the 3rd data source will be used. There cache
+ // is disabled, so reload should fail due to "not cached".
+ EXPECT_EQ(ConfigurableClientList::ZONE_NOT_CACHED,
+ doReload(Name("example.com"), "test_type3"));
+
+ // specified name of data source doesn't exist.
+ EXPECT_EQ(ConfigurableClientList::DATASRC_NOT_FOUND,
+ doReload(Name("example.org"), "test_type4"));
+}
+
+// This takes the accessor provided by getZoneTableAccessor(), iterates
+// through the table, and verifies that the expected number of zones are
+// present, as well as the named zone.
+void
+ListTest::accessorIterate(const ConstZoneTableAccessorPtr& accessor,
+ int numZones, const string& zoneName="")
+{
+ // Confirm basic iterator behavior.
+ ASSERT_TRUE(accessor);
+ ZoneTableAccessor::IteratorPtr it = accessor->getIterator();
+ ASSERT_TRUE(it);
+ // Iterator does not guarantee ordering, so we look for the target
+ // name anywhere in the table.
+ bool found = false;
+ int i;
+ for (i = 0; !it->isLast(); ++i, it->next()) {
+ if (Name(zoneName) == it->getCurrent().origin) {
+ found = true;
+ }
+ }
+ EXPECT_EQ(i, numZones);
+ if (numZones > 0) {
+ EXPECT_TRUE(found);
+ }
+}
+
+TEST_F(ListTest, zoneTableAccessor) {
+ // empty configuration
+ const ConstElementPtr elem(new ListElement);
+ list_->configure(elem, true);
+ // null pointer treated as false
+ EXPECT_FALSE(list_->getZoneTableAccessor("", true));
+
+ // empty list; expect it to return an empty list
+ list_->configure(config_elem_, true);
+ ConstZoneTableAccessorPtr z(list_->getZoneTableAccessor("", true));
+ accessorIterate(z, 0);
+
+ const ConstElementPtr elem2(Element::fromJSON("["
+ "{"
+ " \"type\": \"type1\","
+ " \"cache-enable\": true,"
+ " \"cache-zones\": [\"example.com\"],"
+ " \"params\": [\"example.com\"]"
+ "},"
+ "{"
+ " \"type\": \"type2\","
+ " \"cache-enable\": false,"
+ " \"params\": [\"example.org\"]"
+ "},"
+ "{"
+ " \"type\": \"type3\","
+ " \"cache-enable\": true,"
+ " \"cache-zones\": [\"example.net\", \"example.info\"],"
+ " \"params\": [\"example.net\", \"example.info\"]"
+ "}]"));
+
+ // allow_cache = false
+ // ask for a non-existent zone table, expect null
+ list_->configure(elem2, false);
+ EXPECT_FALSE(list_->getZoneTableAccessor("bogus", true));
+ // ask for any zone table, expect an empty list
+ z = list_->getZoneTableAccessor("", true);
+ accessorIterate(z, 0);
+
+ // allow_cache = true, use_cache = false
+ list_->configure(elem2, true);
+ EXPECT_THROW(list_->getZoneTableAccessor("", false), isc::NotImplemented);
+ EXPECT_THROW(list_->getZoneTableAccessor("type1", false),
+ isc::NotImplemented);
+
+ // datasrc not found, returns NULL pointer
+ EXPECT_FALSE(list_->getZoneTableAccessor("bogus", true));
+
+ // return first datasrc
+ z = list_->getZoneTableAccessor("", true);
+ accessorIterate(z, 1, "example.com");
+
+ // datasrc has cache disabled, returns accessor to empty list
+ z = list_->getZoneTableAccessor("type2", true);
+ accessorIterate(z, 0);
+
+ // search by name
+ z = list_->getZoneTableAccessor("type3", true);
+ accessorIterate(z, 2, "example.net");
}
// Check the status holds data
@@ -1056,7 +1455,7 @@ TEST(DataSourceStatus, status) {
EXPECT_EQ("Test", status.getName());
EXPECT_EQ(SEGMENT_INUSE, status.getSegmentState());
EXPECT_EQ("local", status.getSegmentType());
- const DataSourceStatus status_unused("Unused", SEGMENT_UNUSED, "");
+ const DataSourceStatus status_unused("Unused");
EXPECT_EQ("Unused", status_unused.getName());
EXPECT_EQ(SEGMENT_UNUSED, status_unused.getSegmentState());
EXPECT_THROW(status_unused.getSegmentType(), isc::InvalidOperation);
diff --git a/src/lib/datasrc/tests/database_unittest.cc b/src/lib/datasrc/tests/database_unittest.cc
index 16b0627..e6a27b4 100644
--- a/src/lib/datasrc/tests/database_unittest.cc
+++ b/src/lib/datasrc/tests/database_unittest.cc
@@ -1137,6 +1137,9 @@ const char* TEST_NSEC3_RECORDS[][5] = {
};
DatabaseClientTest::DatabaseClientTest() :
+ // We need to initialize to something, and not being mock is safer
+ // until we know for sure.
+ is_mock_(false),
zname_("example.org"), qname_("www.example.org"),
qclass_(dns::RRClass::IN()),
qtype_(dns::RRType::A()),
@@ -1417,7 +1420,7 @@ TEST(GenericDatabaseClientTest, noAccessorException) {
// If the zone doesn't exist, exception is thrown
TEST_P(DatabaseClientTest, noZoneIterator) {
- EXPECT_THROW(client_->getIterator(Name("example.com")), DataSourceError);
+ EXPECT_THROW(client_->getIterator(Name("example.com")), NoSuchZone);
}
// If the zone doesn't exist and iteration is not implemented, it still throws
@@ -1427,7 +1430,7 @@ TEST(GenericDatabaseClientTest, noZoneNotImplementedIterator) {
boost::shared_ptr<DatabaseAccessor>(
new NopAccessor())).getIterator(
Name("example.com")),
- DataSourceError);
+ NoSuchZone);
}
TEST(GenericDatabaseClientTest, notImplementedIterator) {
diff --git a/src/lib/datasrc/tests/factory_unittest.cc b/src/lib/datasrc/tests/factory_unittest.cc
index 5a01a27..2708e0e 100644
--- a/src/lib/datasrc/tests/factory_unittest.cc
+++ b/src/lib/datasrc/tests/factory_unittest.cc
@@ -39,6 +39,8 @@ void
pathtestHelper(const std::string& file, const std::string& expected_error) {
std::string error;
try {
+ // cppcheck-suppress unusedScopedObject We just check if it throws
+ // to create, not use it. That's OK.
DataSourceClientContainer(file, ElementPtr());
} catch (const DataSourceLibraryError& dsle) {
error = dsle.what();
diff --git a/src/lib/datasrc/tests/memory/Makefile.am b/src/lib/datasrc/tests/memory/Makefile.am
index e0fc0f5..fd60af5 100644
--- a/src/lib/datasrc/tests/memory/Makefile.am
+++ b/src/lib/datasrc/tests/memory/Makefile.am
@@ -4,9 +4,18 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += -I$(top_builddir)/src/lib/dns -I$(top_srcdir)/src/lib/dns
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_srcdir)/testdata\"
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_builddir)\"
AM_CXXFLAGS = $(B10_CXXFLAGS)
+if USE_SHARED_MEMORY
+# See src/lib/util/Makefile.am. We need this because some tests directly
+# use the Boost managed_mapped_file. It's not ideal to deal with it as
+# a module-wide flag, but considering it's for tests and only for limited
+# environments, it would still be better to introduce more complicated setup.
+AM_CXXFLAGS += $(BOOST_MAPPED_FILE_CXXFLAG)
+endif
+
if USE_STATIC_LINK
AM_LDFLAGS = -static
endif
@@ -30,14 +39,19 @@ run_unittests_SOURCES += zone_table_unittest.cc
run_unittests_SOURCES += zone_data_unittest.cc
run_unittests_SOURCES += zone_finder_unittest.cc
run_unittests_SOURCES += ../../tests/faked_nsec3.h ../../tests/faked_nsec3.cc
-run_unittests_SOURCES += memory_segment_test.h
+run_unittests_SOURCES += memory_segment_mock.h
run_unittests_SOURCES += segment_object_holder_unittest.cc
run_unittests_SOURCES += memory_client_unittest.cc
run_unittests_SOURCES += rrset_collection_unittest.cc
run_unittests_SOURCES += zone_data_loader_unittest.cc
run_unittests_SOURCES += zone_data_updater_unittest.cc
-run_unittests_SOURCES += zone_table_segment_test.h
+run_unittests_SOURCES += zone_table_segment_mock.h
run_unittests_SOURCES += zone_table_segment_unittest.cc
+
+if USE_SHARED_MEMORY
+run_unittests_SOURCES += zone_table_segment_mapped_unittest.cc
+endif
+
run_unittests_SOURCES += zone_writer_unittest.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
diff --git a/src/lib/datasrc/tests/memory/domaintree_unittest.cc b/src/lib/datasrc/tests/memory/domaintree_unittest.cc
index 45e256a..7568f8d 100644
--- a/src/lib/datasrc/tests/memory/domaintree_unittest.cc
+++ b/src/lib/datasrc/tests/memory/domaintree_unittest.cc
@@ -17,6 +17,7 @@
#include <exceptions/exceptions.h>
#include <util/memory_segment_local.h>
+#include <util/random/random_number_generator.h>
#include <dns/name.h>
#include <dns/rrclass.h>
@@ -28,11 +29,19 @@
#include <dns/tests/unittest_util.h>
+#include <boost/format.hpp>
+
+#include <stdlib.h>
+
+#include <set>
+#include <algorithm>
+
using namespace std;
using namespace isc;
using namespace isc::dns;
using isc::UnitTestUtil;
using namespace isc::datasrc::memory;
+using isc::util::random::UniformRandomIntegerGenerator;
// XXX: some compilers cannot find class static constants used in
// EXPECT_xxx macros, for which we need an explicit empty definition.
@@ -59,6 +68,53 @@ const size_t Name::MAX_LABELS;
namespace {
+// The full absolute names of the nodes in the tree (the tree also
+// contains "." which is not included in this list).
+const char* const domain_names[] = {
+ "c", "b", "a", "x.d.e.f", "z.d.e.f", "g.h", "i.g.h", "o.w.y.d.e.f",
+ "j.z.d.e.f", "p.w.y.d.e.f", "q.w.y.d.e.f", "k.g.h"
+};
+
+// These are set as the node data.
+const int node_distances[] = {
+ 3, 1, 2, 2, 2, 3, 1, 2, 1, 1, 2, 2
+};
+
+const int name_count = sizeof(domain_names) / sizeof(domain_names[0]);
+
+/*
+ * The domain order should be:
+ * ., a, b, c, d.e.f, x.d.e.f, w.y.d.e.f, o.w.y.d.e.f, p.w.y.d.e.f,
+ * q.w.y.d.e.f, z.d.e.f, j.z.d.e.f, g.h, i.g.h, k.g.h
+ * . (no data, can't be found)
+ * |
+ * b
+ * / \
+ * a d.e.f
+ * / | \
+ * c | g.h
+ * | |
+ * w.y i
+ * / | \ \
+ * x | z k
+ * | |
+ * p j
+ * / \
+ * o q
+ */
+
+const char* const ordered_names[] = {
+ "a", "b", "c", "d.e.f", "x.d.e.f", "w.y.d.e.f", "o.w.y.d.e.f",
+ "p.w.y.d.e.f", "q.w.y.d.e.f", "z.d.e.f", "j.z.d.e.f",
+ "g.h", "i.g.h", "k.g.h"};
+const size_t ordered_names_count(sizeof(ordered_names) /
+ sizeof(*ordered_names));
+
+const char* const upper_node_names[] = {
+ ".", ".", ".", ".", "d.e.f", "d.e.f", "w.y.d.e.f",
+ "w.y.d.e.f", "w.y.d.e.f", "d.e.f", "z.d.e.f",
+ ".", "g.h", "g.h"};
+
void deleteData(int* i) {
delete i;
}
@@ -85,16 +141,13 @@ class DomainTreeTest : public::testing::Test {
protected:
DomainTreeTest() :
dtree_holder_(mem_sgmt_, TestDomainTree::create(mem_sgmt_)),
- dtree_expose_empty_node_holder_(mem_sgmt_,
- TestDomainTree::create(mem_sgmt_, true)),
+ dtree_expose_empty_node_holder_
+ (mem_sgmt_, TestDomainTree::create(mem_sgmt_, true)),
dtree(*dtree_holder_.get()),
dtree_expose_empty_node(*dtree_expose_empty_node_holder_.get()),
- cdtnode(NULL)
+ cdtnode(NULL),
+ name_gen_('a', 'z')
{
- const char* const domain_names[] = {
- "c", "b", "a", "x.d.e.f", "z.d.e.f", "g.h", "i.g.h", "o.w.y.d.e.f",
- "j.z.d.e.f", "p.w.y.d.e.f", "q.w.y.d.e.f", "k.g.h"};
- int name_count = sizeof(domain_names) / sizeof(domain_names[0]);
for (int i = 0; i < name_count; ++i) {
dtree.insert(mem_sgmt_, Name(domain_names[i]), &dtnode);
// Check the node doesn't have any data initially.
@@ -103,8 +156,11 @@ protected:
dtree_expose_empty_node.insert(mem_sgmt_, Name(domain_names[i]),
&dtnode);
- EXPECT_EQ(static_cast<int*>(NULL), dtnode->setData(new int(i + 1)));
+ EXPECT_EQ(static_cast<int*>(NULL), dtnode->setData(
+ new int(node_distances[i])));
}
+
+ srandom(time(NULL));
}
util::MemorySegmentLocal mem_sgmt_;
@@ -114,6 +170,7 @@ protected:
TestDomainTree& dtree_expose_empty_node;
TestDomainTreeNode* dtnode;
const TestDomainTreeNode* cdtnode;
+ UniformRandomIntegerGenerator name_gen_;
uint8_t buf[LabelSequence::MAX_SERIALIZED_LENGTH];
};
@@ -121,11 +178,108 @@ TEST_F(DomainTreeTest, nodeCount) {
EXPECT_EQ(15, dtree.getNodeCount());
// Delete all nodes, then the count should be set to 0. This also tests
- // the behavior of deleteAllNodes().
- dtree.deleteAllNodes(mem_sgmt_, deleteData);
+ // the behavior of removeAllNodes().
+ dtree.removeAllNodes(mem_sgmt_, deleteData);
EXPECT_EQ(0, dtree.getNodeCount());
}
+TEST_F(DomainTreeTest, getDistance) {
+ TestDomainTreeNodeChain node_path;
+ const TestDomainTreeNode* node = NULL;
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ dtree_expose_empty_node.find(Name("a"),
+ &node,
+ node_path));
+ while (node != NULL) {
+ const int* distance = node->getData();
+ if (distance != NULL) {
+ EXPECT_EQ(*distance, node->getDistance());
+ }
+ node = dtree_expose_empty_node.nextNode(node_path);
+ }
+}
+
+TEST_F(DomainTreeTest, checkDistanceRandom) {
+ // This test checks an important performance-related property of the
+ // DomainTree (a red-black tree), which is important for us: the
+ // longest path from a sub-tree's root to a node is no more than
+ // 2log(n). This tests that the tree is balanced.
+
+ // Names are inserted in random order.
+
+ TreeHolder mytree_holder(mem_sgmt_, TestDomainTree::create(mem_sgmt_));
+ TestDomainTree& mytree = *mytree_holder.get();
+ const int log_num_nodes = 20;
+
+ // Make a large million+ node top-level domain tree, i.e., the
+ // following code inserts names such as:
+ //
+ // savoucnsrkrqzpkqypbygwoiliawpbmz.
+ // wkadamcbbpjtundbxcmuayuycposvngx.
+ // wzbpznemtooxdpjecdxynsfztvnuyfao.
+ // yueojmhyffslpvfmgyfwioxegfhepnqq.
+ //
+ for (int i = 0; i < (1 << log_num_nodes); i++) {
+ string namestr;
+ while (true) {
+ for (int j = 0; j < 32; j++) {
+ namestr += name_gen_();
+ }
+ namestr += '.';
+
+ if (mytree.insert(mem_sgmt_, Name(namestr), &dtnode) ==
+ TestDomainTree::SUCCESS) {
+ break;
+ }
+
+ namestr.clear();
+ }
+
+ EXPECT_EQ(static_cast<int*>(NULL), dtnode->setData(new int(i + 1)));
+ }
+
+ // The distance from each node to its sub-tree root must be less
+ // than 2 * log(n).
+ EXPECT_GE(2 * log_num_nodes, mytree.getHeight());
+
+ // Also check RB tree properties
+ EXPECT_TRUE(mytree.checkProperties());
+}
+
+TEST_F(DomainTreeTest, checkDistanceSorted) {
+ // This test checks an important performance-related property of the
+ // DomainTree (a red-black tree), which is important for us: the
+ // longest path from a sub-tree's root to a node is no more than
+ // 2log(n). This tests that the tree is balanced.
+
+ // Names are inserted in sorted order.
+
+ TreeHolder mytree_holder(mem_sgmt_, TestDomainTree::create(mem_sgmt_));
+ TestDomainTree& mytree = *mytree_holder.get();
+ const int log_num_nodes = 20;
+
+ // Make a large million+ node top-level domain tree, i.e., the
+ // following code inserts names such as:
+ //
+ // name00000000.
+ // name00000001.
+ // name00000002.
+ // name00000003.
+ //
+ for (int i = 0; i < (1 << log_num_nodes); i++) {
+ const string namestr(boost::str(boost::format("name%08x.") % i));
+ mytree.insert(mem_sgmt_, Name(namestr), &dtnode);
+ EXPECT_EQ(static_cast<int*>(NULL), dtnode->setData(new int(i + 1)));
+ }
+
+ // The distance from each node to its sub-tree root must be less
+ // than 2 * log(n).
+ EXPECT_GE(2 * log_num_nodes, mytree.getHeight());
+
+ // Also check RB tree properties
+ EXPECT_TRUE(mytree.checkProperties());
+}
+
TEST_F(DomainTreeTest, setGetData) {
// set new data to an existing node. It should have some data.
int* newdata = new int(11);
@@ -232,6 +386,329 @@ TEST_F(DomainTreeTest, insertNames) {
&dtnode));
}
+TEST_F(DomainTreeTest, remove) {
+ // This testcase checks that after node removal, the binary-search
+ // tree is valid and all nodes that are supposed to exist are
+ // present in the correct order. It mainly tests DomainTree as a
+ // BST, and not particularly as a red-black tree. This test checks
+ // node deletion when upper nodes have data.
+
+ // Delete single nodes and check if the rest of the nodes exist
+ for (int j = 0; j < ordered_names_count; ++j) {
+ TreeHolder holder(mem_sgmt_, TestDomainTree::create(mem_sgmt_, true));
+ TestDomainTree& tree(*holder.get());
+ TestDomainTreeNode* node;
+
+ for (int i = 0; i < name_count; ++i) {
+ tree.insert(mem_sgmt_, Name(domain_names[i]), NULL);
+ }
+
+ for (int i = 0; i < ordered_names_count; ++i) {
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[i]), &node));
+ // Check nodes are not empty.
+ EXPECT_EQ(static_cast<int*>(NULL), node->setData(new int(i)));
+ EXPECT_FALSE(node->isEmpty());
+ }
+
+ // Now, delete the j'th node from the tree.
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[j]), &node));
+ tree.remove(mem_sgmt_, node, deleteData);
+
+ // Check RB tree properties
+ ASSERT_TRUE(tree.checkProperties());
+
+ // Now, walk through nodes in order.
+ TestDomainTreeNodeChain node_path;
+ const TestDomainTreeNode* cnode;
+ int start_node;
+
+ if (j == 0) {
+ EXPECT_NE(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[0]),
+ &cnode));
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[1]),
+ &cnode, node_path));
+ start_node = 1;
+ } else {
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[0]),
+ &cnode, node_path));
+ start_node = 0;
+ }
+
+ for (int i = start_node; i < ordered_names_count; ++i) {
+ const Name nj(ordered_names[j]);
+ const Name ni(ordered_names[i]);
+
+ if (ni == nj) {
+ // This may be true for the last node if we seek ahead
+ // in the loop using nextNode() below.
+ if (!cnode) {
+ break;
+ }
+ // All ordered nodes have data initially. If any node is
+ // empty, it means it was remove()d, but an empty node
+ // exists because it is a super-domain. Just skip it.
+ if (cnode->isEmpty()) {
+ cnode = tree.nextNode(node_path);
+ }
+ continue;
+ }
+
+ ASSERT_NE(static_cast<void*>(NULL), cnode);
+ const int* data = cnode->getData();
+
+ if (data) {
+ EXPECT_EQ(i, *data);
+ }
+
+ uint8_t buf[isc::dns::LabelSequence::MAX_SERIALIZED_LENGTH];
+ const LabelSequence ls(cnode->getAbsoluteLabels(buf));
+ EXPECT_EQ(LabelSequence(ni), ls);
+
+ cnode = tree.nextNode(node_path);
+ }
+
+ // We should have reached the end of the tree.
+ ASSERT_EQ(static_cast<void*>(NULL), cnode);
+ }
+}
+
+TEST_F(DomainTreeTest, removeEmpty) {
+ // This test is similar to the .remove test. But it checks node
+ // deletion when upper nodes are empty.
+
+ // Delete single nodes and check if the rest of the nodes exist
+ for (int j = 0; j < ordered_names_count; ++j) {
+ TreeHolder holder(mem_sgmt_, TestDomainTree::create(mem_sgmt_, true));
+ TestDomainTree& tree(*holder.get());
+ TestDomainTreeNode* node;
+
+ for (int i = 0; i < name_count; ++i) {
+ tree.insert(mem_sgmt_, Name(domain_names[i]), NULL);
+ }
+
+ for (int i = 0; i < ordered_names_count; ++i) {
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[i]), &node));
+ // Check nodes are empty.
+ EXPECT_TRUE(node->isEmpty());
+ }
+
+ // Now, delete the j'th node from the tree.
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[j]), &node));
+ tree.remove(mem_sgmt_, node, deleteData);
+
+ // Check RB tree properties
+ ASSERT_TRUE(tree.checkProperties());
+
+ // Now, walk through nodes in order.
+ TestDomainTreeNodeChain node_path;
+ const TestDomainTreeNode* cnode;
+ int start_node;
+
+ if (j == 0) {
+ EXPECT_NE(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[0]),
+ &cnode));
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[1]),
+ &cnode, node_path));
+ start_node = 1;
+ } else {
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(ordered_names[0]),
+ &cnode, node_path));
+ start_node = 0;
+ }
+
+ for (int i = start_node; i < ordered_names_count; ++i) {
+ const Name nj(ordered_names[j]);
+ const Name ni(ordered_names[i]);
+
+ if ((nj == Name("j.z.d.e.f")) &&
+ (ni == Name("z.d.e.f")))
+ {
+ // The only special case in the tree. Here, "z.d.e.f"
+ // will not exist as it would have been removed during
+ // removal of "j.z.d.e.f".
+ continue;
+ }
+
+ if (ni == nj) {
+ // This may be true for the last node if we seek ahead
+ // in the loop using nextNode() below.
+ if (!cnode) {
+ break;
+ }
+ // All ordered nodes are empty initially. If an empty
+ // removed node exists because it is a super-domain,
+ // just skip it.
+ if ((nj == Name("d.e.f")) ||
+ (nj == Name("w.y.d.e.f")) ||
+ (nj == Name("z.d.e.f")) ||
+ (nj == Name("g.h")))
+ {
+ cnode = tree.nextNode(node_path);
+ }
+ continue;
+ }
+
+ ASSERT_NE(static_cast<void*>(NULL), cnode);
+
+ uint8_t buf[isc::dns::LabelSequence::MAX_SERIALIZED_LENGTH];
+ const LabelSequence ls(cnode->getAbsoluteLabels(buf));
+ EXPECT_EQ(LabelSequence(ni), ls);
+
+ cnode = tree.nextNode(node_path);
+ }
+
+ // We should have reached the end of the tree.
+ ASSERT_EQ(static_cast<void*>(NULL), cnode);
+ }
+}
+
+void
+insertNodes(util::MemorySegment& mem_sgmt, TestDomainTree& tree,
+ std::set<std::string>& names, size_t num_nodes,
+ UniformRandomIntegerGenerator& name_gen)
+{
+ for (size_t i = 0; i < num_nodes; ++i) {
+ std::string namestr;
+ while (true) {
+ for (int j = 0; j < 32; j++) {
+ namestr += name_gen();
+ }
+ namestr += '.';
+
+ TestDomainTreeNode* cnode;
+ if (tree.insert(mem_sgmt, Name(namestr), &cnode) ==
+ TestDomainTree::SUCCESS) {
+ names.insert(namestr);
+ break;
+ }
+
+ namestr.clear();
+ }
+ }
+}
+
+void
+removeNodes(util::MemorySegment& mem_sgmt, TestDomainTree& tree,
+ std::set<std::string>& names, size_t num_nodes)
+{
+ size_t set_size = names.size();
+
+ for (size_t i = 0; i < num_nodes; ++i) {
+ // Here, UniformRandomIntegerGenerator is not a great RNG as
+ // it'll likely get seeded with the same seed throughout this
+ // testcase, and the size of the names set keeps changing.
+
+ std::set<std::string>::iterator it(names.begin());
+ // This is rather inefficient, but it's a test...
+ std::advance(it, random() % set_size);
+ std::string nstr(*it);
+
+ TestDomainTreeNode* node;
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name(nstr), &node));
+
+ tree.remove(mem_sgmt, node, deleteData);
+
+ names.erase(*it);
+ --set_size;
+ }
+}
+
+void
+checkTree(const TestDomainTree& tree,
+ const std::set<std::string>& names)
+{
+ // The distance from each node to its sub-tree root must be less
+ // than 2 * log_2(1024).
+ EXPECT_GE(2 * 10, tree.getHeight());
+
+ // Also check RB tree properties
+ EXPECT_TRUE(tree.checkProperties());
+
+ // Now, walk through nodes in order.
+ TestDomainTreeNodeChain node_path;
+ const TestDomainTreeNode* cnode;
+
+ EXPECT_EQ(TestDomainTree::EXACTMATCH,
+ tree.find(Name("."), &cnode, node_path));
+
+ // Skip to the next node after "."
+ cnode = tree.nextNode(node_path);
+ if (names.empty()) {
+ // Empty tree.
+ EXPECT_EQ(static_cast<void*>(NULL), cnode);
+ return;
+ }
+
+ for (std::set<std::string>::const_iterator it = names.begin();
+ it != names.end(); ++it)
+ {
+ const Name n1(*it);
+ const Name n2(cnode->getName());
+
+ const NameComparisonResult result = n1.compare(n2);
+ EXPECT_EQ(NameComparisonResult::EQUAL, result.getRelation());
+
+ cnode = tree.nextNode(node_path);
+ }
+
+ // We should have reached the end of the tree.
+ EXPECT_EQ(static_cast<void*>(NULL), cnode);
+}
+
+TEST_F(DomainTreeTest, insertAndRemove) {
+ // What is the best way to test our red-black tree code? It is not a
+ // good method to test every case handled in the actual code itself
+ // (such as in insertRebalance() and removeRebalance()). This is
+ // because our approach itself may be incorrect.
+ //
+ // We test our code at the interface level here by exercising the
+ // tree randomly multiple times, checking that red-black tree
+ // properties are valid, and all the nodes that are supposed to be
+ // in the tree exist and are in order.
+
+ // NOTE: These tests are run within a single tree in the
+ // forest. Fusion, etc. are tested elsewhere. The number of nodes in
+ // the tree doesn't grow over 1024.
+
+ TreeHolder holder(mem_sgmt_, TestDomainTree::create(mem_sgmt_, true));
+ TestDomainTree& tree(*holder.get());
+ std::set<std::string> names;
+
+ size_t node_count = 0;
+
+ // Repeat the insert/remove test some 4096 times
+ for (int i = 0; i < 4096; ++i) {
+ UniformRandomIntegerGenerator gen(1, 1024 - node_count);
+ size_t num_nodes = gen();
+ node_count += num_nodes;
+
+ insertNodes(mem_sgmt_, tree, names, num_nodes, name_gen_);
+ checkTree(tree, names);
+
+ UniformRandomIntegerGenerator gen2(1, node_count);
+ num_nodes = gen2();
+ node_count -= num_nodes;
+
+ removeNodes(mem_sgmt_, tree, names, num_nodes);
+ checkTree(tree, names);
+ }
+
+ // Remove the rest of the nodes.
+ removeNodes(mem_sgmt_, tree, names, node_count);
+ checkTree(tree, names);
+}
+
TEST_F(DomainTreeTest, subTreeRoot) {
// This is a testcase for a particular issue that went unchecked in
// #2089's implementation, but was fixed in #2092. The issue was
@@ -666,45 +1143,14 @@ TEST_F(DomainTreeTest, getAbsoluteNameError) {
EXPECT_THROW(chain.getAbsoluteName(), BadValue);
}
-/*
- * The domain order should be:
- * ., a, b, c, d.e.f, x.d.e.f, w.y.d.e.f, o.w.y.d.e.f, p.w.y.d.e.f,
- * q.w.y.d.e.f, z.d.e.f, j.z.d.e.f, g.h, i.g.h, k.g.h
- * . (no data, can't be found)
- * |
- * b
- * / \
- * a d.e.f
- * / | \
- * c | g.h
- * | |
- * w.y i
- * / | \ \
- * x | z k
- * | |
- * p j
- * / \
- * o q
- */
-const char* const names[] = {
- "a", "b", "c", "d.e.f", "x.d.e.f", "w.y.d.e.f", "o.w.y.d.e.f",
- "p.w.y.d.e.f", "q.w.y.d.e.f", "z.d.e.f", "j.z.d.e.f",
- "g.h", "i.g.h", "k.g.h"};
-const size_t name_count(sizeof(names) / sizeof(*names));
-
-const char* const upper_node_names[] = {
- ".", ".", ".", ".", "d.e.f", "d.e.f", "w.y.d.e.f",
- "w.y.d.e.f", "w.y.d.e.f", "d.e.f", "z.d.e.f",
- ".", "g.h", "g.h"};
-
TEST_F(DomainTreeTest, getUpperNode) {
TestDomainTreeNodeChain node_path;
const TestDomainTreeNode* node = NULL;
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree_expose_empty_node.find(Name(names[0]),
- &node,
- node_path));
- for (int i = 0; i < name_count; ++i) {
+ dtree_expose_empty_node.find(Name(ordered_names[0]),
+ &node,
+ node_path));
+ for (int i = 0; i < ordered_names_count; ++i) {
EXPECT_NE(static_cast<void*>(NULL), node);
const TestDomainTreeNode* upper_node = node->getUpperNode();
@@ -740,7 +1186,7 @@ TEST_F(DomainTreeTest, getSubTreeRoot) {
TestDomainTreeNodeChain node_path;
const TestDomainTreeNode* node = NULL;
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree_expose_empty_node.find(Name(names[0]),
+ dtree_expose_empty_node.find(Name(ordered_names[0]),
&node,
node_path));
for (int i = 0; i < name_count; ++i) {
@@ -772,10 +1218,10 @@ TEST_F(DomainTreeTest, nextNode) {
TestDomainTreeNodeChain node_path;
const TestDomainTreeNode* node = NULL;
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree.find(Name(names[0]), &node, node_path));
- for (int i = 0; i < name_count; ++i) {
+ dtree.find(Name(ordered_names[0]), &node, node_path));
+ for (int i = 0; i < ordered_names_count; ++i) {
EXPECT_NE(static_cast<void*>(NULL), node);
- EXPECT_EQ(Name(names[i]), node_path.getAbsoluteName());
+ EXPECT_EQ(Name(ordered_names[i]), node_path.getAbsoluteName());
node = dtree.nextNode(node_path);
}
@@ -791,7 +1237,7 @@ TEST_F(DomainTreeTest, nextNode) {
// node_path - the path from the previous call to find(), will be modified
// chain_length - the number of names that should be in the chain to be walked
// (0 means it should be empty, 3 means 'a', 'b' and 'c' should be there -
-// this is always from the beginning of the names[] list).
+// this is always from the beginning of the ordered_names[] list).
// skip_first - if this is false, the node should already contain the node with
// the first name of the chain. If it is true, the node should be NULL
// (true is for finds that return no match, false for the ones that return
@@ -809,7 +1255,7 @@ previousWalk(TestDomainTree& dtree, const TestDomainTreeNode* node,
}
for (size_t i(chain_length); i > 0; --i) {
EXPECT_NE(static_cast<void*>(NULL), node);
- EXPECT_EQ(Name(names[i - 1]), node_path.getAbsoluteName());
+ EXPECT_EQ(Name(ordered_names[i - 1]), node_path.getAbsoluteName());
// Find the node at the path and check the value is the same
// (that it really returns the correct corresponding node)
//
@@ -818,7 +1264,8 @@ previousWalk(TestDomainTree& dtree, const TestDomainTreeNode* node,
const TestDomainTreeNode* node2(NULL);
TestDomainTreeNodeChain node_path2;
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree.find(Name(names[i - 1]), &node2, node_path2));
+ dtree.find(Name(ordered_names[i - 1]),
+ &node2, node_path2));
EXPECT_EQ(node, node2);
}
node = dtree.previousNode(node_path);
@@ -848,8 +1295,9 @@ TEST_F(DomainTreeTest, previousNode) {
{
SCOPED_TRACE("Iterate through");
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree.find(Name(names[name_count - 1]), &node, node_path));
- previousWalk(dtree, node, node_path, name_count, false);
+ dtree.find(Name(ordered_names[ordered_names_count - 1]),
+ &node, node_path));
+ previousWalk(dtree, node, node_path, ordered_names_count, false);
node = NULL;
node_path.clear();
}
@@ -858,7 +1306,7 @@ TEST_F(DomainTreeTest, previousNode) {
SCOPED_TRACE("Iterate from the middle");
// Now, start somewhere in the middle, but within the real node.
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree.find(Name(names[4]), &node, node_path));
+ dtree.find(Name(ordered_names[4]), &node, node_path));
previousWalk(dtree, node, node_path, 5, false);
node = NULL;
node_path.clear();
@@ -869,7 +1317,7 @@ TEST_F(DomainTreeTest, previousNode) {
// If we start at the lowest (which is "a"), we get to the beginning
// right away.
EXPECT_EQ(TestDomainTree::EXACTMATCH,
- dtree.find(Name(names[0]), &node, node_path));
+ dtree.find(Name(ordered_names[0]), &node, node_path));
EXPECT_NE(static_cast<void*>(NULL), node);
node = dtree.previousNode(node_path);
ASSERT_NE(static_cast<void*>(NULL), node);
@@ -896,7 +1344,7 @@ TEST_F(DomainTreeTest, previousNode) {
SCOPED_TRACE("Start after the last");
EXPECT_EQ(TestDomainTree::NOTFOUND,
dtree.find(Name("z"), &node, node_path));
- previousWalk(dtree, node, node_path, name_count, true);
+ previousWalk(dtree, node, node_path, ordered_names_count, true);
node = NULL;
node_path.clear();
}
@@ -958,7 +1406,7 @@ TEST_F(DomainTreeTest, previousNode) {
EXPECT_GT(node_path.getLastComparisonResult().getOrder(), 0);
// We then descend into 'i.g.h' and walk all the nodes in the
// tree.
- previousWalk(dtree, node, node_path, name_count, true);
+ previousWalk(dtree, node, node_path, ordered_names_count, true);
node = NULL;
node_path.clear();
}
@@ -1252,17 +1700,11 @@ TEST_F(DomainTreeTest, root) {
}
TEST_F(DomainTreeTest, getAbsoluteLabels) {
- // The full absolute names of the nodes in the tree
- // with the addition of the explicit root node
- const char* const domain_names[] = {
- "c", "b", "a", "x.d.e.f", "z.d.e.f", "g.h", "i.g.h", "o.w.y.d.e.f",
- "j.z.d.e.f", "p.w.y.d.e.f", "q.w.y.d.e.f", "k.g.h"};
// The names of the nodes themselves, as they end up in the tree
const char* const first_labels[] = {
"c", "b", "a", "x", "z", "g.h", "i", "o",
"j", "p", "q", "k"};
- const int name_count = sizeof(domain_names) / sizeof(domain_names[0]);
for (int i = 0; i < name_count; ++i) {
EXPECT_EQ(TestDomainTree::EXACTMATCH, dtree.find(Name(domain_names[i]),
&cdtnode));
diff --git a/src/lib/datasrc/tests/memory/memory_client_unittest.cc b/src/lib/datasrc/tests/memory/memory_client_unittest.cc
index f5b72a0..73862e3 100644
--- a/src/lib/datasrc/tests/memory/memory_client_unittest.cc
+++ b/src/lib/datasrc/tests/memory/memory_client_unittest.cc
@@ -37,8 +37,8 @@
#include <testutils/dnsmessage_test.h>
-#include "memory_segment_test.h"
-#include "zone_table_segment_test.h"
+#include <datasrc/tests/memory/memory_segment_mock.h>
+#include <datasrc/tests/memory/zone_table_segment_mock.h>
#include <gtest/gtest.h>
@@ -169,7 +169,7 @@ public:
class MemoryClientTest : public ::testing::Test {
protected:
MemoryClientTest() : zclass_(RRClass::IN()),
- ztable_segment_(new test::ZoneTableSegmentTest(
+ ztable_segment_(new test::ZoneTableSegmentMock(
zclass_, mem_sgmt_)),
client_(new InMemoryClient(ztable_segment_, zclass_))
{}
@@ -179,7 +179,7 @@ protected:
EXPECT_TRUE(mem_sgmt_.allMemoryDeallocated()); // catch any leak here.
}
const RRClass zclass_;
- test::MemorySegmentTest mem_sgmt_;
+ test::MemorySegmentMock mem_sgmt_;
shared_ptr<ZoneTableSegment> ztable_segment_;
boost::scoped_ptr<InMemoryClient> client_;
};
@@ -305,12 +305,12 @@ TEST_F(MemoryClientTest, loadMemoryAllocationFailures) {
mem_sgmt_.setThrowCount(i);
EXPECT_THROW({
shared_ptr<ZoneTableSegment> ztable_segment(
- new test::ZoneTableSegmentTest(
+ new test::ZoneTableSegmentMock(
zclass_, mem_sgmt_));
// Include the InMemoryClient construction too here. Now,
// even allocations done from InMemoryClient constructor
- // fail (due to MemorySegmentTest throwing) and we check for
+ // fail (due to MemorySegmentMock throwing) and we check for
// leaks when this happens.
InMemoryClient client2(ztable_segment, zclass_);
loadZoneIntoTable(*ztable_segment, Name("example.org"), zclass_,
@@ -669,7 +669,7 @@ TEST_F(MemoryClientTest, getZoneCount) {
TEST_F(MemoryClientTest, getIteratorForNonExistentZone) {
// Zone "." doesn't exist
- EXPECT_THROW(client_->getIterator(Name(".")), DataSourceError);
+ EXPECT_THROW(client_->getIterator(Name(".")), NoSuchZone);
}
TEST_F(MemoryClientTest, getIterator) {
@@ -694,6 +694,15 @@ TEST_F(MemoryClientTest, getIterator) {
EXPECT_THROW(iterator->getNextRRset(), isc::Unexpected);
}
+TEST_F(MemoryClientTest, getIteratorForEmptyZone) {
+ // trying to load a broken zone (zone file not existent). It's internally
+ // stored an empty zone.
+ loadZoneIntoTable(*ztable_segment_, Name("example.org"), zclass_,
+ TEST_DATA_DIR "/no-such-file.zone", true);
+ // Then getIterator will result in an exception.
+ EXPECT_THROW(client_->getIterator(Name("example.org")), EmptyZone);
+}
+
TEST_F(MemoryClientTest, getIteratorSeparateRRs) {
loadZoneIntoTable(*ztable_segment_, Name("example.org"), zclass_,
TEST_DATA_DIR "/example.org-multiple.zone");
@@ -791,6 +800,35 @@ TEST_F(MemoryClientTest, addEmptyRRsetThrows) {
// Teardown checks for memory segment leaks
}
+TEST_F(MemoryClientTest, findEmptyZone) {
+ // trying to load a broken zone (zone file not existent). It's internally
+ // stored an empty zone.
+ loadZoneIntoTable(*ztable_segment_, Name("example.org"), zclass_,
+ TEST_DATA_DIR "/no-such-file.zone", true);
+
+ using namespace isc::datasrc::result;
+
+ // findZone() returns the match, with NULL zone finder and the result
+ // flag indicating it's empty.
+ const DataSourceClient::FindResult result =
+ client_->findZone(Name("example.org"));
+ EXPECT_EQ(SUCCESS, result.code);
+ EXPECT_EQ(ZONE_EMPTY, result.flags);
+ EXPECT_FALSE(result.zone_finder);
+
+ // Same for the case of subdomain match
+ const DataSourceClient::FindResult result_sub =
+ client_->findZone(Name("www.example.org"));
+ EXPECT_EQ(PARTIALMATCH, result_sub.code);
+ EXPECT_EQ(ZONE_EMPTY, result_sub.flags);
+ EXPECT_FALSE(result_sub.zone_finder);
+
+ // findZoneData() will simply NULL (this is for testing only anyway,
+ // so any result would be okay as long as it doesn't cause disruption).
+ EXPECT_EQ(static_cast<const ZoneData*>(NULL),
+ client_->findZoneData(Name("example.org")));
+}
+
TEST_F(MemoryClientTest, findZoneData) {
loadZoneIntoTable(*ztable_segment_, Name("example.org"), zclass_,
TEST_DATA_DIR "/example.org-rrsigs.zone");
diff --git a/src/lib/datasrc/tests/memory/memory_segment_mock.h b/src/lib/datasrc/tests/memory/memory_segment_mock.h
new file mode 100644
index 0000000..92c291d
--- /dev/null
+++ b/src/lib/datasrc/tests/memory/memory_segment_mock.h
@@ -0,0 +1,62 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DATASRC_MEMORY_SEGMENT_TEST_H
+#define DATASRC_MEMORY_SEGMENT_TEST_H 1
+
+#include <util/memory_segment_local.h>
+
+#include <cstddef> // for size_t
+#include <new> // for bad_alloc
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+namespace test {
+
+// A special memory segment that can be used for tests. It normally behaves
+// like a "local" memory segment. If "throw count" is set to non 0 via
+// setThrowCount(), it continues the normal behavior until the specified
+// number of calls to allocate(), exclusive, and throws an exception at the
+// next call. For example, if count is set to 3, the next two calls to
+// allocate() will succeed, and the 3rd call will fail with an exception.
+// This segment object can be used after the exception is thrown, and the
+// count is internally reset to 0.
+class MemorySegmentMock : public isc::util::MemorySegmentLocal {
+public:
+ MemorySegmentMock() : throw_count_(0) {}
+ virtual void* allocate(std::size_t size) {
+ if (throw_count_ > 0) {
+ if (--throw_count_ == 0) {
+ throw std::bad_alloc();
+ }
+ }
+ return (isc::util::MemorySegmentLocal::allocate(size));
+ }
+ void setThrowCount(std::size_t count) { throw_count_ = count; }
+
+private:
+ std::size_t throw_count_;
+};
+
+} // namespace test
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
+
+#endif // DATASRC_MEMORY_SEGMENT_TEST_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/datasrc/tests/memory/memory_segment_test.h b/src/lib/datasrc/tests/memory/memory_segment_test.h
deleted file mode 100644
index 3195a9b..0000000
--- a/src/lib/datasrc/tests/memory/memory_segment_test.h
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef DATASRC_MEMORY_SEGMENT_TEST_H
-#define DATASRC_MEMORY_SEGMENT_TEST_H 1
-
-#include <util/memory_segment_local.h>
-
-#include <cstddef> // for size_t
-#include <new> // for bad_alloc
-
-namespace isc {
-namespace datasrc {
-namespace memory {
-namespace test {
-
-// A special memory segment that can be used for tests. It normally behaves
-// like a "local" memory segment. If "throw count" is set to non 0 via
-// setThrowCount(), it continues the normal behavior until the specified
-// number of calls to allocate(), exclusive, and throws an exception at the
-// next call. For example, if count is set to 3, the next two calls to
-// allocate() will succeed, and the 3rd call will fail with an exception.
-// This segment object can be used after the exception is thrown, and the
-// count is internally reset to 0.
-class MemorySegmentTest : public isc::util::MemorySegmentLocal {
-public:
- MemorySegmentTest() : throw_count_(0) {}
- virtual void* allocate(std::size_t size) {
- if (throw_count_ > 0) {
- if (--throw_count_ == 0) {
- throw std::bad_alloc();
- }
- }
- return (isc::util::MemorySegmentLocal::allocate(size));
- }
- void setThrowCount(std::size_t count) { throw_count_ = count; }
-
-private:
- std::size_t throw_count_;
-};
-
-} // namespace test
-} // namespace memory
-} // namespace datasrc
-} // namespace isc
-
-#endif // DATASRC_MEMORY_SEGMENT_TEST_H
-
-// Local Variables:
-// mode: c++
-// End:
diff --git a/src/lib/datasrc/tests/memory/rdata_serialization_unittest.cc b/src/lib/datasrc/tests/memory/rdata_serialization_unittest.cc
index fb01dd6..4e4397e 100644
--- a/src/lib/datasrc/tests/memory/rdata_serialization_unittest.cc
+++ b/src/lib/datasrc/tests/memory/rdata_serialization_unittest.cc
@@ -825,7 +825,7 @@ TEST_F(RdataSerializationTest, badAddRdata) {
EXPECT_THROW(encoder_.addRdata(*aaaa_rdata_), std::bad_cast);
const ConstRdataPtr rp_rdata =
- createRdata(RRType::RP(), RRClass::IN(), "a.example. b.example");
+ createRdata(RRType::RP(), RRClass::IN(), "a.example. b.example.");
encoder_.start(RRClass::IN(), RRType::NS());
EXPECT_THROW(encoder_.addRdata(*rp_rdata), std::bad_cast);
diff --git a/src/lib/datasrc/tests/memory/rdataset_unittest.cc b/src/lib/datasrc/tests/memory/rdataset_unittest.cc
index 1a964ef..e6fd5cc 100644
--- a/src/lib/datasrc/tests/memory/rdataset_unittest.cc
+++ b/src/lib/datasrc/tests/memory/rdataset_unittest.cc
@@ -103,8 +103,10 @@ checkData(const void* data, size_t size, const RRType* rrtype,
ASSERT_TRUE(*it != it_end); // shouldn't reach the end yet
isc::util::InputBuffer b(data, size);
- EXPECT_EQ(0, createRdata(*rrtype, RRClass::IN(), b, size)->compare(
- *createRdata(*rrtype, RRClass::IN(), **it)));
+ const RdataPtr& actual(createRdata(*rrtype, RRClass::IN(), b, size));
+ const RdataPtr& expected(createRdata(*rrtype, RRClass::IN(), **it));
+ EXPECT_EQ(0, actual->compare(*expected)) << actual->toText() <<
+ " vs. " << expected->toText();
++(*it); // move to the next expected data
}
@@ -191,20 +193,16 @@ TEST_F(RdataSetTest, mergeCreate) {
SCOPED_TRACE("creating merge case " + lexical_cast<string>(i) +
", " + lexical_cast<string>(j));
// Create old rdataset
- SegmentObjectHolder<RdataSet, RRClass> holder1(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_,
+ SegmentObjectHolder<RdataSet, RRClass> holder1(mem_sgmt_, rrclass);
+ holder1.set(RdataSet::create(mem_sgmt_, encoder_,
(i & 1) != 0 ? a_rrsets[0] : null_rrset,
- (i & 2) != 0 ? rrsig_rrsets[0] : null_rrset),
- rrclass);
+ (i & 2) != 0 ? rrsig_rrsets[0] : null_rrset));
// Create merged rdataset, based on the old one and RRsets
- SegmentObjectHolder<RdataSet, RRClass> holder2(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_,
+ SegmentObjectHolder<RdataSet, RRClass> holder2(mem_sgmt_, rrclass);
+ holder2.set(RdataSet::create(mem_sgmt_, encoder_,
(j & 1) != 0 ? a_rrsets[1] : null_rrset,
(j & 2) != 0 ? rrsig_rrsets[1] : null_rrset,
- holder1.get()),
- rrclass);
+ holder1.get()));
// Set up the expected data for the case.
vector<string> expected_rdata;
@@ -228,6 +226,91 @@ TEST_F(RdataSetTest, mergeCreate) {
}
}
+TEST_F(RdataSetTest, subtract) {
+ // Prepare test data
+ const char* const a_rdatas[] = { "192.0.2.1", "192.0.2.2" };
+ const char* const sig_rdatas[] = {
+ "A 5 2 3600 20120814220826 20120715220826 1234 example.com. FAKE",
+ "A 5 2 3600 20120814220826 20120715220826 4321 example.com. FAKE" };
+ ConstRRsetPtr a_rrsets = textToRRset("www.example.com. 1076895760 IN A "
+ + string(a_rdatas[0]) + "\n"
+ + "www.example.com. 1076895760 IN A "
+ + string(a_rdatas[1]));
+ ConstRRsetPtr rrsig_rrsets =
+ textToRRset("www.example.com. 1076895760 IN RRSIG "
+ + string(sig_rdatas[0]) + "\n"
+ + "www.example.com. 1076895760 IN RRSIG "
+ + string(sig_rdatas[1]));
+ ConstRRsetPtr null_rrset; // convenience shortcut
+ // Prepare the data to subtract (they have one common and one differing
+ // element each).
+ const char* const a_rdatas_rm[] = { "192.0.2.1", "192.0.2.3" };
+ const char* const sig_rdatas_rm[] = {
+ "A 5 2 3600 20120814220826 20120715220826 1234 example.com. FAKE",
+ "A 5 2 3600 20120814220826 20120715220826 5678 example.com. FAKE" };
+ ConstRRsetPtr a_rrsets_rm =
+ textToRRset("www.example.com. 1076895760 IN A "
+ + string(a_rdatas_rm[0]) + "\n"
+ + "www.example.com. 1076895760 IN A "
+ + string(a_rdatas_rm[1]));
+ ConstRRsetPtr rrsig_rrsets_rm =
+ textToRRset("www.example.com. 1076895760 IN RRSIG "
+ + string(sig_rdatas_rm[0]) + "\n"
+ + "www.example.com. 1076895760 IN RRSIG "
+ + string(sig_rdatas_rm[1]));
+
+ // A similar cycle as in the mergeCreate test.
+ for (int i = 1; i < 4; ++i) {
+ for (int j = 1; j < 4; ++j) {
+ SCOPED_TRACE("creating subtract case " + lexical_cast<string>(i) +
+ ", " + lexical_cast<string>(j));
+ // Create old rdataset
+ SegmentObjectHolder<RdataSet, RRClass> holder1(mem_sgmt_, rrclass);
+ holder1.set(RdataSet::create(mem_sgmt_, encoder_,
+ (i & 1) ? a_rrsets : null_rrset,
+ (i & 2) ? rrsig_rrsets : null_rrset));
+ // Create subtracted rdataset, from the old one and RRsets
+ SegmentObjectHolder<RdataSet, RRClass> holder2(mem_sgmt_, rrclass);
+ holder2.set(RdataSet::subtract(mem_sgmt_, encoder_,
+ (j & 1) ? a_rrsets_rm : null_rrset,
+ (j & 2) ? rrsig_rrsets_rm : null_rrset,
+ *holder1.get()));
+
+ // Set up the expected data for the case.
+ vector<string> expected_rdata;
+ if (i & 1) {
+ if (!(j & 1)) { // Not removed the other
+ expected_rdata.push_back(a_rdatas[0]);
+ }
+ expected_rdata.push_back(a_rdatas[1]);
+ }
+ vector<string> expected_sigs;
+ if (i & 2) {
+ if (!(j & 2)) { // Not removed the other
+ expected_sigs.push_back(sig_rdatas[0]);
+ }
+ expected_sigs.push_back(sig_rdatas[1]);
+ }
+
+ // Then perform the check
+ checkRdataSet(*holder2.get(), expected_rdata, expected_sigs);
+ }
+ }
+ // Reusing the data we have, test some corner cases.
+ SegmentObjectHolder<RdataSet, RRClass> holder_old(mem_sgmt_, rrclass);
+ holder_old.set(RdataSet::create(mem_sgmt_, encoder_, a_rrsets,
+ rrsig_rrsets));
+
+ // It throws if no Rdata passed.
+ EXPECT_THROW(RdataSet::subtract(mem_sgmt_, encoder_, null_rrset,
+ null_rrset, *holder_old.get()),
+ isc::BadValue);
+
+ // If we remove everything, it returns NULL
+ EXPECT_EQ(NULL, RdataSet::subtract(mem_sgmt_, encoder_, a_rrsets,
+ rrsig_rrsets, *holder_old.get()));
+}
+
TEST_F(RdataSetTest, duplicate) {
// Create RRset and RRSIG containing duplicate RDATA.
ConstRRsetPtr dup_rrset =
@@ -241,16 +324,14 @@ TEST_F(RdataSetTest, duplicate) {
// After suppressing duplicates, it should be the same as the default
// RdataSet. Check that.
- SegmentObjectHolder<RdataSet, RRClass> holder1(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, dup_rrset, dup_rrsig), rrclass);
+ SegmentObjectHolder<RdataSet, RRClass> holder1(mem_sgmt_, rrclass);
+ holder1.set(RdataSet::create(mem_sgmt_, encoder_, dup_rrset, dup_rrsig));
checkRdataSet(*holder1.get(), def_rdata_txt_, def_rrsig_txt_);
// Confirm the same thing for the merge mode.
- SegmentObjectHolder<RdataSet, RRClass> holder2(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, a_rrset_, rrsig_rrset_,
- holder1.get()), rrclass);
+ SegmentObjectHolder<RdataSet, RRClass> holder2(mem_sgmt_, rrclass);
+ holder2.set(RdataSet::create(mem_sgmt_, encoder_, a_rrset_, rrsig_rrset_,
+ holder1.get()));
checkRdataSet(*holder2.get(), def_rdata_txt_, def_rrsig_txt_);
}
@@ -275,24 +356,21 @@ TEST_F(RdataSetTest, getNext) {
TEST_F(RdataSetTest, find) {
// Create some RdataSets and make a chain of them.
- SegmentObjectHolder<RdataSet, RRClass> holder1(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, a_rrset_, ConstRRsetPtr()),
- RRClass::IN());
+ SegmentObjectHolder<RdataSet, RRClass> holder1(mem_sgmt_, RRClass::IN());
+ holder1.set(RdataSet::create(mem_sgmt_, encoder_, a_rrset_,
+ ConstRRsetPtr()));
ConstRRsetPtr aaaa_rrset =
textToRRset("www.example.com. 1076895760 IN AAAA 2001:db8::1");
- SegmentObjectHolder<RdataSet, RRClass> holder2(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, aaaa_rrset, ConstRRsetPtr()),
- RRClass::IN());
+ SegmentObjectHolder<RdataSet, RRClass> holder2(mem_sgmt_, RRClass::IN());
+ holder2.set(RdataSet::create(mem_sgmt_, encoder_, aaaa_rrset,
+ ConstRRsetPtr()));
ConstRRsetPtr sigonly_rrset =
textToRRset("www.example.com. 1076895760 IN RRSIG "
"TXT 5 2 3600 20120814220826 20120715220826 "
"1234 example.com. FAKE");
- SegmentObjectHolder<RdataSet, RRClass> holder3(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, ConstRRsetPtr(), sigonly_rrset),
- RRClass::IN());
+ SegmentObjectHolder<RdataSet, RRClass> holder3(mem_sgmt_, RRClass::IN());
+ holder3.set(RdataSet::create(mem_sgmt_, encoder_, ConstRRsetPtr(),
+ sigonly_rrset));
RdataSet* rdataset_a = holder1.get();
RdataSet* rdataset_aaaa = holder2.get();
@@ -376,10 +454,8 @@ TEST_F(RdataSetTest, createManyRRs) {
TEST_F(RdataSetTest, mergeCreateManyRRs) {
ConstRRsetPtr rrset = textToRRset("example.com. 3600 IN TXT some-text");
- SegmentObjectHolder<RdataSet, RRClass> holder(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, rrset, ConstRRsetPtr()),
- RRClass::IN());
+ SegmentObjectHolder<RdataSet, RRClass> holder(mem_sgmt_, RRClass::IN());
+ holder.set(RdataSet::create(mem_sgmt_, encoder_, rrset, ConstRRsetPtr()));
checkCreateManyRRs(boost::bind(&RdataSet::create, _1, _2, _3, _4,
holder.get()), rrset->getRdataCount());
@@ -474,10 +550,8 @@ TEST_F(RdataSetTest, mergeCreateManyRRSIGs) {
ConstRRsetPtr rrsig = textToRRset(
"example.com. 3600 IN RRSIG A 5 2 3600 20120814220826 20120715220826 "
"1234 example.com. FAKEFAKE");
- SegmentObjectHolder<RdataSet, RRClass> holder(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, ConstRRsetPtr(), rrsig),
- rrclass);
+ SegmentObjectHolder<RdataSet, RRClass> holder(mem_sgmt_, rrclass);
+ holder.set(RdataSet::create(mem_sgmt_, encoder_, ConstRRsetPtr(), rrsig));
checkCreateManyRRSIGs(boost::bind(&RdataSet::create, _1, _2, _3, _4,
holder.get()), rrsig->getRdataCount());
@@ -543,12 +617,10 @@ TEST_F(RdataSetTest, badCreate) {
TEST_F(RdataSetTest, badMergeCreate) {
// The 'old RdataSet' for merge. Its content doesn't matter much; the test
// should trigger exception before examining it except for the last checks.
- SegmentObjectHolder<RdataSet, RRClass> holder(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_,
+ SegmentObjectHolder<RdataSet, RRClass> holder(mem_sgmt_, RRClass::IN());
+ holder.set(RdataSet::create(mem_sgmt_, encoder_,
textToRRset("www.example.com. 0 IN AAAA 2001:db8::1"),
- ConstRRsetPtr()),
- RRClass::IN());
+ ConstRRsetPtr()));
checkBadCreate(boost::bind(&RdataSet::create, _1, _2, _3, _4,
holder.get()));
@@ -585,9 +657,8 @@ TEST_F(RdataSetTest, varyingTTL) {
RdataSet::destroy(mem_sgmt_, rdataset, rrclass);
// RRSIG's TTL is smaller
- SegmentObjectHolder<RdataSet, RRClass> holder1(
- mem_sgmt_,
- RdataSet::create(mem_sgmt_, encoder_, aaaa_large, sig_small), rrclass);
+ SegmentObjectHolder<RdataSet, RRClass> holder1(mem_sgmt_, rrclass);
+ holder1.set(RdataSet::create(mem_sgmt_, encoder_, aaaa_large, sig_small));
EXPECT_EQ(RRTTL(10), restoreTTL(holder1.get()->getTTLData()));
// Merging another RRset (w/o sig) that has larger TTL
@@ -626,4 +697,58 @@ TEST_F(RdataSetTest, varyingTTL) {
EXPECT_EQ(RRTTL(5), restoreTTL(rdataset->getTTLData()));
RdataSet::destroy(mem_sgmt_, rdataset, rrclass);
}
+
+// Creation of rdataset with bad params, with create and subtract
+TEST_F(RdataSetTest, badParams) {
+ const ConstRRsetPtr empty_rrset(new RRset(Name("www.example.com"),
+ RRClass::IN(), RRType::A(),
+ RRTTL(3600)));
+ const ConstRRsetPtr empty_rrsig(new RRset(Name("www.example.com"),
+ RRClass::IN(), RRType::RRSIG(),
+ RRTTL(3600)));
+ const ConstRRsetPtr a_rrset = textToRRset("www.example.com. 3600 IN A "
+ "192.0.2.1");
+ const ConstRRsetPtr aaaa_rrset = textToRRset("www.example.com. 3600 IN AAAA "
+ "2001:db8::1");
+ const ConstRRsetPtr sig_rrset = textToRRset("www.example.com. 3600 IN RRSIG "
+ "A 5 2 3600 20120814220826 "
+ "20120715220826 1234 "
+ "example.com. FAKE");
+ const ConstRRsetPtr sig_rrset_ch = textToRRset("www.example.com. 3600 CH RRSIG "
+ "A 5 2 3600 20120814220826 "
+ "20120715220826 1234 "
+ "example.com. FAKE",
+ RRClass::CH());
+ SegmentObjectHolder<RdataSet, RRClass> holder(mem_sgmt_, rrclass);
+ holder.set(RdataSet::create(mem_sgmt_, encoder_, a_rrset, sig_rrset));
+ // Empty RRset as rdata
+ EXPECT_THROW(RdataSet::create(mem_sgmt_, encoder_, empty_rrset, sig_rrset),
+ isc::BadValue);
+ // The same for rrsig
+ EXPECT_THROW(RdataSet::create(mem_sgmt_, encoder_, a_rrset, empty_rrsig),
+ isc::BadValue);
+ // Mismatched type
+ EXPECT_THROW(RdataSet::create(mem_sgmt_, encoder_, empty_rrset, a_rrset),
+ isc::BadValue);
+ // Similar for subtract
+ EXPECT_THROW(RdataSet::subtract(mem_sgmt_, encoder_, empty_rrset,
+ sig_rrset, *holder.get()),
+ isc::BadValue);
+ EXPECT_THROW(RdataSet::subtract(mem_sgmt_, encoder_, a_rrset, empty_rrset,
+ *holder.get()),
+ isc::BadValue);
+ // Class mismatch
+ EXPECT_THROW(RdataSet::create(mem_sgmt_, encoder_, a_rrset, sig_rrset_ch),
+ isc::BadValue);
+ EXPECT_THROW(RdataSet::subtract(mem_sgmt_, encoder_, a_rrset,
+ sig_rrset_ch, *holder.get()),
+ isc::BadValue);
+ // Bad rrtype
+ EXPECT_THROW(RdataSet::create(mem_sgmt_, encoder_, aaaa_rrset,
+ ConstRRsetPtr(), holder.get()),
+ isc::BadValue);
+ EXPECT_THROW(RdataSet::subtract(mem_sgmt_, encoder_, aaaa_rrset,
+ ConstRRsetPtr(), *holder.get()),
+ isc::BadValue);
+}
}
diff --git a/src/lib/datasrc/tests/memory/rrset_collection_unittest.cc b/src/lib/datasrc/tests/memory/rrset_collection_unittest.cc
index 04e285b..f3d3934 100644
--- a/src/lib/datasrc/tests/memory/rrset_collection_unittest.cc
+++ b/src/lib/datasrc/tests/memory/rrset_collection_unittest.cc
@@ -15,15 +15,17 @@
#include <datasrc/memory/rrset_collection.h>
-#include "memory_segment_test.h"
-
#include <datasrc/memory/zone_data_loader.h>
#include <datasrc/memory/segment_object_holder.h>
#include <dns/rrttl.h>
#include <dns/rdataclass.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
+
#include <gtest/gtest.h>
+#include <boost/scoped_ptr.hpp>
+
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace std;
@@ -43,22 +45,24 @@ public:
rrclass("IN"),
origin("example.org"),
zone_file(TEST_DATA_DIR "/rrset-collection.zone"),
- zone_data_holder(mem_sgmt,
- loadZoneData(mem_sgmt, rrclass, origin, zone_file),
- rrclass),
- collection(*zone_data_holder.get(), rrclass)
- {}
+ zone_data_holder(mem_sgmt, rrclass)
+ {
+ zone_data_holder.set(loadZoneData(mem_sgmt, rrclass, origin,
+ zone_file));
+ collection.reset(new RRsetCollection(*zone_data_holder.get(),
+ rrclass));
+ }
const RRClass rrclass;
const Name origin;
std::string zone_file;
- test::MemorySegmentTest mem_sgmt;
+ test::MemorySegmentMock mem_sgmt;
SegmentObjectHolder<ZoneData, RRClass> zone_data_holder;
- RRsetCollection collection;
+ boost::scoped_ptr<RRsetCollection> collection;
};
TEST_F(RRsetCollectionTest, find) {
- const RRsetCollection& ccln = collection;
+ const RRsetCollection& ccln = *collection;
ConstRRsetPtr rrset = ccln.find(Name("www.example.org"), rrclass,
RRType::A());
EXPECT_TRUE(rrset);
diff --git a/src/lib/datasrc/tests/memory/segment_object_holder_unittest.cc b/src/lib/datasrc/tests/memory/segment_object_holder_unittest.cc
index d27e364..73aa18b 100644
--- a/src/lib/datasrc/tests/memory/segment_object_holder_unittest.cc
+++ b/src/lib/datasrc/tests/memory/segment_object_holder_unittest.cc
@@ -12,10 +12,17 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <config.h>
+
#include <util/memory_segment_local.h>
+#include <util/memory_segment_mapped.h>
#include <datasrc/memory/segment_object_holder.h>
+#ifdef USE_SHARED_MEMORY
+#include <boost/interprocess/managed_mapped_file.hpp>
+#endif
+
#include <gtest/gtest.h>
using namespace isc::util;
@@ -24,12 +31,14 @@ using namespace isc::datasrc::memory::detail;
namespace {
const int TEST_ARG_VAL = 42; // arbitrary chosen magic number
+const char* const mapped_file = TEST_DATA_BUILDDIR "/test.mapped";
class TestObject {
public:
static void destroy(MemorySegment& sgmt, TestObject* obj, int arg) {
sgmt.deallocate(obj, sizeof(*obj));
EXPECT_EQ(TEST_ARG_VAL, arg);
+ EXPECT_TRUE(obj);
}
};
@@ -42,7 +51,8 @@ useHolder(MemorySegment& sgmt, TestObject* obj, bool release) {
// deallocate().
typedef SegmentObjectHolder<TestObject, int> HolderType;
- HolderType holder(sgmt, obj, TEST_ARG_VAL);
+ HolderType holder(sgmt, TEST_ARG_VAL);
+ holder.set(obj);
EXPECT_EQ(obj, holder.get());
if (release) {
EXPECT_EQ(obj, holder.release());
@@ -64,4 +74,70 @@ TEST(SegmentObjectHolderTest, foo) {
useHolder(sgmt, obj, false);
EXPECT_TRUE(sgmt.allMemoryDeallocated());
}
+
+// Test nothing bad happens if the holder is not set before it is destroyed
+TEST(SegmentObjectHolderTest, destroyNotSet) {
+ MemorySegmentLocal sgmt;
+ {
+ typedef SegmentObjectHolder<TestObject, int> HolderType;
+ HolderType holder(sgmt, TEST_ARG_VAL);
+ }
+ EXPECT_TRUE(sgmt.allMemoryDeallocated());
+}
+
+#ifdef USE_SHARED_MEMORY
+// Keep allocating bigger and bigger chunks of data until the allocation
+// fails with growing the segment.
+void
+allocateUntilGrows(MemorySegment& segment, size_t& current_size) {
+ // Create an object that will not be explicitly deallocated.
+ // It must be deallocated by the segment holder and even in case
+ // the position moved.
+ void *object_memory = segment.allocate(sizeof(TestObject));
+ TestObject* object = new(object_memory) TestObject;
+ SegmentObjectHolder<TestObject, int> holder(segment, TEST_ARG_VAL);
+ holder.set(object);
+ while (true) {
+ void* data = segment.allocate(current_size);
+ segment.deallocate(data, current_size);
+ current_size *= 2;
+ }
+}
+
+// Check that the segment thing releases stuff even in case it throws
+// SegmentGrown exception and the thing moves address
+TEST(SegmentObjectHolderTest, grow) {
+ MemorySegmentMapped segment(mapped_file,
+ isc::util::MemorySegmentMapped::CREATE_ONLY);
+ // Allocate a bit of memory, to get a unique address
+ void* mark = segment.allocate(1);
+ segment.setNamedAddress("mark", mark);
+
+ // We'd like to cause 'mark' will be mapped at a different address on
+ // MemorySegmentGrown; there doesn't seem to be a reliable and safe way
+ // to cause this situation, but opening another mapped region seems to
+ // often work in practice. We use Boost managed_mapped_file directly
+ // to ignore the imposed file lock with MemorySegmentMapped.
+ using boost::interprocess::managed_mapped_file;
+ using boost::interprocess::open_only;
+ managed_mapped_file mapped_sgmt(open_only, mapped_file);
+
+ // Try allocating bigger and bigger chunks of data until the segment
+ // actually relocates
+ size_t alloc_size = 1024;
+ EXPECT_THROW(allocateUntilGrows(segment, alloc_size), MemorySegmentGrown);
+ // Confirm it's now mapped at a different address.
+ EXPECT_NE(mark, segment.getNamedAddress("mark").second)
+ << "portability assumption for the test doesn't hold; "
+ "disable the test by setting env variable GTEST_FILTER to "
+ "'-SegmentObjectHolderTest.grow'";
+ mark = segment.getNamedAddress("mark").second;
+ segment.clearNamedAddress("mark");
+ segment.deallocate(mark, 1);
+ EXPECT_TRUE(segment.allMemoryDeallocated());
+ // Remove the file
+ EXPECT_EQ(0, unlink(mapped_file));
+}
+#endif
+
}
diff --git a/src/lib/datasrc/tests/memory/testdata/Makefile.am b/src/lib/datasrc/tests/memory/testdata/Makefile.am
index b076837..da27482 100644
--- a/src/lib/datasrc/tests/memory/testdata/Makefile.am
+++ b/src/lib/datasrc/tests/memory/testdata/Makefile.am
@@ -29,6 +29,7 @@ EXTRA_DIST += example.org-rrsigs.zone
EXTRA_DIST += example.org-wildcard-dname.zone
EXTRA_DIST += example.org-wildcard-ns.zone
EXTRA_DIST += example.org-wildcard-nsec3.zone
+EXTRA_DIST += template.zone
EXTRA_DIST += rrset-collection.zone
EXTRA_DIST += 2503-test.zone
diff --git a/src/lib/datasrc/tests/memory/testdata/template.zone b/src/lib/datasrc/tests/memory/testdata/template.zone
new file mode 100644
index 0000000..d8ae6d8
--- /dev/null
+++ b/src/lib/datasrc/tests/memory/testdata/template.zone
@@ -0,0 +1,4 @@
+; a zone file that can be used with any origin.
+
+@ 3600 IN SOA . . 1 0 0 0 0
+@ 3600 IN NS ns1
diff --git a/src/lib/datasrc/tests/memory/treenode_rrset_unittest.cc b/src/lib/datasrc/tests/memory/treenode_rrset_unittest.cc
index 921ca68..4d1f6e1 100644
--- a/src/lib/datasrc/tests/memory/treenode_rrset_unittest.cc
+++ b/src/lib/datasrc/tests/memory/treenode_rrset_unittest.cc
@@ -661,7 +661,6 @@ TEST_F(TreeNodeRRsetTest, unexpectedMethods) {
TreeNodeRRset rrset(rrclass_, www_node_, a_rdataset_, true);
EXPECT_THROW(rrset.setTTL(RRTTL(0)), isc::Unexpected);
- EXPECT_THROW(rrset.setName(Name("example")), isc::Unexpected);
EXPECT_THROW(rrset.addRdata(createRdata(RRType::A(), rrclass_, "0.0.0.0")),
isc::Unexpected);
RdataPtr sig_rdata = createRdata(
diff --git a/src/lib/datasrc/tests/memory/zone_data_loader_unittest.cc b/src/lib/datasrc/tests/memory/zone_data_loader_unittest.cc
index abc6f13..0e5677c 100644
--- a/src/lib/datasrc/tests/memory/zone_data_loader_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_data_loader_unittest.cc
@@ -12,22 +12,35 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <config.h>
+
#include <datasrc/memory/zone_data_loader.h>
#include <datasrc/memory/rdataset.h>
#include <datasrc/memory/zone_data.h>
#include <datasrc/memory/zone_data_updater.h>
+#include <datasrc/memory/segment_object_holder.h>
+#include <datasrc/zone_iterator.h>
#include <util/buffer.h>
#include <dns/name.h>
#include <dns/rrclass.h>
+#include <dns/rdataclass.h>
+#ifdef USE_SHARED_MEMORY
+#include <util/memory_segment_mapped.h>
+#endif
+#include <util/memory_segment_local.h>
-#include "memory_segment_test.h"
+#include <datasrc/tests/memory/memory_segment_mock.h>
#include <gtest/gtest.h>
using namespace isc::dns;
using namespace isc::datasrc::memory;
+#ifdef USE_SHARED_MEMORY
+using isc::util::MemorySegmentMapped;
+#endif
+using isc::datasrc::memory::detail::SegmentObjectHolder;
namespace {
@@ -41,7 +54,7 @@ protected:
EXPECT_TRUE(mem_sgmt_.allMemoryDeallocated()); // catch any leak here.
}
const RRClass zclass_;
- test::MemorySegmentTest mem_sgmt_;
+ test::MemorySegmentMock mem_sgmt_;
ZoneData* zone_data_;
};
@@ -73,4 +86,35 @@ TEST_F(ZoneDataLoaderTest, zoneMinTTL) {
EXPECT_EQ(RRTTL(1200), RRTTL(b));
}
+// Load bunch of small zones, hoping some of the relocation will happen
+// during the memory creation, not only Rdata creation.
+// Note: this doesn't even compile unless USE_SHARED_MEMORY is defined.
+#ifdef USE_SHARED_MEMORY
+TEST(ZoneDataLoaterTest, relocate) {
+ const char* const mapped_file = TEST_DATA_BUILDDIR "/test.mapped";
+ MemorySegmentMapped segment(mapped_file,
+ isc::util::MemorySegmentMapped::CREATE_ONLY,
+ 4096);
+ const size_t zone_count = 10000;
+ typedef SegmentObjectHolder<ZoneData, RRClass> Holder;
+ typedef boost::shared_ptr<Holder> HolderPtr;
+ std::vector<HolderPtr> zones;
+ for (size_t i = 0; i < zone_count; ++i) {
+ // Load some zone
+ ZoneData* data = loadZoneData(segment, RRClass::IN(),
+ Name("example.org"),
+ TEST_DATA_DIR
+ "/example.org-nsec3-signed.zone");
+ // Store it, so it is cleaned up later
+ zones.push_back(HolderPtr(new Holder(segment, RRClass::IN())));
+ zones.back()->set(data);
+
+ }
+ // Deallocate all the zones now.
+ zones.clear();
+ EXPECT_TRUE(segment.allMemoryDeallocated());
+ EXPECT_EQ(0, unlink(mapped_file));
+}
+#endif
+
}
diff --git a/src/lib/datasrc/tests/memory/zone_data_unittest.cc b/src/lib/datasrc/tests/memory/zone_data_unittest.cc
index ffbd0f6..54a0fc4 100644
--- a/src/lib/datasrc/tests/memory/zone_data_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_data_unittest.cc
@@ -16,8 +16,6 @@
#include <datasrc/memory/rdata_serialization.h>
#include <datasrc/memory/rdataset.h>
-#include "memory_segment_test.h"
-
#include <dns/rdataclass.h>
#include <exceptions/exceptions.h>
@@ -30,6 +28,7 @@
#include <dns/rrttl.h>
#include <testutils/dnsmessage_test.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
#include <gtest/gtest.h>
@@ -73,7 +72,7 @@ protected:
EXPECT_TRUE(mem_sgmt_.allMemoryDeallocated());
}
- MemorySegmentTest mem_sgmt_;
+ MemorySegmentMock mem_sgmt_;
NSEC3Data* nsec3_data_;
const generic::NSEC3PARAM param_rdata_, param_rdata_nosalt_,
param_rdata_largesalt_;
@@ -88,7 +87,7 @@ protected:
// Shared by both test cases using NSEC3 and NSEC3PARAM Rdata
template <typename RdataType>
void
-checkNSEC3Data(MemorySegmentTest& mem_sgmt,
+checkNSEC3Data(MemorySegmentMock& mem_sgmt,
const Name& zone_name,
const RdataType& expect_rdata)
{
@@ -278,4 +277,14 @@ TEST_F(ZoneDataTest, minTTL) {
zone_data_->setMinTTL(1200);
EXPECT_EQ(RRTTL(1200), createRRTTL(zone_data_->getMinTTLData()));
}
+
+TEST_F(ZoneDataTest, emptyData) {
+ // normally create zone data are never "empty"
+ EXPECT_FALSE(zone_data_->isEmpty());
+
+ // zone data instance created by the special create() is made "empty".
+ ZoneData* empty_data = ZoneData::create(mem_sgmt_);
+ EXPECT_TRUE(empty_data->isEmpty());
+ ZoneData::destroy(mem_sgmt_, empty_data, RRClass::IN());
+}
}
diff --git a/src/lib/datasrc/tests/memory/zone_data_updater_unittest.cc b/src/lib/datasrc/tests/memory/zone_data_updater_unittest.cc
index 6399923..d4918fa 100644
--- a/src/lib/datasrc/tests/memory/zone_data_updater_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_data_updater_unittest.cc
@@ -12,6 +12,8 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <config.h>
+
#include <datasrc/memory/zone_data_updater.h>
#include <datasrc/memory/rdataset.h>
#include <datasrc/memory/zone_data.h>
@@ -25,11 +27,14 @@
#include <dns/rrset.h>
#include <dns/rrttl.h>
-#include "memory_segment_test.h"
+#include <util/memory_segment_local.h>
+#include <util/memory_segment_mapped.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
+#include <boost/lexical_cast.hpp>
#include <cassert>
@@ -39,71 +44,158 @@ using namespace isc::datasrc::memory;
namespace {
-class ZoneDataUpdaterTest : public ::testing::Test {
+const char* const mapped_file = TEST_DATA_BUILDDIR "/test.mapped";
+
+// An abstract factory class for the segments. We want fresh segment for each
+// test, so we have different factories for them.
+class SegmentCreator {
+public:
+ virtual ~SegmentCreator() {}
+ typedef boost::shared_ptr<isc::util::MemorySegment> SegmentPtr;
+ // Create the segment.
+ virtual SegmentPtr create() const = 0;
+ // Clean-up after the test. Most of them will be just NOP (the default),
+ // but the file-mapped one needs to remove the file.
+ virtual void cleanup() const {}
+};
+
+ZoneNode*
+getNode(isc::util::MemorySegment& mem_sgmt, const Name& name,
+ ZoneData* zone_data)
+{
+ ZoneNode* node = NULL;
+ zone_data->insertName(mem_sgmt, name, &node);
+ EXPECT_NE(static_cast<ZoneNode*>(NULL), node);
+ return (node);
+}
+
+class ZoneDataUpdaterTest : public ::testing::TestWithParam<SegmentCreator*> {
protected:
ZoneDataUpdaterTest() :
zname_("example.org"), zclass_(RRClass::IN()),
- zone_data_(ZoneData::create(mem_sgmt_, zname_)),
- updater_(new ZoneDataUpdater(mem_sgmt_, zclass_, zname_, *zone_data_))
- {}
+ mem_sgmt_(GetParam()->create())
+ {
+ ZoneData* data = ZoneData::create(*mem_sgmt_, zname_);
+ mem_sgmt_->setNamedAddress("Test zone data", data);
+ updater_.reset(new ZoneDataUpdater(*mem_sgmt_, zclass_, zname_,
+ *data));
+ }
~ZoneDataUpdaterTest() {
- if (zone_data_ != NULL) {
- ZoneData::destroy(mem_sgmt_, zone_data_, zclass_);
- }
- if (!mem_sgmt_.allMemoryDeallocated()) {
+ assert(updater_);
+ ZoneData::destroy(*mem_sgmt_, getZoneData(), zclass_);
+ // Release the updater, so it frees all memory inside the segment too
+ updater_.reset();
+ mem_sgmt_->clearNamedAddress("Test zone data");
+ if (!mem_sgmt_->allMemoryDeallocated()) {
ADD_FAILURE() << "Memory leak detected";
}
+ GetParam()->cleanup();
}
void clearZoneData() {
- assert(zone_data_ != NULL);
- ZoneData::destroy(mem_sgmt_, zone_data_, zclass_);
- zone_data_ = ZoneData::create(mem_sgmt_, zname_);
- updater_.reset(new ZoneDataUpdater(mem_sgmt_, zclass_, zname_,
- *zone_data_));
+ assert(updater_);
+ ZoneData::destroy(*mem_sgmt_, getZoneData(), zclass_);
+ mem_sgmt_->clearNamedAddress("Test zone data");
+ updater_.reset();
+ ZoneData* data = ZoneData::create(*mem_sgmt_, zname_);
+ mem_sgmt_->setNamedAddress("Test zone data", data);
+ updater_.reset(new ZoneDataUpdater(*mem_sgmt_, zclass_, zname_,
+ *data));
+ }
+
+ ZoneData* getZoneData() {
+ return (static_cast<ZoneData*>(
+ mem_sgmt_->getNamedAddress("Test zone data").second));
}
const Name zname_;
const RRClass zclass_;
- test::MemorySegmentTest mem_sgmt_;
- ZoneData* zone_data_;
+ boost::shared_ptr<isc::util::MemorySegment> mem_sgmt_;
boost::scoped_ptr<ZoneDataUpdater> updater_;
};
-TEST_F(ZoneDataUpdaterTest, bothNull) {
+class TestSegmentCreator : public SegmentCreator {
+public:
+ virtual SegmentPtr create() const {
+ return (SegmentPtr(new test::MemorySegmentMock));
+ }
+};
+
+TestSegmentCreator test_segment_creator;
+
+INSTANTIATE_TEST_CASE_P(TestSegment, ZoneDataUpdaterTest,
+ ::testing::Values(static_cast<SegmentCreator*>(
+ &test_segment_creator)));
+
+class MemorySegmentCreator : public SegmentCreator {
+public:
+ virtual SegmentPtr create() const {
+ // We are not really supposed to create the segment directly in real
+ // code, but it should be OK inside tests.
+ return (SegmentPtr(new isc::util::MemorySegmentLocal));
+ }
+};
+
+MemorySegmentCreator memory_segment_creator;
+
+INSTANTIATE_TEST_CASE_P(LocalSegment, ZoneDataUpdaterTest,
+ ::testing::Values(static_cast<SegmentCreator*>(
+ &memory_segment_creator)));
+
+#ifdef USE_SHARED_MEMORY
+class MappedSegmentCreator : public SegmentCreator {
+public:
+ MappedSegmentCreator(size_t initial_size =
+ isc::util::MemorySegmentMapped::INITIAL_SIZE) :
+ initial_size_(initial_size)
+ {}
+ virtual SegmentPtr create() const {
+ return (SegmentPtr(new isc::util::MemorySegmentMapped(
+ mapped_file,
+ isc::util::MemorySegmentMapped::CREATE_ONLY,
+ initial_size_)));
+ }
+ virtual void cleanup() const {
+ EXPECT_EQ(0, unlink(mapped_file));
+ }
+private:
+ const size_t initial_size_;
+};
+
+// There should be no initialization fiasco there. We only set int value inside
+// and don't use it until the create() is called.
+MappedSegmentCreator small_creator(4092), default_creator;
+
+INSTANTIATE_TEST_CASE_P(MappedSegment, ZoneDataUpdaterTest, ::testing::Values(
+ static_cast<SegmentCreator*>(&small_creator),
+ static_cast<SegmentCreator*>(&default_creator)));
+#endif
+
+TEST_P(ZoneDataUpdaterTest, bothNull) {
// At least either covered RRset or RRSIG must be non NULL.
EXPECT_THROW(updater_->add(ConstRRsetPtr(), ConstRRsetPtr()),
ZoneDataUpdater::NullRRset);
}
-ZoneNode*
-getNode(isc::util::MemorySegment& mem_sgmt, const Name& name,
- ZoneData* zone_data)
-{
- ZoneNode* node = NULL;
- zone_data->insertName(mem_sgmt, name, &node);
- EXPECT_NE(static_cast<ZoneNode*>(NULL), node);
- return (node);
-}
-
-TEST_F(ZoneDataUpdaterTest, zoneMinTTL) {
+TEST_P(ZoneDataUpdaterTest, zoneMinTTL) {
// If we add SOA, zone's min TTL will be updated.
updater_->add(textToRRset(
"example.org. 3600 IN SOA . . 0 0 0 0 1200",
zclass_, zname_),
ConstRRsetPtr());
- isc::util::InputBuffer b(zone_data_->getMinTTLData(), sizeof(uint32_t));
+ isc::util::InputBuffer b(getZoneData()->getMinTTLData(), sizeof(uint32_t));
EXPECT_EQ(RRTTL(1200), RRTTL(b));
}
-TEST_F(ZoneDataUpdaterTest, rrsigOnly) {
+TEST_P(ZoneDataUpdaterTest, rrsigOnly) {
// RRSIG that doesn't have covered RRset can be added. The resulting
// rdataset won't have "normal" RDATA but sig RDATA.
updater_->add(ConstRRsetPtr(), textToRRset(
"www.example.org. 3600 IN RRSIG A 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- ZoneNode* node = getNode(mem_sgmt_, Name("www.example.org"), zone_data_);
+ ZoneNode* node = getNode(*mem_sgmt_, Name("www.example.org"),
+ getZoneData());
const RdataSet* rdset = node->getData();
ASSERT_NE(static_cast<RdataSet*>(NULL), rdset);
rdset = RdataSet::find(rdset, RRType::A(), true);
@@ -121,7 +213,7 @@ TEST_F(ZoneDataUpdaterTest, rrsigOnly) {
updater_->add(ConstRRsetPtr(), textToRRset(
"*.wild.example.org. 3600 IN RRSIG A 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- node = getNode(mem_sgmt_, Name("wild.example.org"), zone_data_);
+ node = getNode(*mem_sgmt_, Name("wild.example.org"), getZoneData());
EXPECT_TRUE(node->getFlag(ZoneData::WILDCARD_NODE));
// Simply adding RRSIG covering (delegating NS) shouldn't enable callback
@@ -129,14 +221,14 @@ TEST_F(ZoneDataUpdaterTest, rrsigOnly) {
updater_->add(ConstRRsetPtr(), textToRRset(
"child.example.org. 3600 IN RRSIG NS 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- node = getNode(mem_sgmt_, Name("child.example.org"), zone_data_);
+ node = getNode(*mem_sgmt_, Name("child.example.org"), getZoneData());
EXPECT_FALSE(node->getFlag(ZoneNode::FLAG_CALLBACK));
// Same for DNAME
updater_->add(ConstRRsetPtr(), textToRRset(
"dname.example.org. 3600 IN RRSIG DNAME 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- node = getNode(mem_sgmt_, Name("dname.example.org"), zone_data_);
+ node = getNode(*mem_sgmt_, Name("dname.example.org"), getZoneData());
EXPECT_FALSE(node->getFlag(ZoneNode::FLAG_CALLBACK));
// Likewise, RRSIG for NSEC3PARAM alone shouldn't make the zone
@@ -144,13 +236,13 @@ TEST_F(ZoneDataUpdaterTest, rrsigOnly) {
updater_->add(ConstRRsetPtr(), textToRRset(
"example.org. 3600 IN RRSIG NSEC3PARAM 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- EXPECT_FALSE(zone_data_->isNSEC3Signed());
+ EXPECT_FALSE(getZoneData()->isNSEC3Signed());
// And same for (RRSIG for) NSEC and "is signed".
updater_->add(ConstRRsetPtr(), textToRRset(
"example.org. 3600 IN RRSIG NSEC 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- EXPECT_FALSE(zone_data_->isSigned());
+ EXPECT_FALSE(getZoneData()->isSigned());
}
// Commonly used checks for rrsigForNSEC3Only
@@ -168,7 +260,7 @@ checkNSEC3Rdata(isc::util::MemorySegment& mem_sgmt, const Name& name,
EXPECT_EQ(1, rdset->getSigRdataCount());
}
-TEST_F(ZoneDataUpdaterTest, rrsigForNSEC3Only) {
+TEST_P(ZoneDataUpdaterTest, rrsigForNSEC3Only) {
// Adding only RRSIG covering NSEC3 is tricky. It should go to the
// separate NSEC3 tree, but the separate space is only created when
// NSEC3 or NSEC3PARAM is added. So, in many cases RRSIG-only is allowed,
@@ -183,12 +275,12 @@ TEST_F(ZoneDataUpdaterTest, rrsigForNSEC3Only) {
textToRRset(
"example.org. 3600 IN RRSIG NSEC3PARAM 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- EXPECT_TRUE(zone_data_->isNSEC3Signed());
+ EXPECT_TRUE(getZoneData()->isNSEC3Signed());
updater_->add(ConstRRsetPtr(),
textToRRset(
"09GM.example.org. 3600 IN RRSIG NSEC3 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- checkNSEC3Rdata(mem_sgmt_, Name("09GM.example.org"), zone_data_);
+ checkNSEC3Rdata(*mem_sgmt_, Name("09GM.example.org"), getZoneData());
// Clear the current content of zone, then add NSEC3
clearZoneData();
@@ -201,7 +293,7 @@ TEST_F(ZoneDataUpdaterTest, rrsigForNSEC3Only) {
textToRRset(
"09GM.example.org. 3600 IN RRSIG NSEC3 5 3 3600 "
"20150420235959 20051021000000 1 example.org. FAKE"));
- checkNSEC3Rdata(mem_sgmt_, Name("09GM.example.org"), zone_data_);
+ checkNSEC3Rdata(*mem_sgmt_, Name("09GM.example.org"), getZoneData());
// If we add only RRSIG without any NSEC3 related data beforehand,
// it will be rejected; it's a limitation of the current implementation.
@@ -214,4 +306,39 @@ TEST_F(ZoneDataUpdaterTest, rrsigForNSEC3Only) {
isc::NotImplemented);
}
+// Generate many small RRsets. This tests that the underlying memory segment
+// can grow during the execution and that the updater handles that well.
+//
+// Some of the grows will happen inserting the RRSIG, some with the TXT. Or,
+// at least we hope so.
+TEST_P(ZoneDataUpdaterTest, manySmallRRsets) {
+ for (size_t i = 0; i < 32768; ++i) {
+ const std::string name(boost::lexical_cast<std::string>(i) +
+ ".example.org.");
+ updater_->add(textToRRset(name + " 3600 IN TXT " +
+ std::string(30, 'X')),
+ textToRRset(name + " 3600 IN RRSIG TXT 5 3 3600 "
+ "20150420235959 20051021000000 1 "
+ "example.org. FAKE"));
+ ZoneNode* node = getNode(*mem_sgmt_,
+ Name(boost::lexical_cast<std::string>(i) +
+ ".example.org"), getZoneData());
+ const RdataSet* rdset = node->getData();
+ ASSERT_NE(static_cast<RdataSet*>(NULL), rdset);
+ rdset = RdataSet::find(rdset, RRType::TXT(), true);
+ ASSERT_NE(static_cast<RdataSet*>(NULL), rdset);
+ EXPECT_EQ(1, rdset->getRdataCount());
+ EXPECT_EQ(1, rdset->getSigRdataCount());
+ }
+}
+
+TEST_P(ZoneDataUpdaterTest, updaterCollision) {
+ ZoneData* zone_data = ZoneData::create(*mem_sgmt_,
+ Name("another.example.com."));
+ EXPECT_THROW(ZoneDataUpdater(*mem_sgmt_, RRClass::IN(),
+ Name("another.example.com."), *zone_data),
+ isc::InvalidOperation);
+ ZoneData::destroy(*mem_sgmt_, zone_data, RRClass::IN());
+}
+
}
diff --git a/src/lib/datasrc/tests/memory/zone_finder_unittest.cc b/src/lib/datasrc/tests/memory/zone_finder_unittest.cc
index 0350ed9..d3c7f49 100644
--- a/src/lib/datasrc/tests/memory/zone_finder_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_finder_unittest.cc
@@ -12,8 +12,8 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-#include <datasrc/tests/memory/memory_segment_test.h>
-#include <datasrc/tests/memory/zone_table_segment_test.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
+#include <datasrc/tests/memory/zone_table_segment_mock.h>
#include <datasrc/tests/memory/zone_loader_util.h>
// NOTE: this faked_nsec3 inclusion (and all related code below)
@@ -44,7 +44,6 @@ using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::datasrc;
using namespace isc::testutils;
-using boost::shared_ptr;
using namespace isc::datasrc::test;
using namespace isc::datasrc::memory::test;
using namespace isc::datasrc::memory;
@@ -98,7 +97,7 @@ protected:
origin_("example.org"),
zone_data_(ZoneData::create(mem_sgmt_, origin_)),
zone_finder_(*zone_data_, class_),
- updater_(mem_sgmt_, class_, origin_, *zone_data_)
+ updater_(new ZoneDataUpdater(mem_sgmt_, class_, origin_, *zone_data_))
{
// Build test RRsets. Below, we construct an RRset for
// each textual RR(s) of zone_data, and assign it to the corresponding
@@ -196,7 +195,7 @@ protected:
}
void addToZoneData(const ConstRRsetPtr rrset) {
- updater_.add(rrset, rrset->getRRsig());
+ updater_->add(rrset, rrset->getRRsig());
}
/// \brief expensive rrset converter
@@ -221,10 +220,10 @@ protected:
const RRClass class_;
const Name origin_;
// The zone finder to torture by tests
- MemorySegmentTest mem_sgmt_;
+ MemorySegmentMock mem_sgmt_;
memory::ZoneData* zone_data_;
memory::InMemoryZoneFinder zone_finder_;
- ZoneDataUpdater updater_;
+ boost::scoped_ptr<ZoneDataUpdater> updater_;
// Placeholder for storing RRsets to be checked with rrsetsCheck()
vector<ConstRRsetPtr> actual_rrsets_;
@@ -1549,19 +1548,19 @@ TEST_F(InMemoryZoneFinderTest, findOrphanRRSIG) {
// Add A for ns.example.org, and RRSIG-only covering TXT for the same name.
// query for the TXT should result in NXRRSET.
addToZoneData(rr_ns_a_);
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- "ns.example.org. 300 IN RRSIG TXT 5 3 300 20120814220826 "
- "20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ "ns.example.org. 300 IN RRSIG TXT 5 3 300 20120814220826 "
+ "20120715220826 1234 example.com. FAKE"));
findTest(Name("ns.example.org"), RRType::TXT(),
ZoneFinder::NXRRSET, true, ConstRRsetPtr(), expected_flags);
// Add RRSIG-only covering NSEC. This shouldn't be returned when NSEC is
// requested, whether it's for NXRRSET or NXDOMAIN
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- "ns.example.org. 300 IN RRSIG NSEC 5 3 300 "
- "20120814220826 20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ "ns.example.org. 300 IN RRSIG NSEC 5 3 300 "
+ "20120814220826 20120715220826 1234 example.com. FAKE"));
// The added RRSIG for NSEC could be used for NXRRSET but shouldn't
findTest(Name("ns.example.org"), RRType::TXT(),
ZoneFinder::NXRRSET, true, ConstRRsetPtr(),
@@ -1572,29 +1571,29 @@ TEST_F(InMemoryZoneFinderTest, findOrphanRRSIG) {
expected_flags, NULL, ZoneFinder::FIND_DNSSEC);
// RRSIG-only CNAME shouldn't be accidentally confused with real CNAME.
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- "nocname.example.org. 300 IN RRSIG CNAME 5 3 300 "
- "20120814220826 20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ "nocname.example.org. 300 IN RRSIG CNAME 5 3 300 "
+ "20120814220826 20120715220826 1234 example.com. FAKE"));
findTest(Name("nocname.example.org"), RRType::A(),
ZoneFinder::NXRRSET, true, ConstRRsetPtr(), expected_flags);
// RRSIG-only for NS wouldn't invoke delegation anyway, but we check this
// case explicitly.
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- "nodelegation.example.org. 300 IN RRSIG NS 5 3 300 "
- "20120814220826 20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ "nodelegation.example.org. 300 IN RRSIG NS 5 3 300 "
+ "20120814220826 20120715220826 1234 example.com. FAKE"));
findTest(Name("nodelegation.example.org"), RRType::A(),
ZoneFinder::NXRRSET, true, ConstRRsetPtr(), expected_flags);
findTest(Name("www.nodelegation.example.org"), RRType::A(),
ZoneFinder::NXDOMAIN, true, ConstRRsetPtr(), expected_flags);
// Same for RRSIG-only for DNAME
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- "nodname.example.org. 300 IN RRSIG DNAME 5 3 300 "
- "20120814220826 20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ "nodname.example.org. 300 IN RRSIG DNAME 5 3 300 "
+ "20120814220826 20120715220826 1234 example.com. FAKE"));
findTest(Name("www.nodname.example.org"), RRType::A(),
ZoneFinder::NXDOMAIN, true, ConstRRsetPtr(), expected_flags);
// If we have a delegation NS at this node, it will be a bit trickier,
@@ -1612,8 +1611,9 @@ TEST_F(InMemoryZoneFinderTest, findOrphanRRSIG) {
// handling)
TEST_F(InMemoryZoneFinderTest, NSECNonExistentTest) {
const Name name("example.com.");
- shared_ptr<ZoneTableSegment> ztable_segment(
- new ZoneTableSegmentTest(class_, mem_sgmt_));
+ boost::shared_ptr<ZoneTableSegment> ztable_segment(
+ new ZoneTableSegmentMock(class_, mem_sgmt_));
+ updater_.reset();
loadZoneIntoTable(*ztable_segment, name, class_,
TEST_DATA_DIR "/2504-test.zone");
InMemoryClient client(ztable_segment, class_);
@@ -1758,10 +1758,10 @@ TEST_F(InMemoryZoneFinderNSEC3Test, RRSIGOnly) {
// add an RRSIG-only NSEC3 to the NSEC3 space, and try to find it; it
// should result in an exception.
const string n8_hash = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ";
- updater_.add(ConstRRsetPtr(),
- textToRRset(
- n8_hash + ".example.org. 300 IN RRSIG NSEC3 5 3 300 "
- "20120814220826 20120715220826 1234 example.com. FAKE"));
+ updater_->add(ConstRRsetPtr(),
+ textToRRset(
+ n8_hash + ".example.org. 300 IN RRSIG NSEC3 5 3 300 "
+ "20120814220826 20120715220826 1234 example.com. FAKE"));
EXPECT_THROW(zone_finder_.findNSEC3(Name("n8.example.org"), false),
DataSourceError);
}
@@ -1774,8 +1774,9 @@ TEST_F(InMemoryZoneFinderNSEC3Test, findNSEC3MissingOrigin) {
setNSEC3HashCreator(&creator);
const Name name("example.com.");
- shared_ptr<ZoneTableSegment> ztable_segment(
- new ZoneTableSegmentTest(class_, mem_sgmt_));
+ boost::shared_ptr<ZoneTableSegment> ztable_segment(
+ new ZoneTableSegmentMock(class_, mem_sgmt_));
+ updater_.reset();
loadZoneIntoTable(*ztable_segment, name, class_,
TEST_DATA_DIR "/2503-test.zone");
InMemoryClient client(ztable_segment, class_);
diff --git a/src/lib/datasrc/tests/memory/zone_loader_util.cc b/src/lib/datasrc/tests/memory/zone_loader_util.cc
index 1bf9cfa..155871d 100644
--- a/src/lib/datasrc/tests/memory/zone_loader_util.cc
+++ b/src/lib/datasrc/tests/memory/zone_loader_util.cc
@@ -24,9 +24,6 @@
#include <cc/data.h>
-#include <boost/bind.hpp>
-#include <boost/scoped_ptr.hpp>
-
#include <string>
namespace isc {
@@ -36,19 +33,19 @@ namespace test {
void
loadZoneIntoTable(ZoneTableSegment& zt_sgmt, const dns::Name& zname,
- const dns::RRClass& zclass, const std::string& zone_file)
+ const dns::RRClass& zclass, const std::string& zone_file,
+ bool load_error_ok)
{
const isc::datasrc::internal::CacheConfig cache_conf(
"MasterFiles", NULL, *data::Element::fromJSON(
"{\"cache-enable\": true,"
" \"params\": {\"" + zname.toText() + "\": \"" + zone_file +
"\"}}"), true);
- boost::scoped_ptr<memory::ZoneWriter> writer(
- zt_sgmt.getZoneWriter(cache_conf.getLoadAction(zclass, zname),
- zname, zclass));
- writer->load();
- writer->install();
- writer->cleanup();
+ memory::ZoneWriter writer(zt_sgmt, cache_conf.getLoadAction(zclass, zname),
+ zname, zclass, load_error_ok);
+ writer.load();
+ writer.install();
+ writer.cleanup();
}
namespace {
@@ -75,12 +72,11 @@ void
loadZoneIntoTable(ZoneTableSegment& zt_sgmt, const dns::Name& zname,
const dns::RRClass& zclass, ZoneIterator& iterator)
{
- boost::scoped_ptr<memory::ZoneWriter> writer(
- zt_sgmt.getZoneWriter(IteratorLoader(zclass, zname, iterator),
- zname, zclass));
- writer->load();
- writer->install();
- writer->cleanup();
+ memory::ZoneWriter writer(zt_sgmt, IteratorLoader(zclass, zname, iterator),
+ zname, zclass, false);
+ writer.load();
+ writer.install();
+ writer.cleanup();
}
} // namespace test
diff --git a/src/lib/datasrc/tests/memory/zone_loader_util.h b/src/lib/datasrc/tests/memory/zone_loader_util.h
index 06eba87..6d1f764 100644
--- a/src/lib/datasrc/tests/memory/zone_loader_util.h
+++ b/src/lib/datasrc/tests/memory/zone_loader_util.h
@@ -33,9 +33,14 @@ namespace test {
/// This function does nothing special, simply provides a shortcut for commonly
/// used pattern that would be used in tests with a ZoneTableSegment loading
/// a zone from file into it.
+///
+/// If the optional load_error_ok parameter is set to true, it will create
+/// an internal empty zone in the table when it encounters a loading error.
+/// Otherwise ZoneLoaderException will be thrown in such cases.
void
loadZoneIntoTable(ZoneTableSegment& zt_sgmt, const dns::Name& zname,
- const dns::RRClass& zclass, const std::string& zone_file);
+ const dns::RRClass& zclass, const std::string& zone_file,
+ bool load_error_ok = false);
/// \brief A shortcut utility to load a specified zone into ZoneTableSegment
/// from a zone iterator.
diff --git a/src/lib/datasrc/tests/memory/zone_table_segment_mapped_unittest.cc b/src/lib/datasrc/tests/memory/zone_table_segment_mapped_unittest.cc
new file mode 100644
index 0000000..b770e38
--- /dev/null
+++ b/src/lib/datasrc/tests/memory/zone_table_segment_mapped_unittest.cc
@@ -0,0 +1,566 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <datasrc/memory/zone_writer.h>
+#include <datasrc/memory/zone_table_segment_mapped.h>
+#include <util/random/random_number_generator.h>
+#include <util/unittests/check_valgrind.h>
+
+#include <gtest/gtest.h>
+#include <boost/format.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <boost/interprocess/file_mapping.hpp>
+
+#include <memory>
+#include <cerrno>
+
+#include <sys/stat.h>
+
+using namespace isc::dns;
+using namespace isc::datasrc::memory;
+using namespace isc::data;
+using namespace isc::util;
+using namespace isc::util::random;
+using namespace std;
+using boost::scoped_ptr;
+
+namespace {
+
+const char* const mapped_file = TEST_DATA_BUILDDIR "/test.mapped";
+const char* const mapped_file2 = TEST_DATA_BUILDDIR "/test2.mapped";
+
+class ZoneTableSegmentMappedTest : public ::testing::Test {
+protected:
+ ZoneTableSegmentMappedTest() :
+ ztable_segment_(
+ ZoneTableSegment::create(RRClass::IN(), "mapped")),
+ config_params_(
+ Element::fromJSON(
+ "{\"mapped-file\": \"" + std::string(mapped_file) + "\"}")),
+ config_params2_(
+ Element::fromJSON(
+ "{\"mapped-file\": \"" + std::string(mapped_file2) + "\"}"))
+ {
+ EXPECT_NE(static_cast<void*>(NULL), ztable_segment_.get());
+ // Verify that a ZoneTableSegmentMapped is created.
+ ZoneTableSegmentMapped* mapped_segment =
+ dynamic_cast<ZoneTableSegmentMapped*>(ztable_segment_.get());
+ EXPECT_NE(static_cast<void*>(NULL), mapped_segment);
+
+ createTestData();
+ }
+
+ ~ZoneTableSegmentMappedTest() {
+ ZoneTableSegment::destroy(ztable_segment_.release());
+ boost::interprocess::file_mapping::remove(mapped_file);
+ boost::interprocess::file_mapping::remove(mapped_file2);
+ }
+
+ typedef std::pair<std::string, int> TestDataElement;
+
+ void createTestData() {
+ UniformRandomIntegerGenerator gen(0, INT_MAX);
+ for (int i = 0; i < 256; ++i) {
+ const string name(boost::str(boost::format("name%d") % i));
+ const int value = gen();
+ test_data_.push_back(TestDataElement(name, value));
+ }
+ }
+
+ void setupMappedFiles();
+ void addData(MemorySegment& segment);
+ bool verifyData(const MemorySegment& segment);
+
+ // Ideally, this should be something similar to a
+ // SegmentObjectHolder, not an auto_ptr.
+ std::auto_ptr<ZoneTableSegment> ztable_segment_;
+ const ConstElementPtr config_params_;
+ const ConstElementPtr config_params2_;
+ std::vector<TestDataElement> test_data_;
+};
+
+bool
+fileExists(const char* path) {
+ struct stat sb;
+ const int status = stat(path, &sb);
+ if (status != 0) {
+ EXPECT_EQ(ENOENT, errno);
+ return (false);
+ }
+ return (true);
+}
+
+void
+deleteChecksum(MemorySegment& segment) {
+ segment.clearNamedAddress("zone_table_checksum");
+}
+
+void
+corruptChecksum(MemorySegment& segment) {
+ const MemorySegment::NamedAddressResult result =
+ segment.getNamedAddress("zone_table_checksum");
+ ASSERT_TRUE(result.first);
+
+ size_t checksum = *static_cast<size_t*>(result.second);
+ ++checksum;
+ *static_cast<size_t*>(result.second) = checksum;
+}
+
+void
+deleteHeader(MemorySegment& segment) {
+ segment.clearNamedAddress("zone_table_header");
+}
+
+void
+ZoneTableSegmentMappedTest::addData(MemorySegment& segment) {
+ // For purposes of this test, we assume that the following
+ // allocations do not resize the mapped segment. For this, we have
+ // to keep the size of test data reasonably small in
+ // createTestData().
+
+ // One by one, add all the elements in test_data_.
+ for (int i = 0; i < test_data_.size(); ++i) {
+ void* ptr = segment.allocate(sizeof(int));
+ ASSERT_TRUE(ptr);
+ *static_cast<int*>(ptr) = test_data_[i].second;
+ const bool grew = segment.setNamedAddress(test_data_[i].first.c_str(),
+ ptr);
+ ASSERT_FALSE(grew);
+ }
+}
+
+bool
+ZoneTableSegmentMappedTest::verifyData(const MemorySegment& segment) {
+ // One by one, verify all the elements in test_data_ exist and have
+ // the expected values.
+ for (int i = 0; i < test_data_.size(); ++i) {
+ const MemorySegment::NamedAddressResult result =
+ segment.getNamedAddress(test_data_[i].first.c_str());
+ if (!result.first) {
+ return (false);
+ }
+ if (*static_cast<int*>(result.second) != test_data_[i].second) {
+ return (false);
+ }
+ }
+
+ return (true);
+}
+
+void
+ZoneTableSegmentMappedTest::setupMappedFiles() {
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params_);
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params2_);
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Now, clear the segment, closing the underlying mapped file.
+ ztable_segment_->clear();
+}
+
+TEST_F(ZoneTableSegmentMappedTest, getImplType) {
+ EXPECT_EQ("mapped", ztable_segment_->getImplType());
+}
+
+TEST_F(ZoneTableSegmentMappedTest, getHeaderUninitialized) {
+ // This should throw as we haven't called reset() yet.
+ EXPECT_THROW(ztable_segment_->getHeader(), isc::InvalidOperation);
+}
+
+TEST_F(ZoneTableSegmentMappedTest, getMemorySegmentUninitialized) {
+ // This should throw as we haven't called reset() yet.
+ EXPECT_THROW(ztable_segment_->getMemorySegment(), isc::InvalidOperation);
+}
+
+TEST_F(ZoneTableSegmentMappedTest, isUsableUninitialized) {
+ // isUsable() must return false by default, when the segment has not
+ // been reset() yet.
+ EXPECT_FALSE(ztable_segment_->isUsable());
+}
+
+TEST_F(ZoneTableSegmentMappedTest, isWritableUninitialized) {
+ // isWritable() must return false by default, when the segment has
+ // not been reset() yet.
+ EXPECT_FALSE(ztable_segment_->isWritable());
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetBadConfig) {
+ // Open a mapped file in create mode.
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params_);
+
+ // Populate it with some data.
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // All the following resets() with invalid configuration must
+ // provide a strong exception guarantee that the segment is still
+ // usable as before.
+
+ // NULL is passed in config params
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ ConstElementPtr());
+ }, isc::InvalidParameter);
+
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Not a map
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("42"));
+ }, isc::InvalidParameter);
+
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Empty map
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("{}"));
+ }, isc::InvalidParameter);
+
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // No "mapped-file" key
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("{\"foo\": \"bar\"}"));
+ }, isc::InvalidParameter);
+
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Value of "mapped-file" key is not a string
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("{\"mapped-file\": 42}"));
+ }, isc::InvalidParameter);
+
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, reset) {
+ // By default, the mapped file doesn't exist, so we cannot open it
+ // in READ_ONLY mode (which does not create the file).
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::READ_ONLY, config_params_);
+ }, MemorySegmentOpenError);
+
+ // The following should still throw, unaffected by the failed open.
+ EXPECT_THROW(ztable_segment_->getHeader(), isc::InvalidOperation);
+ EXPECT_THROW(ztable_segment_->getMemorySegment(), isc::InvalidOperation);
+
+ // isUsable() and isWritable() must still return false, because the
+ // segment has not been successfully reset() yet.
+ EXPECT_FALSE(ztable_segment_->isUsable());
+ EXPECT_FALSE(ztable_segment_->isWritable());
+
+ // If a Python binding passes an invalid integer as the mode,
+ // reset() should reject it.
+ EXPECT_THROW({
+ ztable_segment_->reset
+ (static_cast<ZoneTableSegment::MemorySegmentOpenMode>(1234),
+ config_params_);
+ }, isc::InvalidParameter);
+
+ // READ_WRITE mode must create the mapped file if it doesn't exist
+ // (and must not result in an exception).
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+
+ // The following method calls should no longer throw:
+ EXPECT_NO_THROW(ztable_segment_->getHeader());
+ EXPECT_NO_THROW(ztable_segment_->getMemorySegment());
+
+ // Let's try to re-open the mapped file in READ_ONLY mode. It should
+ // not fail now.
+ ztable_segment_->reset(ZoneTableSegment::READ_ONLY, config_params_);
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_FALSE(ztable_segment_->isWritable());
+
+ // Re-creating the mapped file should erase old data and should not
+ // trigger any exceptions inside reset() due to old data (such as
+ // named addresses).
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params_);
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+
+ // When we reset() with an invalid paramter and it fails, then the
+ // segment should still be usable.
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("{}"));
+ }, isc::InvalidParameter);
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // The following should not throw.
+ EXPECT_NO_THROW(ztable_segment_->getHeader());
+ EXPECT_NO_THROW(ztable_segment_->getMemorySegment());
+
+ // READ_WRITE with an existing map file ought to work too. This
+ // would use existing named addresses. This actually re-opens the
+ // currently open map.
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetCreate) {
+ // At this point, the underlying file must not exist.
+ ASSERT_FALSE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in create mode.
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params_);
+
+ ASSERT_TRUE(ztable_segment_->isUsable());
+ ASSERT_TRUE(ztable_segment_->isWritable());
+
+ // Add the data.
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Close the segment.
+ ztable_segment_->clear();
+
+ // At this point, the underlying file must still exist.
+ ASSERT_TRUE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in create mode again.
+ ztable_segment_->reset(ZoneTableSegment::CREATE, config_params_);
+
+ // The old data should be gone.
+ EXPECT_FALSE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetReadWrite) {
+ // At this point, the underlying file must not exist.
+ ASSERT_FALSE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in read+write mode.
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ ASSERT_TRUE(ztable_segment_->isUsable());
+ ASSERT_TRUE(ztable_segment_->isWritable());
+
+ // Add the data.
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Close the segment.
+ ztable_segment_->clear();
+
+ // At this point, the underlying file must still exist.
+ ASSERT_TRUE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in read+write mode again.
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ // The old data should still be available.
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetReadOnly) {
+ // At this point, the underlying file must not exist.
+ ASSERT_FALSE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in read+write mode.
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ ASSERT_TRUE(ztable_segment_->isUsable());
+ ASSERT_TRUE(ztable_segment_->isWritable());
+
+ // Add the data.
+ addData(ztable_segment_->getMemorySegment());
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Close the segment.
+ ztable_segment_->clear();
+
+ // At this point, the underlying file must still exist.
+ ASSERT_TRUE(fileExists(mapped_file));
+
+ // Open the underlying mapped file in read-only mode again.
+ ztable_segment_->reset(ZoneTableSegment::READ_ONLY, config_params_);
+
+ // The old data should still be available.
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // But trying to allocate new data should result in an exception as
+ // the segment is read-only!
+ EXPECT_THROW(addData(ztable_segment_->getMemorySegment()),
+ MemorySegmentError);
+}
+
+TEST_F(ZoneTableSegmentMappedTest, clearUninitialized) {
+ // Clearing a segment that has not been reset() is a nop, as clear()
+ // returns it to a fresh uninitialized state anyway.
+ EXPECT_NO_THROW(ztable_segment_->clear());
+
+ // The following should still throw, because the segment has not
+ // been successfully reset() yet.
+ EXPECT_THROW(ztable_segment_->getHeader(), isc::InvalidOperation);
+ EXPECT_THROW(ztable_segment_->getMemorySegment(), isc::InvalidOperation);
+
+ // isWritable() must still return false, because the segment has not
+ // been successfully reset() yet.
+ EXPECT_FALSE(ztable_segment_->isUsable());
+ EXPECT_FALSE(ztable_segment_->isWritable());
+}
+
+TEST_F(ZoneTableSegmentMappedTest, clear) {
+ // First, open an underlying mapped file in read+write mode (doesn't
+ // exist yet)
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // The following method calls should no longer throw:
+ EXPECT_NO_THROW(ztable_segment_->getHeader());
+ EXPECT_NO_THROW(ztable_segment_->getMemorySegment());
+
+ // Now, clear the segment.
+ ztable_segment_->clear();
+
+ EXPECT_FALSE(ztable_segment_->isUsable());
+ EXPECT_FALSE(ztable_segment_->isWritable());
+ // The following method calls should now throw.
+ EXPECT_THROW(ztable_segment_->getHeader(), isc::InvalidOperation);
+ EXPECT_THROW(ztable_segment_->getMemorySegment(), isc::InvalidOperation);
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetFailedCorruptedChecksum) {
+ setupMappedFiles();
+
+ // Open mapped file 1 in read-write mode
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ // Corrupt mapped file 2.
+ scoped_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(mapped_file2,
+ MemorySegmentMapped::OPEN_OR_CREATE));
+ EXPECT_TRUE(verifyData(*segment));
+ corruptChecksum(*segment);
+ segment.reset();
+
+ // Resetting to mapped file 2 in read-write mode should fail
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params2_);
+ }, ResetFailed);
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // Check for the old data in the segment to make sure it is still
+ // available and correct.
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetFailedMissingChecksum) {
+ setupMappedFiles();
+
+ // Open mapped file 1 in read-write mode
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ // Corrupt mapped file 2.
+ scoped_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(mapped_file2,
+ MemorySegmentMapped::OPEN_OR_CREATE));
+ EXPECT_TRUE(verifyData(*segment));
+ deleteChecksum(*segment);
+ segment.reset();
+
+ // Resetting to mapped file 2 in read-only mode should fail
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::READ_ONLY, config_params2_);
+ }, ResetFailed);
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // Check for the old data in the segment to make sure it is still
+ // available and correct.
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetFailedMissingHeader) {
+ setupMappedFiles();
+
+ // Open mapped file 1 in read-write mode
+ ztable_segment_->reset(ZoneTableSegment::READ_WRITE, config_params_);
+
+ // Corrupt mapped file 2.
+ scoped_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(mapped_file2,
+ MemorySegmentMapped::OPEN_OR_CREATE));
+ EXPECT_TRUE(verifyData(*segment));
+ deleteHeader(*segment);
+ segment.reset();
+
+ // Resetting to mapped file 2 in read-only mode should fail
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::READ_ONLY, config_params2_);
+ }, ResetFailed);
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // Check for the old data in the segment to make sure it is still
+ // available and correct.
+ EXPECT_TRUE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+TEST_F(ZoneTableSegmentMappedTest, resetCreateOverCorruptedFile) {
+ setupMappedFiles();
+
+ // Corrupt mapped file 1.
+ scoped_ptr<MemorySegmentMapped> segment
+ (new MemorySegmentMapped(mapped_file,
+ MemorySegmentMapped::OPEN_OR_CREATE));
+ EXPECT_TRUE(verifyData(*segment));
+ corruptChecksum(*segment);
+ segment.reset();
+
+ // Resetting to mapped file 1 in CREATE mode over a corrupted file
+ // should pass.
+ EXPECT_NO_THROW(ztable_segment_->reset(ZoneTableSegment::CREATE,
+ config_params_));
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // Check for the old data in the segment. It should not be present
+ // (as we opened the segment in CREATE mode).
+ EXPECT_FALSE(verifyData(ztable_segment_->getMemorySegment()));
+
+ // Now try the same with missing checksum.
+ setupMappedFiles();
+
+ // Corrupt mapped file 1.
+ segment.reset(new MemorySegmentMapped(mapped_file,
+ MemorySegmentMapped::OPEN_OR_CREATE));
+ EXPECT_TRUE(verifyData(*segment));
+ deleteChecksum(*segment);
+ segment.reset();
+
+ // Resetting to mapped file 1 in CREATE mode over a file missing
+ // checksum should pass.
+ EXPECT_NO_THROW(ztable_segment_->reset(ZoneTableSegment::CREATE,
+ config_params_));
+
+ EXPECT_TRUE(ztable_segment_->isUsable());
+ EXPECT_TRUE(ztable_segment_->isWritable());
+ // Check for the old data in the segment. It should not be present
+ // (as we opened the segment in CREATE mode).
+ EXPECT_FALSE(verifyData(ztable_segment_->getMemorySegment()));
+}
+
+} // anonymous namespace
diff --git a/src/lib/datasrc/tests/memory/zone_table_segment_mock.h b/src/lib/datasrc/tests/memory/zone_table_segment_mock.h
new file mode 100644
index 0000000..f73ef49
--- /dev/null
+++ b/src/lib/datasrc/tests/memory/zone_table_segment_mock.h
@@ -0,0 +1,94 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H
+#define DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H 1
+
+#include <datasrc/memory/zone_table_segment.h>
+#include <datasrc/memory/zone_table.h>
+#include <datasrc/memory/zone_data.h>
+#include <datasrc/memory/zone_writer.h>
+
+#include <string>
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+namespace test {
+
+// A special ZoneTableSegment that can be used for tests. It can be
+// passed a MemorySegment that can be used later to test if all memory
+// was de-allocated on it.
+class ZoneTableSegmentMock : public ZoneTableSegment {
+public:
+ ZoneTableSegmentMock(const isc::dns::RRClass& rrclass,
+ isc::util::MemorySegment& mem_sgmt) :
+ ZoneTableSegment(rrclass),
+ impl_type_("mock"),
+ mem_sgmt_(mem_sgmt),
+ header_(ZoneTable::create(mem_sgmt_, rrclass))
+ {}
+
+ virtual ~ZoneTableSegmentMock() {
+ ZoneTable::destroy(mem_sgmt_, header_.getTable());
+ }
+
+ const std::string& getImplType() const {
+ return (impl_type_);
+ }
+
+ virtual void reset(MemorySegmentOpenMode, isc::data::ConstElementPtr) {
+ isc_throw(isc::NotImplemented, "reset() is not implemented");
+ }
+
+ virtual void clear() {
+ isc_throw(isc::NotImplemented, "clear() is not implemented");
+ }
+
+ virtual ZoneTableHeader& getHeader() {
+ return (header_);
+ }
+
+ virtual const ZoneTableHeader& getHeader() const {
+ return (header_);
+ }
+
+ virtual isc::util::MemorySegment& getMemorySegment() {
+ return (mem_sgmt_);
+ }
+
+ virtual bool isUsable() const {
+ return (true);
+ }
+
+ virtual bool isWritable() const {
+ return (true);
+ }
+
+private:
+ std::string impl_type_;
+ isc::util::MemorySegment& mem_sgmt_;
+ ZoneTableHeader header_;
+};
+
+} // namespace test
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
+
+#endif // DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/datasrc/tests/memory/zone_table_segment_test.h b/src/lib/datasrc/tests/memory/zone_table_segment_test.h
deleted file mode 100644
index 2078036..0000000
--- a/src/lib/datasrc/tests/memory/zone_table_segment_test.h
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H
-#define DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H 1
-
-#include <datasrc/memory/zone_table_segment.h>
-#include <datasrc/memory/zone_table.h>
-#include <datasrc/memory/zone_data.h>
-#include <datasrc/memory/zone_writer_local.h>
-
-namespace isc {
-namespace datasrc {
-namespace memory {
-namespace test {
-
-// A special ZoneTableSegment that can be used for tests. It can be
-// passed a MemorySegment that can be used later to test if all memory
-// was de-allocated on it.
-class ZoneTableSegmentTest : public ZoneTableSegment {
-public:
- ZoneTableSegmentTest(isc::dns::RRClass rrclass,
- isc::util::MemorySegment& mem_sgmt) :
- ZoneTableSegment(rrclass),
- mem_sgmt_(mem_sgmt),
- header_(ZoneTable::create(mem_sgmt_, rrclass))
- {}
-
- virtual ~ZoneTableSegmentTest() {
- ZoneTable::destroy(mem_sgmt_, header_.getTable());
- }
-
- virtual ZoneTableHeader& getHeader() {
- return (header_);
- }
-
- virtual const ZoneTableHeader& getHeader() const {
- return (header_);
- }
-
- virtual isc::util::MemorySegment& getMemorySegment() {
- return (mem_sgmt_);
- }
-
- virtual ZoneWriter* getZoneWriter(const LoadAction& load_action,
- const dns::Name& name,
- const dns::RRClass& rrclass)
- {
- return (new Writer(this, load_action, name, rrclass));
- }
-
-private:
- isc::util::MemorySegment& mem_sgmt_;
- ZoneTableHeader header_;
-
- // A writer for this segment. The implementation is similar
- // to ZoneWriterLocal, but all the error handling is stripped
- // for simplicity. Also, we do everything inside the
- // install(), for the same reason. We just need something
- // inside the tests, not a full-blown implementation
- // for background loading.
- class Writer : public ZoneWriter {
- public:
- Writer(ZoneTableSegmentTest* segment, const LoadAction& load_action,
- const dns::Name& name, const dns::RRClass& rrclass) :
- segment_(segment),
- load_action_(load_action),
- name_(name),
- rrclass_(rrclass)
- {}
-
- void load() {}
-
- void install() {
- ZoneTable* table(segment_->getHeader().getTable());
- const ZoneTable::AddResult
- result(table->addZone(segment_->getMemorySegment(), rrclass_,
- name_,
- load_action_(segment_->
- getMemorySegment())));
- if (result.zone_data != NULL) {
- ZoneData::destroy(segment_->getMemorySegment(),
- result.zone_data, rrclass_);
- }
- }
-
- virtual void cleanup() {}
- private:
- ZoneTableSegmentTest* segment_;
- LoadAction load_action_;
- dns::Name name_;
- dns::RRClass rrclass_;
- };
-};
-
-} // namespace test
-} // namespace memory
-} // namespace datasrc
-} // namespace isc
-
-#endif // DATASRC_MEMORY_ZONE_TABLE_SEGMENT_TEST_H
-
-// Local Variables:
-// mode: c++
-// End:
diff --git a/src/lib/datasrc/tests/memory/zone_table_segment_unittest.cc b/src/lib/datasrc/tests/memory/zone_table_segment_unittest.cc
index 51d5e0f..1027a26 100644
--- a/src/lib/datasrc/tests/memory/zone_table_segment_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_table_segment_unittest.cc
@@ -12,9 +12,8 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-#include <datasrc/memory/zone_writer_local.h>
+#include <datasrc/memory/zone_writer.h>
#include <datasrc/memory/zone_table_segment_local.h>
-#include <util/memory_segment_local.h>
#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
@@ -42,6 +41,9 @@ protected:
ZoneTableSegment* ztable_segment_;
};
+TEST_F(ZoneTableSegmentTest, getImplType) {
+ EXPECT_EQ("local", ztable_segment_->getImplType());
+}
TEST_F(ZoneTableSegmentTest, create) {
// By default, a local zone table segment is created.
@@ -52,6 +54,21 @@ TEST_F(ZoneTableSegmentTest, create) {
UnknownSegmentType);
}
+TEST_F(ZoneTableSegmentTest, reset) {
+ // reset() should throw that it's not implemented so that any
+ // accidental calls are found out.
+ EXPECT_THROW({
+ ztable_segment_->reset(ZoneTableSegment::CREATE,
+ Element::fromJSON("{}"));
+ }, isc::NotImplemented);
+}
+
+TEST_F(ZoneTableSegmentTest, clear) {
+ // clear() should throw that it's not implemented so that any
+ // accidental calls are found out.
+ EXPECT_THROW(ztable_segment_->clear(), isc::NotImplemented);
+}
+
// Helper function to check const and non-const methods.
template <typename TS, typename TH, typename TT>
void
@@ -79,22 +96,14 @@ TEST_F(ZoneTableSegmentTest, getMemorySegment) {
mem_sgmt.allMemoryDeallocated(); // use mem_sgmt
}
-ZoneData*
-loadAction(MemorySegment&) {
- // The function won't be called, so this is OK
- return (NULL);
+TEST_F(ZoneTableSegmentTest, isUsable) {
+ // Local segments are always usable.
+ EXPECT_TRUE(ztable_segment_->isUsable());
}
-// Test we can get a writer.
-TEST_F(ZoneTableSegmentTest, getZoneWriter) {
- scoped_ptr<ZoneWriter>
- writer(ztable_segment_->getZoneWriter(loadAction, Name("example.org"),
- RRClass::IN()));
- // We have to get something
- EXPECT_NE(static_cast<void*>(NULL), writer.get());
- // And for now, it should be the local writer
- EXPECT_NE(static_cast<void*>(NULL),
- dynamic_cast<ZoneWriterLocal*>(writer.get()));
+TEST_F(ZoneTableSegmentTest, isWritable) {
+ // Local segments are always writable.
+ EXPECT_TRUE(ztable_segment_->isWritable());
}
} // anonymous namespace
diff --git a/src/lib/datasrc/tests/memory/zone_table_unittest.cc b/src/lib/datasrc/tests/memory/zone_table_unittest.cc
index 0109793..7357322 100644
--- a/src/lib/datasrc/tests/memory/zone_table_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_table_unittest.cc
@@ -12,8 +12,6 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-#include "memory_segment_test.h"
-
#include <exceptions/exceptions.h>
#include <util/memory_segment_local.h>
@@ -26,6 +24,8 @@
#include <datasrc/memory/zone_table.h>
#include <datasrc/memory/segment_object_holder.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
+
#include <gtest/gtest.h>
#include <new> // for bad_alloc
@@ -56,7 +56,7 @@ protected:
}
const RRClass zclass_;
const Name zname1, zname2, zname3;
- test::MemorySegmentTest mem_sgmt_;
+ test::MemorySegmentMock mem_sgmt_;
ZoneTable* zone_table;
};
@@ -73,17 +73,24 @@ TEST_F(ZoneTableTest, addZone) {
// By default there's no zone contained.
EXPECT_EQ(0, zone_table->getZoneCount());
- // It doesn't accept empty (NULL) zones
- EXPECT_THROW(zone_table->addZone(mem_sgmt_, zclass_, zname1, NULL),
- isc::BadValue);
+ // It doesn't accept NULL as zone data
+ EXPECT_THROW(zone_table->addZone(mem_sgmt_, zname1, NULL),
+ isc::InvalidParameter);
EXPECT_EQ(0, zone_table->getZoneCount()); // count is still 0
+ // or an empty zone data
+ SegmentObjectHolder<ZoneData, RRClass> holder_empty(
+ mem_sgmt_, zclass_);
+ holder_empty.set(ZoneData::create(mem_sgmt_));
+ EXPECT_THROW(zone_table->addZone(mem_sgmt_, zname1, holder_empty.get()),
+ isc::InvalidParameter);
+
SegmentObjectHolder<ZoneData, RRClass> holder1(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname1), zclass_);
+ mem_sgmt_, zclass_);
+ holder1.set(ZoneData::create(mem_sgmt_, zname1));
const ZoneData* data1(holder1.get());
// Normal successful case.
- const ZoneTable::AddResult result1(zone_table->addZone(mem_sgmt_, zclass_,
- zname1,
+ const ZoneTable::AddResult result1(zone_table->addZone(mem_sgmt_, zname1,
holder1.release()));
EXPECT_EQ(result::SUCCESS, result1.code);
EXPECT_EQ(static_cast<const ZoneData*>(NULL), result1.zone_data);
@@ -91,11 +98,10 @@ TEST_F(ZoneTableTest, addZone) {
EXPECT_EQ(static_cast<const ZoneData*>(NULL), holder1.get());
EXPECT_EQ(1, zone_table->getZoneCount()); // count is now incremented
- // Duplicate add doesn't replace the existing data.
- SegmentObjectHolder<ZoneData, RRClass> holder2(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname1), zclass_);
- const ZoneTable::AddResult result2(zone_table->addZone(mem_sgmt_, zclass_,
- zname1,
+ // Duplicate add replaces the existing data wit the newly added one.
+ SegmentObjectHolder<ZoneData, RRClass> holder2(mem_sgmt_, zclass_);
+ holder2.set(ZoneData::create(mem_sgmt_, zname1));
+ const ZoneTable::AddResult result2(zone_table->addZone(mem_sgmt_, zname1,
holder2.release()));
EXPECT_EQ(result::EXIST, result2.code);
// The old one gets out
@@ -107,55 +113,95 @@ TEST_F(ZoneTableTest, addZone) {
EXPECT_EQ(1, zone_table->getZoneCount()); // count doesn't change.
SegmentObjectHolder<ZoneData, RRClass> holder3(
- mem_sgmt_, ZoneData::create(mem_sgmt_, Name("EXAMPLE.COM")),
- zclass_);
+ mem_sgmt_, zclass_);
+ holder3.set(ZoneData::create(mem_sgmt_, Name("EXAMPLE.COM")));
// names are compared in a case insensitive manner.
- const ZoneTable::AddResult result3(zone_table->addZone(mem_sgmt_, zclass_,
+ const ZoneTable::AddResult result3(zone_table->addZone(mem_sgmt_,
Name("EXAMPLE.COM"),
holder3.release()));
EXPECT_EQ(result::EXIST, result3.code);
ZoneData::destroy(mem_sgmt_, result3.zone_data, zclass_);
// Add some more different ones. Should just succeed.
SegmentObjectHolder<ZoneData, RRClass> holder4(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname2), zclass_);
+ mem_sgmt_, zclass_);
+ holder4.set(ZoneData::create(mem_sgmt_, zname2));
EXPECT_EQ(result::SUCCESS,
- zone_table->addZone(mem_sgmt_, zclass_, zname2,
- holder4.release()).code);
+ zone_table->addZone(mem_sgmt_, zname2, holder4.release()).code);
EXPECT_EQ(2, zone_table->getZoneCount());
SegmentObjectHolder<ZoneData, RRClass> holder5(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname3), zclass_);
+ mem_sgmt_, zclass_);
+ holder5.set(ZoneData::create(mem_sgmt_, zname3));
EXPECT_EQ(result::SUCCESS,
- zone_table->addZone(mem_sgmt_, zclass_, zname3,
- holder5.release()).code);
+ zone_table->addZone(mem_sgmt_, zname3, holder5.release()).code);
EXPECT_EQ(3, zone_table->getZoneCount());
// Have the memory segment throw an exception in extending the internal
- // tree. It still shouldn't cause memory leak (which would be detected
- // in TearDown()).
+ // tree. We'll destroy it after that via SegmentObjectHolder.
SegmentObjectHolder<ZoneData, RRClass> holder6(
- mem_sgmt_, ZoneData::create(mem_sgmt_, Name("example.org")), zclass_);
+ mem_sgmt_, zclass_);
+ holder6.set(ZoneData::create(mem_sgmt_, Name("example.org")));
mem_sgmt_.setThrowCount(1);
- EXPECT_THROW(zone_table->addZone(mem_sgmt_, zclass_, Name("example.org"),
- holder6.release()),
+ EXPECT_THROW(zone_table->addZone(mem_sgmt_, Name("example.org"),
+ holder6.get()),
std::bad_alloc);
}
+TEST_F(ZoneTableTest, addEmptyZone) {
+ // By default there's no zone contained.
+ EXPECT_EQ(0, zone_table->getZoneCount());
+
+ // Adding an empty zone. It should succeed.
+ const ZoneTable::AddResult result1 =
+ zone_table->addEmptyZone(mem_sgmt_, zname1);
+ EXPECT_EQ(result::SUCCESS, result1.code);
+ EXPECT_EQ(static_cast<const ZoneData*>(NULL), result1.zone_data);
+ EXPECT_EQ(1, zone_table->getZoneCount());
+
+ // The empty zone can be "found", with the ZONE_EMPTY flag on, and the
+ // returned ZoneData being NULL.
+ const ZoneTable::FindResult fresult1 = zone_table->findZone(zname1);
+ EXPECT_EQ(result::SUCCESS, fresult1.code);
+ EXPECT_EQ(result::ZONE_EMPTY, fresult1.flags);
+ EXPECT_EQ(static_cast<const ZoneData*>(NULL), fresult1.zone_data);
+
+ // Replacing an empty zone with non-empty one. Should be no problem, but
+ // the empty zone data are not returned in the result structure; it's
+ // internal to the ZoneTable implementation.
+ SegmentObjectHolder<ZoneData, RRClass> holder2(mem_sgmt_, zclass_);
+ holder2.set(ZoneData::create(mem_sgmt_, zname1));
+ const ZoneTable::AddResult result2(zone_table->addZone(mem_sgmt_, zname1,
+ holder2.release()));
+ EXPECT_EQ(result::EXIST, result2.code);
+ EXPECT_EQ(static_cast<const ZoneData*>(NULL), result2.zone_data);
+ EXPECT_EQ(1, zone_table->getZoneCount());
+
+ // Replacing a non-empty zone with an empty one is also okay. It's not
+ // different from replacing with another non-empty one.
+ const ZoneTable::AddResult result3 =
+ zone_table->addEmptyZone(mem_sgmt_, zname1);
+ EXPECT_EQ(result::EXIST, result3.code);
+ EXPECT_NE(static_cast<const ZoneData*>(NULL), result3.zone_data);
+ ZoneData::destroy(mem_sgmt_, result3.zone_data, zclass_);
+ EXPECT_EQ(1, zone_table->getZoneCount());
+}
+
TEST_F(ZoneTableTest, findZone) {
SegmentObjectHolder<ZoneData, RRClass> holder1(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname1), zclass_);
+ mem_sgmt_, zclass_);
+ holder1.set(ZoneData::create(mem_sgmt_, zname1));
ZoneData* zone_data = holder1.get();
- EXPECT_EQ(result::SUCCESS, zone_table->addZone(mem_sgmt_, zclass_, zname1,
+ EXPECT_EQ(result::SUCCESS, zone_table->addZone(mem_sgmt_, zname1,
holder1.release()).code);
SegmentObjectHolder<ZoneData, RRClass> holder2(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname2), zclass_);
+ mem_sgmt_, zclass_);
+ holder2.set(ZoneData::create(mem_sgmt_, zname2));
EXPECT_EQ(result::SUCCESS,
- zone_table->addZone(mem_sgmt_, zclass_, zname2,
- holder2.release()).code);
+ zone_table->addZone(mem_sgmt_, zname2, holder2.release()).code);
SegmentObjectHolder<ZoneData, RRClass> holder3(
- mem_sgmt_, ZoneData::create(mem_sgmt_, zname3), zclass_);
+ mem_sgmt_, zclass_);
+ holder3.set(ZoneData::create(mem_sgmt_, zname3));
EXPECT_EQ(result::SUCCESS,
- zone_table->addZone(mem_sgmt_, zclass_, zname3,
- holder3.release()).code);
+ zone_table->addZone(mem_sgmt_, zname3, holder3.release()).code);
const ZoneTable::FindResult find_result1 =
zone_table->findZone(Name("example.com"));
@@ -177,9 +223,9 @@ TEST_F(ZoneTableTest, findZone) {
// make sure the partial match is indeed the longest match by adding
// a zone with a shorter origin and query again.
SegmentObjectHolder<ZoneData, RRClass> holder4(
- mem_sgmt_, ZoneData::create(mem_sgmt_, Name("com")), zclass_);
- EXPECT_EQ(result::SUCCESS, zone_table->addZone(mem_sgmt_, zclass_,
- Name("com"),
+ mem_sgmt_, zclass_);
+ holder4.set(ZoneData::create(mem_sgmt_, Name("com")));
+ EXPECT_EQ(result::SUCCESS, zone_table->addZone(mem_sgmt_, Name("com"),
holder4.release()).code);
EXPECT_EQ(zone_data,
zone_table->findZone(Name("www.example.com")).zone_data);
diff --git a/src/lib/datasrc/tests/memory/zone_writer_unittest.cc b/src/lib/datasrc/tests/memory/zone_writer_unittest.cc
index 5d2cd0a..e1fe672 100644
--- a/src/lib/datasrc/tests/memory/zone_writer_unittest.cc
+++ b/src/lib/datasrc/tests/memory/zone_writer_unittest.cc
@@ -12,54 +12,75 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-#include <datasrc/memory/zone_writer_local.h>
+#include <config.h>
+
+#include <datasrc/memory/zone_writer.h>
#include <datasrc/memory/zone_table_segment_local.h>
#include <datasrc/memory/zone_data.h>
+#include <datasrc/memory/zone_data_loader.h>
+#include <datasrc/memory/load_action.h>
+#include <datasrc/memory/zone_table.h>
+#include <datasrc/exceptions.h>
+#include <datasrc/result.h>
+
+#include <util/memory_segment_mapped.h>
+
+#include <cc/data.h>
#include <dns/rrclass.h>
#include <dns/name.h>
+#include <datasrc/tests/memory/memory_segment_mock.h>
+#include <datasrc/tests/memory/zone_table_segment_mock.h>
+
#include <gtest/gtest.h>
#include <boost/scoped_ptr.hpp>
#include <boost/bind.hpp>
+#include <boost/format.hpp>
+
+#include <string>
+#include <unistd.h>
using boost::scoped_ptr;
using boost::bind;
using isc::dns::RRClass;
using isc::dns::Name;
+using isc::datasrc::ZoneLoaderException;
using namespace isc::datasrc::memory;
+using namespace isc::datasrc::memory::test;
namespace {
class TestException {};
-class ZoneWriterLocalTest : public ::testing::Test {
-public:
- ZoneWriterLocalTest() :
- segment_(ZoneTableSegment::create(RRClass::IN(), "local")),
+class ZoneWriterTest : public ::testing::Test {
+protected:
+ ZoneWriterTest() :
+ segment_(new ZoneTableSegmentMock(RRClass::IN(), mem_sgmt_)),
writer_(new
- ZoneWriterLocal(dynamic_cast<ZoneTableSegmentLocal*>(segment_.
- get()),
- bind(&ZoneWriterLocalTest::loadAction, this, _1),
- Name("example.org"), RRClass::IN())),
+ ZoneWriter(*segment_,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), false)),
load_called_(false),
load_throw_(false),
+ load_loader_throw_(false),
load_null_(false),
load_data_(false)
{}
- void TearDown() {
+ virtual void TearDown() {
// Release the writer
writer_.reset();
}
-protected:
- scoped_ptr<ZoneTableSegment> segment_;
- scoped_ptr<ZoneWriterLocal> writer_;
+ MemorySegmentMock mem_sgmt_;
+ scoped_ptr<ZoneTableSegmentMock> segment_;
+ scoped_ptr<ZoneWriter> writer_;
bool load_called_;
bool load_throw_;
+ bool load_loader_throw_;
bool load_null_;
bool load_data_;
-private:
+public:
ZoneData* loadAction(isc::util::MemorySegment& segment) {
// Make sure it is the correct segment passed. We know the
// exact instance, can compare pointers to them.
@@ -69,6 +90,9 @@ private:
if (load_throw_) {
throw TestException();
}
+ if (load_loader_throw_) {
+ isc_throw(ZoneLoaderException, "faked loader exception");
+ }
if (load_null_) {
// Be nasty to the caller and return NULL, which is forbidden
@@ -86,9 +110,39 @@ private:
}
};
+class ReadOnlySegment : public ZoneTableSegmentMock {
+public:
+ ReadOnlySegment(const isc::dns::RRClass& rrclass,
+ isc::util::MemorySegment& mem_sgmt) :
+ ZoneTableSegmentMock(rrclass, mem_sgmt)
+ {}
+
+ // Returns false indicating that the segment is not usable. We
+ // override this too as ZoneTableSegment implementations may use it
+ // internally.
+ virtual bool isUsable() const {
+ return (false);
+ }
+
+ // Returns false indicating it is a read-only segment. It is used in
+ // the ZoneWriter tests.
+ virtual bool isWritable() const {
+ return (false);
+ }
+};
+
+TEST_F(ZoneWriterTest, constructForReadOnlySegment) {
+ MemorySegmentMock mem_sgmt;
+ ReadOnlySegment ztable_segment(RRClass::IN(), mem_sgmt);
+ EXPECT_THROW(ZoneWriter(ztable_segment,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), false),
+ isc::InvalidOperation);
+}
+
// We call it the way we are supposed to, check every callback is called in the
// right moment.
-TEST_F(ZoneWriterLocalTest, correctCall) {
+TEST_F(ZoneWriterTest, correctCall) {
// Nothing called before we call it
EXPECT_FALSE(load_called_);
@@ -105,7 +159,7 @@ TEST_F(ZoneWriterLocalTest, correctCall) {
EXPECT_NO_THROW(writer_->cleanup());
}
-TEST_F(ZoneWriterLocalTest, loadTwice) {
+TEST_F(ZoneWriterTest, loadTwice) {
// Load it the first time
EXPECT_NO_THROW(writer_->load());
EXPECT_TRUE(load_called_);
@@ -126,7 +180,7 @@ TEST_F(ZoneWriterLocalTest, loadTwice) {
// Try loading after call to install and call to cleanup. Both is
// forbidden.
-TEST_F(ZoneWriterLocalTest, loadLater) {
+TEST_F(ZoneWriterTest, loadLater) {
// Load first, so we can install
EXPECT_NO_THROW(writer_->load());
EXPECT_NO_THROW(writer_->install());
@@ -144,7 +198,7 @@ TEST_F(ZoneWriterLocalTest, loadLater) {
}
// Try calling install at various bad times
-TEST_F(ZoneWriterLocalTest, invalidInstall) {
+TEST_F(ZoneWriterTest, invalidInstall) {
// Nothing loaded yet
EXPECT_THROW(writer_->install(), isc::InvalidOperation);
EXPECT_FALSE(load_called_);
@@ -161,7 +215,7 @@ TEST_F(ZoneWriterLocalTest, invalidInstall) {
// We check we can clean without installing first and nothing bad
// happens. We also misuse the testcase to check we can't install
// after cleanup.
-TEST_F(ZoneWriterLocalTest, cleanWithoutInstall) {
+TEST_F(ZoneWriterTest, cleanWithoutInstall) {
EXPECT_NO_THROW(writer_->load());
EXPECT_NO_THROW(writer_->cleanup());
@@ -172,7 +226,7 @@ TEST_F(ZoneWriterLocalTest, cleanWithoutInstall) {
}
// Test the case when load callback throws
-TEST_F(ZoneWriterLocalTest, loadThrows) {
+TEST_F(ZoneWriterTest, loadThrows) {
load_throw_ = true;
EXPECT_THROW(writer_->load(), TestException);
@@ -184,9 +238,60 @@ TEST_F(ZoneWriterLocalTest, loadThrows) {
EXPECT_NO_THROW(writer_->cleanup());
}
+// Emulate the situation where load() throws loader error.
+TEST_F(ZoneWriterTest, loadLoaderException) {
+ std::string error_msg;
+
+ // By default, the exception is propagated.
+ load_loader_throw_ = true;
+ EXPECT_THROW(writer_->load(), ZoneLoaderException);
+ // In this case, passed error_msg won't be updated.
+ writer_.reset(new ZoneWriter(*segment_,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), false));
+ EXPECT_THROW(writer_->load(&error_msg), ZoneLoaderException);
+ EXPECT_EQ("", error_msg);
+
+ // If we specify allowing load error, load() will succeed and install()
+ // adds an empty zone. Note that we implicitly pass NULL to load()
+ // as it's the default parameter, so the following also confirms it doesn't
+ // cause disruption.
+ writer_.reset(new ZoneWriter(*segment_,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), true));
+ writer_->load();
+ writer_->install();
+ writer_->cleanup();
+
+ // Check an empty zone has been really installed.
+ using namespace isc::datasrc::result;
+ const ZoneTable* ztable = segment_->getHeader().getTable();
+ ASSERT_TRUE(ztable);
+ const ZoneTable::FindResult result = ztable->findZone(Name("example.org"));
+ EXPECT_EQ(SUCCESS, result.code);
+ EXPECT_EQ(ZONE_EMPTY, result.flags);
+
+ // Allowing an error, and passing a template for the error message.
+ // It will be filled with the reason for the error.
+ writer_.reset(new ZoneWriter(*segment_,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), true));
+ writer_->load(&error_msg);
+ EXPECT_NE("", error_msg);
+
+ // In case of no error, the placeholder will be intact.
+ load_loader_throw_ = false;
+ error_msg.clear();
+ writer_.reset(new ZoneWriter(*segment_,
+ bind(&ZoneWriterTest::loadAction, this, _1),
+ Name("example.org"), RRClass::IN(), true));
+ writer_->load(&error_msg);
+ EXPECT_EQ("", error_msg);
+}
+
// Check the strong exception guarantee - if it throws, nothing happened
// to the content.
-TEST_F(ZoneWriterLocalTest, retry) {
+TEST_F(ZoneWriterTest, retry) {
// First attempt fails due to some exception.
load_throw_ = true;
EXPECT_THROW(writer_->load(), TestException);
@@ -214,7 +319,7 @@ TEST_F(ZoneWriterLocalTest, retry) {
}
// Check the writer defends itsefl when load action returns NULL
-TEST_F(ZoneWriterLocalTest, loadNull) {
+TEST_F(ZoneWriterTest, loadNull) {
load_null_ = true;
EXPECT_THROW(writer_->load(), isc::InvalidOperation);
@@ -226,10 +331,85 @@ TEST_F(ZoneWriterLocalTest, loadNull) {
}
// Check the object cleans up in case we forget it.
-TEST_F(ZoneWriterLocalTest, autoCleanUp) {
+TEST_F(ZoneWriterTest, autoCleanUp) {
// Load data and forget about it. It should get released
// when the writer itself is destroyed.
EXPECT_NO_THROW(writer_->load());
}
+// Used in the manyWrites test, encapsulating loadZoneData() to avoid
+// its signature ambiguity.
+ZoneData*
+loadZoneDataWrapper(isc::util::MemorySegment& segment, const RRClass& rrclass,
+ const Name& name, const std::string& filename)
+{
+ return (loadZoneData(segment, rrclass, name, filename));
+}
+
+// Check the behavior of creating many small zones. The main purpose of
+// test is to trigger MemorySegmentGrown exception in ZoneWriter::install.
+// There's no easy (if any) way to cause that reliably as it's highly
+// dependent on details of the underlying boost implementation and probably
+// also on the system behavior, but we'll try some promising scenario (it
+// in fact triggered the intended result at least on one environment).
+TEST_F(ZoneWriterTest, manyWrites) {
+#ifdef USE_SHARED_MEMORY
+ // First, make a fresh mapped file of a small size (so it'll be more likely
+ // to grow in the test.
+ const char* const mapped_file = TEST_DATA_BUILDDIR "/test.mapped";
+ unlink(mapped_file);
+ boost::scoped_ptr<isc::util::MemorySegmentMapped> segment(
+ new isc::util::MemorySegmentMapped(
+ mapped_file, isc::util::MemorySegmentMapped::CREATE_ONLY, 4096));
+ segment.reset();
+
+ // Then prepare a ZoneTableSegment of the 'mapped' type specifying the
+ // file we just created.
+ boost::scoped_ptr<ZoneTableSegment> zt_segment(
+ ZoneTableSegment::create(RRClass::IN(), "mapped"));
+ const isc::data::ConstElementPtr params =
+ isc::data::Element::fromJSON(
+ "{\"mapped-file\": \"" + std::string(mapped_file) + "\"}");
+ zt_segment->reset(ZoneTableSegment::READ_WRITE, params);
+#else
+ // Do the same test for the local segment, although there shouldn't be
+ // anything tricky in that case.
+ boost::scoped_ptr<ZoneTableSegment> zt_segment(
+ ZoneTableSegment::create(RRClass::IN(), "local"));
+#endif
+
+ // Now, create many small zones in the zone table with a ZoneWriter.
+ // We use larger origin names so it'll (hopefully) require the memory
+ // segment to grow while adding the name into the internal table.
+ const size_t zone_count = 10000; // arbitrary choice
+ for (size_t i = 0; i < zone_count; ++i) {
+ const Name origin(
+ boost::str(boost::format("%063u.%063u.%063u.example.org")
+ % i % i % i));
+ const LoadAction action = boost::bind(loadZoneDataWrapper, _1,
+ RRClass::IN(), origin,
+ TEST_DATA_DIR
+ "/template.zone");
+ ZoneWriter writer(*zt_segment, action, origin, RRClass::IN(), false);
+ writer.load();
+ writer.install();
+ writer.cleanup();
+
+ // Confirm it's been successfully added and can be actually found.
+ const ZoneTable::FindResult result =
+ zt_segment->getHeader().getTable()->findZone(origin);
+ EXPECT_EQ(isc::datasrc::result::SUCCESS, result.code);
+ EXPECT_NE(static_cast<const ZoneData*>(NULL), result.zone_data) <<
+ "unexpected find result: " + origin.toText();
+ }
+
+ // Make sure to close the segment before (possibly) removing the mapped
+ // file.
+ zt_segment.reset();
+
+#ifdef USE_SHARED_MEMORY
+ unlink(mapped_file);
+#endif
+}
+
}
diff --git a/src/lib/datasrc/tests/mock_client.cc b/src/lib/datasrc/tests/mock_client.cc
index fd3916d..8cdc05d 100644
--- a/src/lib/datasrc/tests/mock_client.cc
+++ b/src/lib/datasrc/tests/mock_client.cc
@@ -187,7 +187,7 @@ MockDataSourceClient::getIterator(const Name& name, bool) const {
if (result.code == isc::datasrc::result::SUCCESS) {
return (ZoneIteratorPtr(new Iterator(name, have_a_)));
} else {
- isc_throw(DataSourceError, "No such zone");
+ isc_throw(NoSuchZone, "No such zone");
}
}
}
diff --git a/src/lib/datasrc/tests/mock_client.h b/src/lib/datasrc/tests/mock_client.h
index c51e9a1..7a01440 100644
--- a/src/lib/datasrc/tests/mock_client.h
+++ b/src/lib/datasrc/tests/mock_client.h
@@ -58,6 +58,13 @@ public:
void eraseZone(const dns::Name& zone_name) {
zones.erase(zone_name);
}
+
+ /// \brief Dynamically add a zone to the data source.
+ ///
+ /// \return true if the zone is newly added; false if it already exists.
+ bool insertZone(const dns::Name& zone_name) {
+ return (zones.insert(zone_name).second);
+ }
const std::string type_;
const data::ConstElementPtr configuration_;
diff --git a/src/lib/datasrc/tests/test_client.cc b/src/lib/datasrc/tests/test_client.cc
index c7854ed..f521a6d 100644
--- a/src/lib/datasrc/tests/test_client.cc
+++ b/src/lib/datasrc/tests/test_client.cc
@@ -32,7 +32,6 @@
#include <fstream>
using namespace std;
-using boost::shared_ptr;
using namespace isc::dns;
@@ -48,7 +47,7 @@ addRRset(ZoneUpdaterPtr updater, ConstRRsetPtr rrset) {
}
}
-shared_ptr<DataSourceClient>
+boost::shared_ptr<DataSourceClient>
createSQLite3Client(RRClass zclass, const Name& zname,
const char* const db_file, const char* const zone_file)
{
@@ -60,7 +59,7 @@ createSQLite3Client(RRClass zclass, const Name& zname,
return (createSQLite3Client(zclass, zname, db_file, ifs));
}
-shared_ptr<DataSourceClient>
+boost::shared_ptr<DataSourceClient>
createSQLite3Client(RRClass zclass, const Name& zname,
const char* const db_file, istream& rr_stream)
{
@@ -75,9 +74,10 @@ createSQLite3Client(RRClass zclass, const Name& zname,
"Error setting up; command failed: " << install_cmd);
}
- shared_ptr<SQLite3Accessor> accessor(
+ boost::shared_ptr<SQLite3Accessor> accessor(
new SQLite3Accessor(db_file, zclass.toText()));
- shared_ptr<DatabaseClient> client(new DatabaseClient(zclass, accessor));
+ boost::shared_ptr<DatabaseClient> client(new DatabaseClient(zclass,
+ accessor));
ZoneUpdaterPtr updater = client->getUpdater(zname, true);
masterLoad(rr_stream, zname, zclass, boost::bind(addRRset, updater, _1));
diff --git a/src/lib/datasrc/tests/zone_finder_context_unittest.cc b/src/lib/datasrc/tests/zone_finder_context_unittest.cc
index f541fd8..67118e9 100644
--- a/src/lib/datasrc/tests/zone_finder_context_unittest.cc
+++ b/src/lib/datasrc/tests/zone_finder_context_unittest.cc
@@ -36,15 +36,12 @@
#include <boost/bind.hpp>
#include <boost/foreach.hpp>
#include <boost/shared_ptr.hpp>
-#include <boost/scoped_ptr.hpp>
#include <fstream>
#include <sstream>
#include <vector>
using namespace std;
-using boost::shared_ptr;
-using boost::scoped_ptr;
using namespace isc::data;
using namespace isc::util;
@@ -61,7 +58,7 @@ namespace {
const char* const TEST_ZONE_FILE = TEST_DATA_DIR "/contexttest.zone";
// Convenient shortcut
-typedef shared_ptr<DataSourceClient> DataSourceClientPtr;
+typedef boost::shared_ptr<DataSourceClient> DataSourceClientPtr;
// This is the type used as the test parameter. Note that this is
// intentionally a plain old type (i.e. a function pointer), not a class;
@@ -77,16 +74,16 @@ createInMemoryClient(RRClass zclass, const Name& zname) {
" \"params\":"
" {\"" + zname.toText() + "\": \"" +
string(TEST_ZONE_FILE) + "\"}}"), true);
- shared_ptr<ZoneTableSegment> ztable_segment(
+ boost::shared_ptr<ZoneTableSegment> ztable_segment(
ZoneTableSegment::create(zclass, cache_conf.getSegmentType()));
- scoped_ptr<memory::ZoneWriter> writer(
- ztable_segment->getZoneWriter(cache_conf.getLoadAction(zclass, zname),
- zname, zclass));
- writer->load();
- writer->install();
- writer->cleanup();
- shared_ptr<InMemoryClient> client(new InMemoryClient(ztable_segment,
- zclass));
+ memory::ZoneWriter writer(*ztable_segment,
+ cache_conf.getLoadAction(zclass, zname),
+ zname, zclass, false);
+ writer.load();
+ writer.install();
+ writer.cleanup();
+ boost::shared_ptr<InMemoryClient> client(new InMemoryClient(ztable_segment,
+ zclass));
return (client);
}
diff --git a/src/lib/datasrc/tests/zone_loader_unittest.cc b/src/lib/datasrc/tests/zone_loader_unittest.cc
index 4cf9e9a..cd1999a 100644
--- a/src/lib/datasrc/tests/zone_loader_unittest.cc
+++ b/src/lib/datasrc/tests/zone_loader_unittest.cc
@@ -319,9 +319,10 @@ protected:
rrclass_, cache_conf.getSegmentType()));
if (filename) {
boost::scoped_ptr<memory::ZoneWriter> writer(
- ztable_segment_->getZoneWriter(cache_conf.getLoadAction(
- rrclass_, zone),
- zone, rrclass_));
+ new memory::ZoneWriter(*ztable_segment_,
+ cache_conf.getLoadAction(rrclass_,
+ zone),
+ zone, rrclass_, false));
writer->load();
writer->install();
writer->cleanup();
diff --git a/src/lib/datasrc/zone_table_accessor.h b/src/lib/datasrc/zone_table_accessor.h
index 378c7eb..8e92d51 100644
--- a/src/lib/datasrc/zone_table_accessor.h
+++ b/src/lib/datasrc/zone_table_accessor.h
@@ -202,6 +202,9 @@ public:
virtual IteratorPtr getIterator() const = 0;
};
+typedef boost::shared_ptr<ZoneTableAccessor> ZoneTableAccessorPtr;
+typedef boost::shared_ptr<const ZoneTableAccessor> ConstZoneTableAccessorPtr;
+
}
}
diff --git a/src/lib/dhcp/Makefile.am b/src/lib/dhcp/Makefile.am
index 1e292bd..1748927 100644
--- a/src/lib/dhcp/Makefile.am
+++ b/src/lib/dhcp/Makefile.am
@@ -23,9 +23,13 @@ libb10_dhcp___la_SOURCES += iface_mgr_linux.cc
libb10_dhcp___la_SOURCES += iface_mgr_sun.cc
libb10_dhcp___la_SOURCES += libdhcp++.cc libdhcp++.h
libb10_dhcp___la_SOURCES += option4_addrlst.cc option4_addrlst.h
+libb10_dhcp___la_SOURCES += option4_client_fqdn.cc option4_client_fqdn.h
libb10_dhcp___la_SOURCES += option6_ia.cc option6_ia.h
libb10_dhcp___la_SOURCES += option6_iaaddr.cc option6_iaaddr.h
+libb10_dhcp___la_SOURCES += option6_iaprefix.cc option6_iaprefix.h
libb10_dhcp___la_SOURCES += option6_addrlst.cc option6_addrlst.h
+libb10_dhcp___la_SOURCES += option6_client_fqdn.cc option6_client_fqdn.h
+libb10_dhcp___la_SOURCES += option_vendor.cc option_vendor.h
libb10_dhcp___la_SOURCES += option_int.h
libb10_dhcp___la_SOURCES += option_int_array.h
libb10_dhcp___la_SOURCES += option.cc option.h
@@ -33,12 +37,21 @@ libb10_dhcp___la_SOURCES += option_custom.cc option_custom.h
libb10_dhcp___la_SOURCES += option_data_types.cc option_data_types.h
libb10_dhcp___la_SOURCES += option_definition.cc option_definition.h
libb10_dhcp___la_SOURCES += option_space.cc option_space.h
+libb10_dhcp___la_SOURCES += option_string.cc option_string.h
+libb10_dhcp___la_SOURCES += protocol_util.cc protocol_util.h
libb10_dhcp___la_SOURCES += pkt6.cc pkt6.h
libb10_dhcp___la_SOURCES += pkt4.cc pkt4.h
-libb10_dhcp___la_SOURCES += pkt_filter.h
+libb10_dhcp___la_SOURCES += pkt_filter.h pkt_filter.cc
+libb10_dhcp___la_SOURCES += pkt_filter6.h pkt_filter6.cc
libb10_dhcp___la_SOURCES += pkt_filter_inet.cc pkt_filter_inet.h
+libb10_dhcp___la_SOURCES += pkt_filter_inet6.cc pkt_filter_inet6.h
+
+if OS_LINUX
libb10_dhcp___la_SOURCES += pkt_filter_lpf.cc pkt_filter_lpf.h
+endif
+
libb10_dhcp___la_SOURCES += std_option_defs.h
+libb10_dhcp___la_SOURCES += docsis3_option_defs.h
libb10_dhcp___la_CXXFLAGS = $(AM_CXXFLAGS)
libb10_dhcp___la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
@@ -49,6 +62,36 @@ libb10_dhcp___la_LDFLAGS = -no-undefined -version-info 2:0:0
EXTRA_DIST = README libdhcp++.dox
+# Specify the headers for copying into the installation directory tree. User-
+# written libraries may need access to all libdhcp++ headers.
+libb10_dhcp___includedir = $(pkgincludedir)/dhcp
+libb10_dhcp___include_HEADERS = \
+ dhcp4.h \
+ dhcp6.h \
+ duid.h \
+ hwaddr.h \
+ iface_mgr.h \
+ libdhcp++.h \
+ option.h \
+ option4_addrlst.h \
+ option6_addrlst.h \
+ option6_ia.h \
+ option6_iaaddr.h \
+ option_custom.h \
+ option_data_types.h \
+ option_definition.h \
+ option_int.h \
+ option_int_array.h \
+ option_space.h \
+ option_string.h \
+ pkt4.h \
+ pkt6.h \
+ pkt_filter.h \
+ pkt_filter_inet.h \
+ pkt_filter_lpf.h \
+ protocol_util.h \
+ std_option_defs.h
+
if USE_CLANGPP
# Disable unused parameter warning caused by some of the
# Boost headers when compiling with clang.
diff --git a/src/lib/dhcp/dhcp4.h b/src/lib/dhcp/dhcp4.h
index 002e74f..392478a 100644
--- a/src/lib/dhcp/dhcp4.h
+++ b/src/lib/dhcp/dhcp4.h
@@ -125,6 +125,8 @@ enum DHCPOptionType {
DHO_DHCP_CLIENT_IDENTIFIER = 61,
DHO_NWIP_DOMAIN_NAME = 62,
DHO_NWIP_SUBOPTIONS = 63,
+ DHO_TFTP_SERVER_NAME = 66,
+ DHO_BOOT_FILE_NAME = 67,
DHO_USER_CLASS = 77,
DHO_FQDN = 81,
DHO_DHCP_AGENT_OPTIONS = 82,
@@ -162,16 +164,27 @@ static const uint16_t DHCP4_SERVER_PORT = 67;
/// extensions field).
static const uint32_t DHCP_OPTIONS_COOKIE = 0x63825363;
+/* Relay Agent Information option subtypes: */
+
+static const uint16_t RAI_OPTION_AGENT_CIRCUIT_ID = 1; // RFC3046
+static const uint16_t RAI_OPTION_REMOTE_ID = 2; // RFC3046
+/* option 3 is reserved and will never be assigned */
+static const uint16_t RAI_OPTION_DOCSIS_DEVICE_CLASS = 4; // RFC3256
+static const uint16_t RAI_OPTION_LINK_SELECTION = 5; // RFC3527
+static const uint16_t RAI_OPTION_SUBSCRIBER_ID = 6; //RFC3993
+static const uint16_t RAI_OPTION_RADIUS = 7; //RFC4014
+static const uint16_t RAI_OPTION_AUTH = 8; //RFC4030
+static const uint16_t RAI_OPTION_VSI = 9; // RFC4243
+static const uint16_t RAI_OPTION_RELAY_FLAGS = 10; // RFC5010
+static const uint16_t RAI_OPTION_SERVER_ID_OVERRIDE = 11; // RFC5107
+static const uint16_t RAI_OPTION_RELAY_ID = 12; //RFC6925
+static const uint16_t RAI_OPTION_VIRTUAL_SUBNET_SELECT = 151; //RFC6607
+static const uint16_t RAI_OPTION_VIRTUAL_SUBNET_SELECT_CTRL = 152; //RFC6607
+
// TODO: Following are leftovers from dhcp.h import from ISC DHCP
// They will be converted to C++-style defines once they will start
// to be used.
#if 0
-/* Relay Agent Information option subtypes: */
-#define RAI_CIRCUIT_ID 1
-#define RAI_REMOTE_ID 2
-#define RAI_AGENT_ID 3
-#define RAI_LINK_SELECT 5
-
/* FQDN suboptions: */
#define FQDN_NO_CLIENT_UPDATE 1
#define FQDN_SERVER_UPDATE 2
diff --git a/src/lib/dhcp/docsis3_option_defs.h b/src/lib/dhcp/docsis3_option_defs.h
new file mode 100644
index 0000000..6031f8d
--- /dev/null
+++ b/src/lib/dhcp/docsis3_option_defs.h
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DOCSIS3_OPTION_DEFS_H
+#define DOCSIS3_OPTION_DEFS_H
+
+#include <dhcp/std_option_defs.h>
+#include <dhcp/option_data_types.h>
+
+namespace isc {
+namespace dhcp {
+
+#define VENDOR_ID_CABLE_LABS 4491
+
+#define DOCSIS3_V4_ORO 1
+#define DOCSIS3_V4_TFTP_SERVERS 2
+
+/// @brief Definitions of standard DHCPv4 options.
+const OptionDefParams DOCSIS3_V4_DEFS[] = {
+ { "oro", DOCSIS3_V4_ORO, OPT_UINT8_TYPE, true, NO_RECORD_DEF, "" },
+ { "tftp-servers", DOCSIS3_V4_TFTP_SERVERS, OPT_IPV4_ADDRESS_TYPE, true, NO_RECORD_DEF, "" }
+};
+
+/// Number of option definitions defined.
+const int DOCSIS3_V4_DEFS_SIZE = sizeof(DOCSIS3_V4_DEFS) / sizeof(OptionDefParams);
+
+/// @todo define remaining docsis3 v6 codes
+#define DOCSIS3_V6_ORO 1
+#define DOCSIS3_V6_DEVICE_TYPE 2
+#define DOCSIS3_V6_VENDOR_NAME 10
+#define DOCSIS3_V6_TFTP_SERVERS 32
+#define DOCSIS3_V6_CONFIG_FILE 33
+#define DOCSIS3_V6_SYSLOG_SERVERS 34
+#define DOCSIS3_V6_TIME_SERVERS 37
+#define DOCSIS3_V6_TIME_OFFSET 38
+
+/// @brief Definitions of standard DHCPv6 options.
+const OptionDefParams DOCSIS3_V6_DEFS[] = {
+ { "oro", DOCSIS3_V6_ORO, OPT_UINT16_TYPE, true, NO_RECORD_DEF, "" },
+ { "device-type", DOCSIS3_V6_DEVICE_TYPE, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
+ { "vendor-type", DOCSIS3_V6_VENDOR_NAME, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
+ { "tftp-servers", DOCSIS3_V6_TFTP_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" },
+ { "time-servers", DOCSIS3_V6_TIME_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" },
+ { "config-file", DOCSIS3_V6_CONFIG_FILE, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
+ { "syslog-servers", DOCSIS3_V6_SYSLOG_SERVERS, OPT_IPV6_ADDRESS_TYPE, true, NO_RECORD_DEF, "" },
+ { "time-offset", DOCSIS3_V6_TIME_OFFSET, OPT_INT32_TYPE, false, NO_RECORD_DEF, "" }
+ // @todo add definitions for all remaning options.
+};
+
+/// Number of option definitions defined.
+const int DOCSIS3_V6_DEFS_SIZE = sizeof(DOCSIS3_V6_DEFS) / sizeof(OptionDefParams);
+
+/// The class as specified in vendor-class option by the devices
+extern const char* DOCSIS3_CLASS_EROUTER;
+extern const char* DOCSIS3_CLASS_MODEM;
+
+}; // isc::dhcp namespace
+}; // isc namespace
+
+#endif // DOCSIS3_OPTION_DEFS_H
diff --git a/src/lib/dhcp/duid.cc b/src/lib/dhcp/duid.cc
index 8570d26..80fa724 100644
--- a/src/lib/dhcp/duid.cc
+++ b/src/lib/dhcp/duid.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -46,7 +46,7 @@ DUID::DUID(const uint8_t* data, size_t len) {
duid_ = std::vector<uint8_t>(data, data + len);
}
-std::vector<uint8_t> DUID::getDuid() const {
+const std::vector<uint8_t>& DUID::getDuid() const {
return (duid_);
}
@@ -104,7 +104,7 @@ ClientId::ClientId(const uint8_t *clientid, size_t len)
}
// Returns a copy of client-id data
-std::vector<uint8_t> ClientId::getClientId() const {
+const std::vector<uint8_t>& ClientId::getClientId() const {
return (duid_);
}
diff --git a/src/lib/dhcp/duid.h b/src/lib/dhcp/duid.h
index d20a58d..1cd8388 100644
--- a/src/lib/dhcp/duid.h
+++ b/src/lib/dhcp/duid.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -58,12 +58,13 @@ class DUID {
/// @brief Returns a const reference to the actual DUID value
///
- /// Note: For safety reasons, this method returns a copy of data as
- /// otherwise the reference would be only valid as long as the object that
- /// returned it. In any case, this method should be used only sporadically.
- /// If there are frequent uses, we must implement some other method
- /// (e.g. storeSelf()) that will avoid data copying.
- std::vector<uint8_t> getDuid() const;
+ /// @warning Since this function returns a reference to the vector (not a
+ /// copy) the returned object must be used with caution because it remains
+ /// valid only for the time period when the object which returned it is
+ /// valid.
+ ///
+ /// @return A reference to a vector holding a DUID.
+ const std::vector<uint8_t>& getDuid() const;
/// @brief Returns the DUID type
DUIDType getType() const;
@@ -116,8 +117,15 @@ public:
/// @brief Constructor based on array and array size
ClientId(const uint8_t* clientid, size_t len);
- /// @brief Returns reference to the client-id data
- std::vector<uint8_t> getClientId() const;
+ /// @brief Returns reference to the client-id data.
+ ///
+ /// @warning Since this function returns a reference to the vector (not a
+ /// copy) the returned object must be used with caution because it remains
+ /// valid only for the time period when the object which returned it is
+ /// valid.
+ ///
+ /// @return A reference to a vector holding a client identifier.
+ const std::vector<uint8_t>& getClientId() const;
/// @brief Returns textual representation of a DUID (e.g. 00:01:02:03:ff)
std::string toText() const;
diff --git a/src/lib/dhcp/hwaddr.h b/src/lib/dhcp/hwaddr.h
index 13a16bf..848ad23 100644
--- a/src/lib/dhcp/hwaddr.h
+++ b/src/lib/dhcp/hwaddr.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -27,6 +27,10 @@ namespace dhcp {
/// @brief Hardware type that represents information from DHCPv4 packet
struct HWAddr {
public:
+
+ /// @brief Size of an ethernet hardware address.
+ static const size_t ETHERNET_HWADDR_LEN = 6;
+
/// @brief Maximum size of a hardware address.
static const size_t MAX_HWADDR_LEN = 20;
diff --git a/src/lib/dhcp/iface_mgr.cc b/src/lib/dhcp/iface_mgr.cc
index 74f5fe8..e3ffe48 100644
--- a/src/lib/dhcp/iface_mgr.cc
+++ b/src/lib/dhcp/iface_mgr.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -23,10 +23,12 @@
#include <dhcp/dhcp6.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/pkt_filter_inet.h>
+#include <dhcp/pkt_filter_inet6.h>
#include <exceptions/exceptions.h>
#include <util/io/pktinfo_utilities.h>
-
+#include <cstring>
+#include <errno.h>
#include <fstream>
#include <sstream>
@@ -35,6 +37,40 @@
#include <string.h>
#include <sys/select.h>
+/// @brief A macro which handles an error in IfaceMgr.
+///
+/// There are certain cases when IfaceMgr may hit an error which shouldn't
+/// result in interruption of the function processing. A typical case is
+/// the function which opens sockets on available interfaces for a DHCP
+/// server. If this function fails to open a socket on a specific interface
+/// (for example, there is another socket already open on this interface
+/// and bound to the same address and port), it is desired that the server
+/// logs a warning but will try to open sockets on other interfaces. In order
+/// to log an error, the IfaceMgr will use the error handler function provided
+/// by the server and pass an error string to it. When the handler function
+/// returns, the IfaceMgr will proceed to open other sockets. It is allowed
+/// that the error handler function is not installed (is NULL). In these
+/// cases it is expected that the exception is thrown instead. A possible
+/// solution would be to enclose this conditional behavior in a function.
+/// However, despite the hate for macros, the macro seems to be a bit
+/// better solution in this case as it allows to convenietly pass an
+/// error string in a stream (not as a string).
+///
+/// @param ex_type Exception to be thrown if error_handler is NULL.
+/// @param handler Error handler function to be called or NULL to indicate
+/// that exception should be thrown instead.
+/// @param stream stream object holding an error string.
+#define IFACEMGR_ERROR(ex_type, handler, stream) \
+{ \
+ std::ostringstream oss__; \
+ oss__ << stream; \
+ if (handler) { \
+ handler(oss__.str()); \
+ } else { \
+ isc_throw(ex_type, oss__); \
+ } \
+} \
+
using namespace std;
using namespace isc::asiolink;
using namespace isc::util::io::internal;
@@ -51,18 +87,56 @@ IfaceMgr::instance() {
Iface::Iface(const std::string& name, int ifindex)
:name_(name), ifindex_(ifindex), mac_len_(0), hardware_type_(0),
flag_loopback_(false), flag_up_(false), flag_running_(false),
- flag_multicast_(false), flag_broadcast_(false), flags_(0)
+ flag_multicast_(false), flag_broadcast_(false), flags_(0),
+ inactive4_(false), inactive6_(false)
{
memset(mac_, 0, sizeof(mac_));
}
void
Iface::closeSockets() {
- for (SocketCollection::iterator sock = sockets_.begin();
- sock != sockets_.end(); ++sock) {
- close(sock->sockfd_);
+ // Close IPv4 sockets.
+ closeSockets(AF_INET);
+ // Close IPv6 sockets.
+ closeSockets(AF_INET6);
+}
+
+void
+Iface::closeSockets(const uint16_t family) {
+ // Check that the correect 'family' value has been specified.
+ // The possible values are AF_INET or AF_INET6. Note that, in
+ // the current code they are used to differentiate that the
+ // socket is used to transmit IPv4 or IPv6 traffic. However,
+ // the actual family types of the sockets may be different,
+ // e.g. for LPF we are using raw sockets of AF_PACKET family.
+ //
+ // @todo Consider replacing the AF_INET and AF_INET6 with some
+ // enum which will not be confused with the actual socket type.
+ if ((family != AF_INET) && (family != AF_INET6)) {
+ isc_throw(BadValue, "Invalid socket family " << family
+ << " specified when requested to close all sockets"
+ << " which belong to this family");
+ }
+ // Search for the socket of the specific type.
+ SocketCollection::iterator sock = sockets_.begin();
+ while (sock != sockets_.end()) {
+ if (sock->family_ == family) {
+ // Close and delete the socket and move to the
+ // next one.
+ close(sock->sockfd_);
+ // Close fallback socket if open.
+ if (sock->fallbackfd_ >= 0) {
+ close(sock->fallbackfd_);
+ }
+ sockets_.erase(sock++);
+
+ } else {
+ // Different type of socket. Let's move
+ // to the next one.
+ ++sock;
+
+ }
}
- sockets_.clear();
}
std::string
@@ -114,6 +188,10 @@ bool Iface::delSocket(uint16_t sockfd) {
while (sock!=sockets_.end()) {
if (sock->sockfd_ == sockfd) {
close(sockfd);
+ // Close fallback socket if open.
+ if (sock->fallbackfd_ >= 0) {
+ close(sock->fallbackfd_);
+ }
sockets_.erase(sock);
return (true); //socket found
}
@@ -126,7 +204,8 @@ IfaceMgr::IfaceMgr()
:control_buf_len_(CMSG_SPACE(sizeof(struct in6_pktinfo))),
control_buf_(new char[control_buf_len_]),
session_socket_(INVALID_SOCKET), session_callback_(NULL),
- packet_filter_(new PktFilterInet())
+ packet_filter_(new PktFilterInet()),
+ packet_filter6_(new PktFilterInet6())
{
try {
@@ -143,6 +222,17 @@ IfaceMgr::IfaceMgr()
}
}
+void Iface::addUnicast(const isc::asiolink::IOAddress& addr) {
+ for (Iface::AddressCollection::const_iterator i = unicasts_.begin();
+ i != unicasts_.end(); ++i) {
+ if (*i == addr) {
+ isc_throw(BadValue, "Address " << addr
+ << " already defined on the " << name_ << " interface.");
+ }
+ }
+ unicasts_.push_back(addr);
+}
+
void IfaceMgr::closeSockets() {
for (IfaceCollection::iterator iface = ifaces_.begin();
iface != ifaces_.end(); ++iface) {
@@ -150,6 +240,14 @@ void IfaceMgr::closeSockets() {
}
}
+void
+IfaceMgr::closeSockets(const uint16_t family) {
+ for (IfaceCollection::iterator iface = ifaces_.begin();
+ iface != ifaces_.end(); ++iface) {
+ iface->closeSockets(family);
+ }
+}
+
IfaceMgr::~IfaceMgr() {
// control_buf_ is deleted automatically (scoped_ptr)
control_buf_len_ = 0;
@@ -157,6 +255,90 @@ IfaceMgr::~IfaceMgr() {
closeSockets();
}
+bool
+IfaceMgr::isDirectResponseSupported() const {
+ return (packet_filter_->isDirectResponseSupported());
+}
+
+void
+IfaceMgr::setPacketFilter(const PktFilterPtr& packet_filter) {
+ // Do not allow NULL pointer.
+ if (!packet_filter) {
+ isc_throw(InvalidPacketFilter, "NULL packet filter object specified for"
+ " DHCPv4");
+ }
+ // Different packet filters use different socket types. It does not make
+ // sense to allow the change of packet filter when there are IPv4 sockets
+ // open because they can't be used by the receive/send functions of the
+ // new packet filter. Below, we check that there are no open IPv4 sockets.
+ // If we find at least one, we have to fail. However, caller still has a
+ // chance to replace the packet filter if he closes sockets explicitly.
+ if (hasOpenSocket(AF_INET)) {
+ // There is at least one socket open, so we have to fail.
+ isc_throw(PacketFilterChangeDenied,
+ "it is not allowed to set new packet"
+ << " filter when there are open IPv4 sockets - need"
+ << " to close them first");
+ }
+ // Everything is fine, so replace packet filter.
+ packet_filter_ = packet_filter;
+}
+
+void
+IfaceMgr::setPacketFilter(const PktFilter6Ptr& packet_filter) {
+ if (!packet_filter) {
+ isc_throw(InvalidPacketFilter, "NULL packet filter object specified for"
+ " DHCPv6");
+ }
+
+ if (hasOpenSocket(AF_INET6)) {
+ // There is at least one socket open, so we have to fail.
+ isc_throw(PacketFilterChangeDenied,
+ "it is not allowed to set new packet"
+ << " filter when there are open IPv6 sockets - need"
+ << " to close them first");
+ }
+
+ packet_filter6_ = packet_filter;
+}
+
+bool
+IfaceMgr::hasOpenSocket(const uint16_t family) const {
+ // Iterate over all interfaces and search for open sockets.
+ for (IfaceCollection::const_iterator iface = ifaces_.begin();
+ iface != ifaces_.end(); ++iface) {
+ const Iface::SocketCollection& sockets = iface->getSockets();
+ for (Iface::SocketCollection::const_iterator sock = sockets.begin();
+ sock != sockets.end(); ++sock) {
+ // Check if the socket matches specified family.
+ if (sock->family_ == family) {
+ // There is at least one socket open, so return.
+ return (true);
+ }
+ }
+ }
+ // There are no open sockets found for the specified family.
+ return (false);
+}
+
+bool
+IfaceMgr::hasOpenSocket(const IOAddress& addr) const {
+ // Iterate over all interfaces and search for open sockets.
+ for (IfaceCollection::const_iterator iface = ifaces_.begin();
+ iface != ifaces_.end(); ++iface) {
+ const Iface::SocketCollection& sockets = iface->getSockets();
+ for (Iface::SocketCollection::const_iterator sock = sockets.begin();
+ sock != sockets.end(); ++sock) {
+ // Check if the socket address matches the specified address.
+ if (sock->addr_ == addr) {
+ return (true);
+ }
+ }
+ }
+ // There are no open sockets found for the specified family.
+ return (false);
+}
+
void IfaceMgr::stubDetectIfaces() {
string ifaceName;
const string v4addr("127.0.0.1"), v6addr("::1");
@@ -195,8 +377,11 @@ void IfaceMgr::stubDetectIfaces() {
addInterface(iface);
}
-bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
- int sock;
+
+
+bool
+IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast,
+ IfaceMgrErrorMsgCallback error_handler) {
int count = 0;
// This option is used to bind sockets to particular interfaces.
@@ -218,7 +403,8 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
if (iface->flag_loopback_ ||
!iface->flag_up_ ||
- !iface->flag_running_) {
+ !iface->flag_running_ ||
+ iface->inactive4_) {
continue;
}
@@ -241,41 +427,58 @@ bool IfaceMgr::openSockets4(const uint16_t port, const bool use_bcast) {
// bind to INADDR_ANY address but we can do it only once. Thus,
// if one socket has been bound we can't do it any further.
if (!bind_to_device && bcast_num > 0) {
- isc_throw(SocketConfigError, "SO_BINDTODEVICE socket option is"
- << " not supported on this OS; therefore, DHCP"
- << " server can only listen broadcast traffic on"
- << " a single interface");
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "SO_BINDTODEVICE socket option is"
+ " not supported on this OS;"
+ " therefore, DHCP server can only"
+ " listen broadcast traffic on a"
+ " single interface");
+ continue;
} else {
- // We haven't open any broadcast sockets yet, so we can
- // open at least one more.
- sock = openSocket(iface->getName(), *addr, port, true, true);
- // Binding socket to an interface is not supported so we can't
- // open any more broadcast sockets. Increase the number of
- // opened broadcast sockets.
+ try {
+ // We haven't open any broadcast sockets yet, so we can
+ // open at least one more.
+ openSocket(iface->getName(), *addr, port, true, true);
+ } catch (const Exception& ex) {
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "failed to open socket on interface "
+ << iface->getName() << ", reason: "
+ << ex.what());
+ continue;
+
+ }
+ // Binding socket to an interface is not supported so we
+ // can't open any more broadcast sockets. Increase the
+ // number of open broadcast sockets.
if (!bind_to_device) {
++bcast_num;
}
}
} else {
- // Not broadcast capable, do not set broadcast flags.
- sock = openSocket(iface->getName(), *addr, port, false, false);
+ try {
+ // Not broadcast capable, do not set broadcast flags.
+ openSocket(iface->getName(), *addr, port, false, false);
+ } catch (const Exception& ex) {
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "failed to open socket on interface "
+ << iface->getName() << ", reason: "
+ << ex.what());
+ continue;
+ }
}
- if (sock < 0) {
- isc_throw(SocketConfigError, "failed to open IPv4 socket"
- << " supporting broadcast traffic");
- }
+ ++count;
- count++;
}
}
return (count > 0);
}
-bool IfaceMgr::openSockets6(const uint16_t port) {
- int sock;
+bool
+IfaceMgr::openSockets6(const uint16_t port,
+ IfaceMgrErrorMsgCallback error_handler) {
int count = 0;
for (IfaceCollection::iterator iface = ifaces_.begin();
@@ -284,10 +487,30 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
if (iface->flag_loopback_ ||
!iface->flag_up_ ||
- !iface->flag_running_) {
+ !iface->flag_running_ ||
+ iface->inactive6_) {
continue;
}
+ // Open unicast sockets if there are any unicast addresses defined
+ Iface::AddressCollection unicasts = iface->getUnicasts();
+ for (Iface::AddressCollection::iterator addr = unicasts.begin();
+ addr != unicasts.end(); ++addr) {
+
+ try {
+ openSocket(iface->getName(), *addr, port);
+ } catch (const Exception& ex) {
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "Failed to open unicast socket on interface "
+ << iface->getName() << ", reason: "
+ << ex.what());
+ continue;
+ }
+
+ count++;
+
+ }
+
Iface::AddressCollection addrs = iface->getAddresses();
for (Iface::AddressCollection::iterator addr = addrs.begin();
addr != addrs.end();
@@ -307,20 +530,35 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
continue;
}
- sock = openSocket(iface->getName(), *addr, port);
- if (sock < 0) {
- isc_throw(SocketConfigError, "failed to open unicast socket");
- }
-
- // Binding socket to unicast address and then joining multicast group
- // works well on Mac OS (and possibly other BSDs), but does not work
- // on Linux.
- if ( !joinMulticast(sock, iface->getName(),
- string(ALL_DHCP_RELAY_AGENTS_AND_SERVERS))) {
- close(sock);
- isc_throw(SocketConfigError, "Failed to join "
- << ALL_DHCP_RELAY_AGENTS_AND_SERVERS
- << " multicast group.");
+ // Open socket and join multicast group only if the interface
+ // is multicast-capable.
+ // @todo The DHCPv6 requires multicast so we may want to think
+ // whether we want to open the socket on a multicast-incapable
+ // interface or not. For now, we prefer to be liberal and allow
+ // it for some odd use cases which may utilize non-multicast
+ // interfaces. Perhaps a warning should be emitted if the
+ // interface is not a multicast one.
+
+ // The sock variable will hold a socket descriptor. It may be
+ // used to close a socket if the function fails to bind to
+ // multicast address on Linux systems. Because we only bind
+ // a socket to multicast address on Linux, on other systems
+ // the sock variable will be initialized but unused. We have
+ // to suppress the cppcheck warning which shows up on non-Linux
+ // systems.
+ // cppcheck-suppress variableScope
+ int sock;
+ try {
+ // cppcheck-suppress unreadVariable
+ sock = openSocket(iface->getName(), *addr, port,
+ iface->flag_multicast_);
+
+ } catch (const Exception& ex) {
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "Failed to open link-local socket on "
+ " interface " << iface->getName() << ": "
+ << ex.what());
+ continue;
}
count++;
@@ -329,14 +567,20 @@ bool IfaceMgr::openSockets6(const uint16_t port) {
#if defined(OS_LINUX)
// To receive multicast traffic, Linux requires binding socket to
// a multicast group. That in turn doesn't work on NetBSD.
-
- int sock2 = openSocket(iface->getName(),
- IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS),
- port);
- if (sock2 < 0) {
- isc_throw(SocketConfigError, "Failed to open multicast socket on "
- << " interface " << iface->getFullName());
- iface->delSocket(sock); // delete previously opened socket
+ if (iface->flag_multicast_) {
+ try {
+ openSocket(iface->getName(),
+ IOAddress(ALL_DHCP_RELAY_AGENTS_AND_SERVERS),
+ port);
+ } catch (const Exception& ex) {
+ // Delete previously opened socket.
+ iface->delSocket(sock);
+ IFACEMGR_ERROR(SocketConfigError, error_handler,
+ "Failed to open multicast socket on"
+ " interface " << iface->getName()
+ << ", reason: " << ex.what());
+ continue;
+ }
}
#endif
}
@@ -396,6 +640,11 @@ IfaceMgr::getIface(const std::string& ifname) {
return (NULL); // not found
}
+void
+IfaceMgr::clearIfaces() {
+ ifaces_.clear();
+}
+
int IfaceMgr::openSocket(const std::string& ifname, const IOAddress& addr,
const uint16_t port, const bool receive_bcast,
const bool send_bcast) {
@@ -407,11 +656,11 @@ int IfaceMgr::openSocket(const std::string& ifname, const IOAddress& addr,
return openSocket4(*iface, addr, port, receive_bcast, send_bcast);
} else if (addr.isV6()) {
- return openSocket6(*iface, addr, port);
+ return openSocket6(*iface, addr, port, receive_bcast);
} else {
isc_throw(BadValue, "Failed to detect family of address: "
- << addr.toText());
+ << addr);
}
}
@@ -436,7 +685,7 @@ int IfaceMgr::openSocketFromIface(const std::string& ifname,
if (addr_it->getFamily() == family) {
// We have interface and address so let's open socket.
// This may cause isc::Unexpected exception.
- return (openSocket(iface->getName(), *addr_it, port));
+ return (openSocket(iface->getName(), *addr_it, port, false));
}
++addr_it;
}
@@ -480,13 +729,13 @@ int IfaceMgr::openSocketFromAddress(const IOAddress& addr,
if (*addr_it == addr) {
// Open socket using local interface, address and port.
// This may cause isc::Unexpected exception.
- return (openSocket(iface->getName(), *addr_it, port));
+ return (openSocket(iface->getName(), *addr_it, port, false));
}
}
}
// If we got here it means that we did not find specified address
// on any available interface.
- isc_throw(BadValue, "There is no such address " << addr.toText());
+ isc_throw(BadValue, "There is no such address " << addr);
}
int IfaceMgr::openSocketFromRemoteAddress(const IOAddress& remote_addr,
@@ -528,7 +777,9 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) {
// interface.
sock.open(asio::ip::udp::v4(), err_code);
if (err_code) {
- isc_throw(Unexpected, "failed to open UDPv4 socket");
+ const char* errstr = strerror(errno);
+ isc_throw(Unexpected, "failed to open UDPv4 socket, reason:"
+ << errstr);
}
sock.set_option(asio::socket_base::broadcast(true), err_code);
if (err_code) {
@@ -557,194 +808,40 @@ IfaceMgr::getLocalAddress(const IOAddress& remote_addr, const uint16_t port) {
}
-int IfaceMgr::openSocket6(Iface& iface, const IOAddress& addr, uint16_t port) {
-
- struct sockaddr_in6 addr6;
- memset(&addr6, 0, sizeof(addr6));
- addr6.sin6_family = AF_INET6;
- addr6.sin6_port = htons(port);
- if (addr.toText() != "::1") {
- addr6.sin6_scope_id = if_nametoindex(iface.getName().c_str());
- }
-
- memcpy(&addr6.sin6_addr, &addr.toBytes()[0], sizeof(addr6.sin6_addr));
-#ifdef HAVE_SA_LEN
- addr6.sin6_len = sizeof(addr6);
-#endif
-
- // TODO: use sockcreator once it becomes available
-
- // make a socket
- int sock = socket(AF_INET6, SOCK_DGRAM, 0);
- if (sock < 0) {
- isc_throw(SocketConfigError, "Failed to create UDP6 socket.");
- }
-
- // Set the REUSEADDR option so that we don't fail to start if
- // we're being restarted.
- int flag = 1;
- if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
- (char *)&flag, sizeof(flag)) < 0) {
- close(sock);
- isc_throw(SocketConfigError, "Can't set SO_REUSEADDR option on dhcpv6 socket.");
- }
-
- if (bind(sock, (struct sockaddr *)&addr6, sizeof(addr6)) < 0) {
- close(sock);
- isc_throw(SocketConfigError, "Failed to bind socket " << sock << " to " << addr.toText()
- << "/port=" << port);
- }
-#ifdef IPV6_RECVPKTINFO
- // RFC3542 - a new way
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO,
- &flag, sizeof(flag)) != 0) {
- close(sock);
- isc_throw(SocketConfigError, "setsockopt: IPV6_RECVPKTINFO failed.");
- }
-#else
- // RFC2292 - an old way
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_PKTINFO,
- &flag, sizeof(flag)) != 0) {
- close(sock);
- isc_throw(SocketConfigError, "setsockopt: IPV6_PKTINFO: failed.");
- }
-#endif
-
- // multicast stuff
- if (addr.getAddress().to_v6().is_multicast()) {
- // both mcast (ALL_DHCP_RELAY_AGENTS_AND_SERVERS and ALL_DHCP_SERVERS)
- // are link and site-scoped, so there is no sense to join those groups
- // with global addresses.
-
- if ( !joinMulticast( sock, iface.getName(),
- string(ALL_DHCP_RELAY_AGENTS_AND_SERVERS) ) ) {
- close(sock);
- isc_throw(SocketConfigError, "Failed to join " << ALL_DHCP_RELAY_AGENTS_AND_SERVERS
- << " multicast group.");
- }
- }
-
- SocketInfo info(sock, addr, port);
+int
+IfaceMgr::openSocket6(Iface& iface, const IOAddress& addr, uint16_t port,
+ const bool join_multicast) {
+ // Assuming that packet filter is not NULL, because its modifier checks it.
+ SocketInfo info = packet_filter6_->openSocket(iface, addr, port,
+ join_multicast);
iface.addSocket(info);
- return (sock);
+ return (info.sockfd_);
}
-int IfaceMgr::openSocket4(Iface& iface, const IOAddress& addr,
+int
+IfaceMgr::openSocket4(Iface& iface, const IOAddress& addr,
const uint16_t port, const bool receive_bcast,
const bool send_bcast) {
- // Skip checking if the packet_filter_ is non-NULL because this check
- // has been already done when packet filter object was set.
-
- int sock = packet_filter_->openSocket(iface, addr, port,
- receive_bcast, send_bcast);
-
- SocketInfo info(sock, addr, port);
+ // Assuming that packet filter is not NULL, because its modifier checks it.
+ SocketInfo info = packet_filter_->openSocket(iface, addr, port,
+ receive_bcast, send_bcast);
iface.addSocket(info);
- return (sock);
-}
-
-bool
-IfaceMgr::joinMulticast(int sock, const std::string& ifname,
-const std::string & mcast) {
-
- struct ipv6_mreq mreq;
-
- if (inet_pton(AF_INET6, mcast.c_str(),
- &mreq.ipv6mr_multiaddr) <= 0) {
- return (false);
- }
-
- mreq.ipv6mr_interface = if_nametoindex(ifname.c_str());
- if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP,
- &mreq, sizeof(mreq)) < 0) {
- return (false);
- }
-
- return (true);
+ return (info.sockfd_);
}
bool
IfaceMgr::send(const Pkt6Ptr& pkt) {
- int result;
-
Iface* iface = getIface(pkt->getIface());
if (!iface) {
- isc_throw(BadValue, "Unable to send Pkt6. Invalid interface ("
+ isc_throw(BadValue, "Unable to send DHCPv6 message. Invalid interface ("
<< pkt->getIface() << ") specified.");
}
- memset(&control_buf_[0], 0, control_buf_len_);
-
-
- // Set the target address we're sending to.
- sockaddr_in6 to;
- memset(&to, 0, sizeof(to));
- to.sin6_family = AF_INET6;
- to.sin6_port = htons(pkt->getRemotePort());
- memcpy(&to.sin6_addr,
- &pkt->getRemoteAddr().toBytes()[0],
- 16);
- to.sin6_scope_id = pkt->getIndex();
-
- // Initialize our message header structure.
- struct msghdr m;
- memset(&m, 0, sizeof(m));
- m.msg_name = &to;
- m.msg_namelen = sizeof(to);
-
- // Set the data buffer we're sending. (Using this wacky
- // "scatter-gather" stuff... we only have a single chunk
- // of data to send, so we declare a single vector entry.)
-
- // As v structure is a C-style is used for both sending and
- // receiving data, it is shared between sending and receiving
- // (sendmsg and recvmsg). It is also defined in system headers,
- // so we have no control over its definition. To set iov_base
- // (defined as void*) we must use const cast from void *.
- // Otherwise C++ compiler would complain that we are trying
- // to assign const void* to void*.
- struct iovec v;
- memset(&v, 0, sizeof(v));
- v.iov_base = const_cast<void *>(pkt->getBuffer().getData());
- v.iov_len = pkt->getBuffer().getLength();
- m.msg_iov = &v;
- m.msg_iovlen = 1;
-
- // Setting the interface is a bit more involved.
- //
- // We have to create a "control message", and set that to
- // define the IPv6 packet information. We could set the
- // source address if we wanted, but we can safely let the
- // kernel decide what that should be.
- m.msg_control = &control_buf_[0];
- m.msg_controllen = control_buf_len_;
- struct cmsghdr *cmsg = CMSG_FIRSTHDR(&m);
-
- // FIXME: Code below assumes that cmsg is not NULL, but
- // CMSG_FIRSTHDR() is coded to return NULL as a possibility. The
- // following assertion should never fail, but if it did and you came
- // here, fix the code. :)
- assert(cmsg != NULL);
-
- cmsg->cmsg_level = IPPROTO_IPV6;
- cmsg->cmsg_type = IPV6_PKTINFO;
- cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
- struct in6_pktinfo *pktinfo = convertPktInfo6(CMSG_DATA(cmsg));
- memset(pktinfo, 0, sizeof(struct in6_pktinfo));
- pktinfo->ipi6_ifindex = pkt->getIndex();
- m.msg_controllen = cmsg->cmsg_len;
-
- pkt->updateTimestamp();
-
- result = sendmsg(getSocket(*pkt), &m, 0);
- if (result < 0) {
- isc_throw(SocketWriteError, "Pkt6 send failed: sendmsg() returned " << result);
- }
-
- return (result);
+ // Assuming that packet filter is not NULL, because its modifier checks it.
+ return (packet_filter6_->send(*iface, getSocket(*pkt), pkt));
}
bool
@@ -752,13 +849,12 @@ IfaceMgr::send(const Pkt4Ptr& pkt) {
Iface* iface = getIface(pkt->getIface());
if (!iface) {
- isc_throw(BadValue, "Unable to send Pkt4. Invalid interface ("
+ isc_throw(BadValue, "Unable to send DHCPv4 message. Invalid interface ("
<< pkt->getIface() << ") specified.");
}
- // Skip checking if packet filter is non-NULL because it has been
- // already checked when packet filter was set.
- return (packet_filter_->send(getSocket(*pkt), pkt));
+ // Assuming that packet filter is not NULL, because its modifier checks it.
+ return (packet_filter_->send(*iface, getSocket(*pkt).sockfd_, pkt));
}
@@ -856,8 +952,7 @@ IfaceMgr::receive4(uint32_t timeout_sec, uint32_t timeout_usec /* = 0 */) {
}
// Now we have a socket, let's get some data from it!
- // Skip checking if packet filter is non-NULL because it has been
- // already checked when packet filter was set.
+ // Assuming that packet filter is not NULL, because its modifier checks it.
return (packet_filter_->receive(*iface, *candidate));
}
@@ -952,139 +1047,78 @@ Pkt6Ptr IfaceMgr::receive6(uint32_t timeout_sec, uint32_t timeout_usec /* = 0 */
if (!candidate) {
isc_throw(SocketReadError, "received data over unknown socket");
}
-
- // Now we have a socket, let's get some data from it!
- uint8_t buf[RCVBUFSIZE];
- memset(&control_buf_[0], 0, control_buf_len_);
- struct sockaddr_in6 from;
- memset(&from, 0, sizeof(from));
-
- // Initialize our message header structure.
- struct msghdr m;
- memset(&m, 0, sizeof(m));
-
- // Point so we can get the from address.
- m.msg_name = &from;
- m.msg_namelen = sizeof(from);
-
- // Set the data buffer we're receiving. (Using this wacky
- // "scatter-gather" stuff... but we that doesn't really make
- // sense for us, so we use a single vector entry.)
- struct iovec v;
- memset(&v, 0, sizeof(v));
- v.iov_base = static_cast<void*>(buf);
- v.iov_len = RCVBUFSIZE;
- m.msg_iov = &v;
- m.msg_iovlen = 1;
-
- // Getting the interface is a bit more involved.
- //
- // We set up some space for a "control message". We have
- // previously asked the kernel to give us packet
- // information (when we initialized the interface), so we
- // should get the destination address from that.
- m.msg_control = &control_buf_[0];
- m.msg_controllen = control_buf_len_;
-
- result = recvmsg(candidate->sockfd_, &m, 0);
-
- struct in6_addr to_addr;
- memset(&to_addr, 0, sizeof(to_addr));
-
- int ifindex = -1;
- if (result >= 0) {
- struct in6_pktinfo* pktinfo = NULL;
-
-
- // If we did read successfully, then we need to loop
- // through the control messages we received and
- // find the one with our destination address.
- //
- // We also keep a flag to see if we found it. If we
- // didn't, then we consider this to be an error.
- bool found_pktinfo = false;
- struct cmsghdr* cmsg = CMSG_FIRSTHDR(&m);
- while (cmsg != NULL) {
- if ((cmsg->cmsg_level == IPPROTO_IPV6) &&
- (cmsg->cmsg_type == IPV6_PKTINFO)) {
- pktinfo = convertPktInfo6(CMSG_DATA(cmsg));
- to_addr = pktinfo->ipi6_addr;
- ifindex = pktinfo->ipi6_ifindex;
- found_pktinfo = true;
- break;
- }
- cmsg = CMSG_NXTHDR(&m, cmsg);
- }
- if (!found_pktinfo) {
- isc_throw(SocketReadError, "unable to find pktinfo");
- }
- } else {
- isc_throw(SocketReadError, "failed to receive data");
- }
-
- // Let's create a packet.
- Pkt6Ptr pkt;
- try {
- pkt = Pkt6Ptr(new Pkt6(buf, result));
- } catch (const std::exception& ex) {
- isc_throw(SocketReadError, "failed to create new packet");
- }
-
- pkt->updateTimestamp();
-
- pkt->setLocalAddr(IOAddress::fromBytes(AF_INET6,
- reinterpret_cast<const uint8_t*>(&to_addr)));
- pkt->setRemoteAddr(IOAddress::fromBytes(AF_INET6,
- reinterpret_cast<const uint8_t*>(&from.sin6_addr)));
- pkt->setRemotePort(ntohs(from.sin6_port));
- pkt->setIndex(ifindex);
-
- Iface* received = getIface(pkt->getIndex());
- if (received) {
- pkt->setIface(received->getName());
- } else {
- isc_throw(SocketReadError, "received packet over unknown interface"
- << "(ifindex=" << pkt->getIndex() << ")");
- }
-
- return (pkt);
+ // Assuming that packet filter is not NULL, because its modifier checks it.
+ return (packet_filter6_->receive(*candidate));
}
uint16_t IfaceMgr::getSocket(const isc::dhcp::Pkt6& pkt) {
Iface* iface = getIface(pkt.getIface());
if (iface == NULL) {
- isc_throw(BadValue, "Tried to find socket for non-existent interface "
- << pkt.getIface());
+ isc_throw(BadValue, "Tried to find socket for non-existent interface");
}
+
const Iface::SocketCollection& socket_collection = iface->getSockets();
+
+ Iface::SocketCollection::const_iterator candidate = socket_collection.end();
+
Iface::SocketCollection::const_iterator s;
for (s = socket_collection.begin(); s != socket_collection.end(); ++s) {
- if ((s->family_ == AF_INET6) &&
- (!s->addr_.getAddress().to_v6().is_multicast())) {
+
+ // We should not merge those conditions for debugging reasons.
+
+ // V4 sockets are useless for sending v6 packets.
+ if (s->family_ != AF_INET6) {
+ continue;
+ }
+
+ // Sockets bound to multicast address are useless for sending anything.
+ if (s->addr_.getAddress().to_v6().is_multicast()) {
+ continue;
+ }
+
+ if (s->addr_ == pkt.getLocalAddr()) {
+ // This socket is bound to the source address. This is perfect
+ // match, no need to look any further.
return (s->sockfd_);
}
- /// @todo: Add more checks here later. If remote address is
- /// not link-local, we can't use link local bound socket
- /// to send data.
+
+ // If we don't have any other candidate, this one will do
+ if (candidate == socket_collection.end()) {
+ candidate = s;
+ } else {
+ // If we want to send something to link-local and the socket is
+ // bound to link-local or we want to send to global and the socket
+ // is bound to global, then use it as candidate
+ if ( (pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
+ s->addr_.getAddress().to_v6().is_link_local()) ||
+ (!pkt.getRemoteAddr().getAddress().to_v6().is_link_local() &&
+ !s->addr_.getAddress().to_v6().is_link_local()) ) {
+ candidate = s;
+ }
+ }
+ }
+
+ if (candidate != socket_collection.end()) {
+ return (candidate->sockfd_);
}
isc_throw(Unexpected, "Interface " << iface->getFullName()
<< " does not have any suitable IPv6 sockets open.");
}
-uint16_t IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) {
+SocketInfo
+IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) {
Iface* iface = getIface(pkt.getIface());
if (iface == NULL) {
- isc_throw(BadValue, "Tried to find socket for non-existent interface "
- << pkt.getIface());
+ isc_throw(BadValue, "Tried to find socket for non-existent interface");
}
const Iface::SocketCollection& socket_collection = iface->getSockets();
Iface::SocketCollection::const_iterator s;
for (s = socket_collection.begin(); s != socket_collection.end(); ++s) {
if (s->family_ == AF_INET) {
- return (s->sockfd_);
+ return (*s);
}
/// TODO: Add more checks here later. If remote address is
/// not link-local, we can't use link local bound socket
@@ -1095,5 +1129,6 @@ uint16_t IfaceMgr::getSocket(isc::dhcp::Pkt4 const& pkt) {
<< " does not have any suitable IPv4 sockets open.");
}
+
} // end of namespace isc::dhcp
} // end of namespace isc
diff --git a/src/lib/dhcp/iface_mgr.h b/src/lib/dhcp/iface_mgr.h
index 2085b97..3fde867 100644
--- a/src/lib/dhcp/iface_mgr.h
+++ b/src/lib/dhcp/iface_mgr.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -21,7 +21,9 @@
#include <dhcp/pkt4.h>
#include <dhcp/pkt6.h>
#include <dhcp/pkt_filter.h>
+#include <dhcp/pkt_filter6.h>
+#include <boost/function.hpp>
#include <boost/noncopyable.hpp>
#include <boost/scoped_array.hpp>
#include <boost/shared_ptr.hpp>
@@ -39,10 +41,10 @@ public:
isc::Exception(file, line, what) { };
};
-/// @brief IfaceMgr exception thrown when invalid packet filter object specified.
-class InvalidPacketFilter : public Exception {
+/// @brief Exception thrown when it is not allowed to set new Packet Filter.
+class PacketFilterChangeDenied : public Exception {
public:
- InvalidPacketFilter(const char* file, size_t line, const char* what) :
+ PacketFilterChangeDenied(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) { };
};
@@ -72,23 +74,53 @@ public:
/// Holds information about socket.
struct SocketInfo {
- uint16_t sockfd_; /// socket descriptor
+
isc::asiolink::IOAddress addr_; /// bound address
uint16_t port_; /// socket port
uint16_t family_; /// IPv4 or IPv6
+ /// @brief Socket descriptor (a.k.a. primary socket).
+ int sockfd_;
+
+ /// @brief Fallback socket descriptor.
+ ///
+ /// This socket descriptor holds the handle to the fallback socket.
+ /// The fallback socket is created when there is a need for the regular
+ /// datagram socket to be bound to an IP address and port, besides
+ /// primary socket (sockfd_) which is actually used to receive and process
+ /// the DHCP messages. The fallback socket (if exists) is always associated
+ /// with the primary socket. In particular, the need for the fallback socket
+ /// arises when raw socket is a primary one. When primary socket is open,
+ /// it is bound to an interface not the address and port. The implications
+ /// include the possibility that the other process (e.g. the other instance
+ /// of DHCP server) will bind to the same address and port through which the
+ /// raw socket receives the DHCP messages.Another implication is that the
+ /// kernel, being unaware of the DHCP server operating through the raw
+ /// socket, will respond with the ICMP "Destination port unreachable"
+ /// messages when DHCP messages are only received through the raw socket.
+ /// In order to workaround the issues mentioned here, the fallback socket
+ /// should be opened so as/ the kernel is aware that the certain address
+ /// and port is in use.
+ ///
+ /// The fallback description is supposed to be set to a negative value if
+ /// the fallback socket is closed (not open).
+ int fallbackfd_;
+
/// @brief SocketInfo constructor.
///
- /// @param sockfd socket descriptor
- /// @param addr an address the socket is bound to
- /// @param port a port the socket is bound to
- SocketInfo(uint16_t sockfd, const isc::asiolink::IOAddress& addr,
- uint16_t port)
- :sockfd_(sockfd), addr_(addr), port_(port), family_(addr.getFamily()) { }
+ /// @param addr An address the socket is bound to.
+ /// @param port A port the socket is bound to.
+ /// @param sockfd Socket descriptor.
+ /// @param fallbackfd A descriptor of the fallback socket.
+ SocketInfo(const isc::asiolink::IOAddress& addr, const uint16_t port,
+ const int sockfd, const int fallbackfd = -1)
+ : addr_(addr), port_(port), family_(addr.getFamily()),
+ sockfd_(sockfd), fallbackfd_(fallbackfd) { }
+
};
-/// @brief represents a single network interface
+/// @brief Represents a single network interface
///
/// Iface structure represents network interface with all useful
/// information, like name, interface index, MAC address and
@@ -96,13 +128,20 @@ struct SocketInfo {
class Iface {
public:
- /// maximum MAC address length (Infiniband uses 20 bytes)
+ /// Maximum MAC address length (Infiniband uses 20 bytes)
static const unsigned int MAX_MAC_LEN = 20;
- /// type that defines list of addresses
+ /// Type that defines list of addresses
typedef std::vector<isc::asiolink::IOAddress> AddressCollection;
- /// type that holds a list of socket informations
+ /// @brief Type that holds a list of socket information.
+ ///
+ /// @warning The type of the container used here must guarantee
+ /// that the iterators do not invalidate when erase() is called.
+ /// This is because, the \ref closeSockets function removes
+ /// elements selectively by calling erase on the element to be
+ /// removed and further iterates through remaining elements.
+ ///
/// @todo: Add SocketCollectionConstIter type
typedef std::list<SocketInfo> SocketCollection;
@@ -117,6 +156,27 @@ public:
/// @brief Closes all open sockets on interface.
void closeSockets();
+ /// @brief Closes all IPv4 or IPv6 sockets.
+ ///
+ /// This function closes sockets of the specific 'type' and closes them.
+ /// The 'type' of the socket indicates whether it is used to send IPv4
+ /// or IPv6 packets. The allowed values of the parameter are AF_INET and
+ /// AF_INET6 for IPv4 and IPv6 packets respectively. It is important
+ /// to realize that the actual types of sockets may be different than
+ /// AF_INET for IPv4 packets. This is because, historically the IfaceMgr
+ /// always used AF_INET sockets for IPv4 traffic. This is no longer the
+ /// case when the Direct IPv4 traffic must be supported. In order to support
+ /// direct traffic, the IfaceMgr operates on raw sockets, e.g. AF_PACKET
+ /// family sockets on Linux.
+ ///
+ /// @todo Replace the AF_INET and AF_INET6 values with an enum
+ /// which will not be confused with the actual socket type.
+ ///
+ /// @param family type of the sockets to be closed (AF_INET or AF_INET6)
+ ///
+ /// @throw BadValue if family value is different than AF_INET or AF_INET6.
+ void closeSockets(const uint16_t family);
+
/// @brief Returns full interface name as "ifname/ifindex" string.
///
/// @return string with interface name
@@ -146,11 +206,12 @@ public:
/// @brief Sets flag_*_ fields based on bitmask value returned by OS
///
- /// Note: Implementation of this method is OS-dependent as bits have
+ /// @note Implementation of this method is OS-dependent as bits have
/// different meaning on each OS.
+ /// We need to make it 64 bits, because Solaris uses 64, not 32 bits.
///
/// @param flags bitmask value returned by OS in interface detection
- void setFlags(uint32_t flags);
+ void setFlags(uint64_t flags);
/// @brief Returns interface index.
///
@@ -236,54 +297,95 @@ public:
/// @return collection of sockets added to interface
const SocketCollection& getSockets() const { return sockets_; }
+ /// @brief Removes any unicast addresses
+ ///
+ /// Removes any unicast addresses that the server was configured to
+ /// listen on
+ void clearUnicasts() {
+ unicasts_.clear();
+ }
+
+ /// @brief Adds unicast the server should listen on
+ ///
+ /// @throw BadValue if specified address is already defined on interface
+ /// @param addr unicast address to listen on
+ void addUnicast(const isc::asiolink::IOAddress& addr);
+
+ /// @brief Returns a container of addresses the server should listen on
+ ///
+ /// @return address collection (may be empty)
+ const AddressCollection& getUnicasts() const {
+ return unicasts_;
+ }
+
protected:
- /// socket used to sending data
+ /// Socket used to send data.
SocketCollection sockets_;
- /// network interface name
+ /// Network interface name.
std::string name_;
- /// interface index (a value that uniquely indentifies an interface)
+ /// Interface index (a value that uniquely indentifies an interface).
int ifindex_;
- /// list of assigned addresses
+ /// List of assigned addresses.
AddressCollection addrs_;
- /// link-layer address
+ /// List of unicast addresses the server should listen on
+ AddressCollection unicasts_;
+
+ /// Link-layer address.
uint8_t mac_[MAX_MAC_LEN];
- /// length of link-layer address (usually 6)
+ /// Length of link-layer address (usually 6).
size_t mac_len_;
- /// hardware type
+ /// Hardware type.
uint16_t hardware_type_;
public:
/// @todo: Make those fields protected once we start supporting more
/// than just Linux
- /// specifies if selected interface is loopback
+ /// Specifies if selected interface is loopback.
bool flag_loopback_;
- /// specifies if selected interface is up
+ /// Specifies if selected interface is up.
bool flag_up_;
- /// flag specifies if selected interface is running
- /// (e.g. cable plugged in, wifi associated)
+ /// Flag specifies if selected interface is running
+ /// (e.g. cable plugged in, wifi associated).
bool flag_running_;
- /// flag specifies if selected interface is multicast capable
+ /// Flag specifies if selected interface is multicast capable.
bool flag_multicast_;
- /// flag specifies if selected interface is broadcast capable
+ /// Flag specifies if selected interface is broadcast capable.
bool flag_broadcast_;
- /// interface flags (this value is as is returned by OS,
- /// it may mean different things on different OSes)
- uint32_t flags_;
+ /// Interface flags (this value is as is returned by OS,
+ /// it may mean different things on different OSes).
+ /// Solaris based os have unsigned long flags field (64 bits).
+ /// It is usually 32 bits, though.
+ uint64_t flags_;
+
+ /// Indicates that IPv4 sockets should (true) or should not (false)
+ /// be opened on this interface.
+ bool inactive4_;
+
+ /// Indicates that IPv6 sockets should (true) or should not (false)
+ /// be opened on this interface.
+ bool inactive6_;
};
-/// @brief handles network interfaces, transmission and reception
+/// @brief This type describes the callback function invoked when error occurs
+/// in the IfaceMgr.
+///
+/// @param errmsg An error message.
+typedef
+boost::function<void(const std::string& errmsg)> IfaceMgrErrorMsgCallback;
+
+/// @brief Handles network interfaces, transmission and reception.
///
/// IfaceMgr is an interface manager class that detects available network
/// interfaces, configured addresses, link-local addresses, and provides
@@ -291,7 +393,7 @@ public:
///
class IfaceMgr : public boost::noncopyable {
public:
- /// defines callback used when commands are received over control session
+ /// Defines callback used when commands are received over control session.
typedef void (*SessionCallback) (void);
/// @brief Packet reception buffer size
@@ -307,7 +409,7 @@ public:
// 2 maps (ifindex-indexed and name-indexed) and
// also hide it (make it public make tests easier for now)
- /// type that holds a list of interfaces
+ /// Type that holds a list of interfaces.
typedef std::list<Iface> IfaceCollection;
/// IfaceMgr is a singleton class. This method returns reference
@@ -324,9 +426,9 @@ public:
/// the client.
///
/// @return true if direct response is supported.
- bool isDirectResponseSupported();
+ bool isDirectResponseSupported() const;
- /// @brief Returns interface with specified interface index
+ /// @brief Returns interfac specified interface index
///
/// @param ifindex index of searched interface
///
@@ -342,8 +444,7 @@ public:
/// @return interface with requested name (or NULL if no such
/// interface is present)
///
- Iface*
- getIface(const std::string& ifname);
+ Iface* getIface(const std::string& ifname);
/// @brief Returns container with all interfaces.
///
@@ -352,7 +453,22 @@ public:
/// main() function completes, you should not worry much about this.
///
/// @return container with all interfaces.
- const IfaceCollection& getIfaces() { return ifaces_; }
+ const IfaceCollection& getIfaces() { return (ifaces_); }
+
+ /// @brief Removes detected interfaces.
+ ///
+ /// This method removes all detected interfaces. This method should be
+ /// used by unit tests to supply a custom set of interfaces. For example:
+ /// a unit test may create a pool of fake interfaces and use the custom
+ /// @c PktFilter class to mimic socket operation on these interfaces.
+ void clearIfaces();
+
+ /// @brief Detects network interfaces.
+ ///
+ /// This method will eventually detect available interfaces. For now
+ /// it offers stub implementation. First interface name and link-local
+ /// IPv6 address is read from interfaces.txt file.
+ void detectIfaces();
/// @brief Return most suitable socket for transmitting specified IPv6 packet.
///
@@ -367,7 +483,7 @@ public:
/// @return a socket descriptor
uint16_t getSocket(const isc::dhcp::Pkt6& pkt);
- /// @brief Return most suitable socket for transmitting specified IPv6 packet.
+ /// @brief Return most suitable socket for transmitting specified IPv4 packet.
///
/// This method takes Pkt4 (see overloaded implementation that takes
/// Pkt6) and chooses appropriate socket to send it. This method
@@ -377,14 +493,13 @@ public:
///
/// @param pkt a packet to be transmitted
///
- /// @return a socket descriptor
- uint16_t getSocket(const isc::dhcp::Pkt4& pkt);
+ /// @return A structure describing a socket.
+ SocketInfo getSocket(const isc::dhcp::Pkt4& pkt);
- /// debugging method that prints out all available interfaces
+ /// Debugging method that prints out all available interfaces.
///
/// @param out specifies stream to print list of interfaces to
- void
- printIfaces(std::ostream& out = std::cout);
+ void printIfaces(std::ostream& out = std::cout);
/// @brief Sends an IPv6 packet.
///
@@ -454,8 +569,8 @@ public:
/// @param ifname name of the interface
/// @param addr address to be bound.
/// @param port UDP port.
- /// @param receive_bcast configure IPv4 socket to receive broadcast messages.
- /// This parameter is ignored for IPv6 sockets.
+ /// @param receive_bcast configure IPv4 socket to receive broadcast
+ /// messages or IPv6 socket to join multicast group.
/// @param send_bcast configure IPv4 socket to send broadcast messages.
/// This parameter is ignored for IPv6 sockets.
///
@@ -477,11 +592,13 @@ public:
/// Instead, the method searches through the addresses on the specified
/// interface and selects one that matches the address family.
///
+ /// @note This method does not join the socket to the multicast group.
+ ///
/// @param ifname name of the interface
/// @param port UDP port
/// @param family address family (AF_INET or AF_INET6)
- /// @return socket descriptor, if socket creation, binding and multicast
- /// group join were all successful.
+ /// @return socket descriptor, if socket creation and binding was
+ /// successful.
/// @throw isc::Unexpected if failed to create and bind socket.
/// @throw isc::BadValue if there is no address on specified interface
/// that belongs to given family.
@@ -494,10 +611,12 @@ public:
/// This methods differs from \ref openSocket in that it does not require
/// the specification of the interface to which the socket will be bound.
///
+ /// @note This method does not join the socket to the multicast group.
+ ///
/// @param addr address to be bound
/// @param port UDP port
- /// @return socket descriptor, if socket creation, binding and multicast
- /// group join were all successful.
+ /// @return socket descriptor, if socket creation and binding was
+ /// successful.
/// @throw isc::Unexpected if failed to create and bind socket
/// @throw isc::BadValue if specified address is not available on
/// any interface
@@ -511,41 +630,156 @@ public:
/// identified, \ref openSocket is called to open a socket and bind it to
/// the interface, address and port.
///
+ /// @note This method does not join the socket to a multicast group.
+ ///
/// @param remote_addr remote address to connect to
/// @param port UDP port
- /// @return socket descriptor, if socket creation, binding and multicast
- /// group join were all successful.
+ /// @return socket descriptor, if socket creation and binding was
+ /// successful.
/// @throw isc::Unexpected if failed to create and bind socket
int openSocketFromRemoteAddress(const isc::asiolink::IOAddress& remote_addr,
const uint16_t port);
- /// Opens IPv6 sockets on detected interfaces.
- ///
- /// Will throw exception if socket creation fails.
+ /// @brief Opens IPv6 sockets on detected interfaces.
+ ///
+ /// On the systems with multiple interfaces, it is often desired that the
+ /// failure to open a socket on a particular interface doesn't cause a
+ /// fatal error and sockets should be opened on remaining interfaces.
+ /// However, the warning about the failure for the particular socket should
+ /// be communicated to the caller. The libdhcp++ is a common library with
+ /// no logger associated with it. Most of the functions in this library
+ /// communicate errors via exceptions. In case of openSockets6 function
+ /// exception must not be thrown if the function is supposed to continue
+ /// opening sockets, despite an error. Therefore, if such a behavior is
+ /// desired, the error handler function can be passed as a parameter.
+ /// This error handler is called (if present) with an error string.
+ /// Typically, error handler will simply log an error using an application
+ /// logger, but it can do more sophisticated error handling too.
+ ///
+ /// @todo It is possible that additional parameters will have to be added
+ /// to the error handler, e.g. Iface if it was really supposed to do
+ /// some more sophisticated error handling.
+ ///
+ /// If the error handler is not installed (is NULL), the exception is thrown
+ /// for each failure (default behavior).
+ ///
+ /// @warning This function does not check if there has been any sockets
+ /// already open by the @c IfaceMgr. Therefore a caller should call
+ /// @c IfaceMgr::closeSockets(AF_INET6) before calling this function.
+ /// If there are any sockets open, the function may either throw an
+ /// exception or invoke an error handler on attempt to bind the new socket
+ /// to the same address and port.
///
/// @param port specifies port number (usually DHCP6_SERVER_PORT)
+ /// @param error_handler A pointer to an error handler function which is
+ /// called by the openSockets6 when it fails to open a socket. This
+ /// parameter can be NULL to indicate that the callback should not be used.
///
/// @throw SocketOpenFailure if tried and failed to open socket.
/// @return true if any sockets were open
- bool openSockets6(const uint16_t port = DHCP6_SERVER_PORT);
-
- /// Opens IPv4 sockets on detected interfaces.
- /// Will throw exception if socket creation fails.
+ bool openSockets6(const uint16_t port = DHCP6_SERVER_PORT,
+ IfaceMgrErrorMsgCallback error_handler = NULL);
+
+ /// @brief Opens IPv4 sockets on detected interfaces.
+ ///
+ /// This function attempts to open sockets on all interfaces which have been
+ /// detected by @c IfaceMgr and meet the following conditions:
+ /// - interface is not a local loopback,
+ /// - interface is running (connected),
+ /// - interface is up,
+ /// - interface is active, e.g. selected from the configuration to be used
+ /// to listen DHCPv4 messages,
+ /// - interface has an IPv4 address assigned.
+ ///
+ /// The type of the socket being open depends on the selected Packet Filter
+ /// represented by a class derived from @c isc::dhcp::PktFilter abstract
+ /// class.
+ ///
+ /// It is possible to specify whether sockets should be broadcast capable.
+ /// In most of the cases, the sockets should support broadcast traffic, e.g.
+ /// DHCPv4 server and relay need to listen to broadcast messages sent by
+ /// clients. If the socket has to be open on the particular interface, this
+ /// interface must have broadcast flag set. If this condition is not met,
+ /// the socket will be created in the unicast-only mode. If there are
+ /// multiple broadcast-capable interfaces present, they may be all open
+ /// in a broadcast mode only if the OS supports SO_BINDTODEVICE (bind socket
+ /// to a device) socket option. If this option is not supported, only the
+ /// first broadcast-capable socket will be opened in the broadcast mode.
+ /// The error will be reported for sockets being opened on other interfaces.
+ /// If the socket is bound to a device (interface), the broadcast traffic
+ /// sent to this interface will be received on this interface only.
+ /// This allows the DHCPv4 server or relay to detect the interface on which
+ /// the broadcast message has been received. This interface is later used
+ /// to send a response.
+ ///
+ /// On the systems with multiple interfaces, it is often desired that the
+ /// failure to open a socket on a particular interface doesn't cause a
+ /// fatal error and sockets should be opened on remaining interfaces.
+ /// However, the warning about the failure for the particular socket should
+ /// be communicated to the caller. The libdhcp++ is a common library with
+ /// no logger associated with it. Most of the functions in this library
+ /// communicate errors via exceptions. In case of openSockets4 function
+ /// exception must not be thrown if the function is supposed to continue
+ /// opening sockets, despite an error. Therefore, if such a behavior is
+ /// desired, the error handler function can be passed as a parameter.
+ /// This error handler is called (if present) with an error string.
+ /// Typically, error handler will simply log an error using an application
+ /// logger, but it can do more sophisticated error handling too.
+ ///
+ /// @todo It is possible that additional parameters will have to be added
+ /// to the error handler, e.g. Iface if it was really supposed to do
+ /// some more sophisticated error handling.
+ ///
+ /// If the error handler is not installed (is NULL), the exception is thrown
+ /// for each failure (default behavior).
+ ///
+ /// @warning This function does not check if there has been any sockets
+ /// already open by the @c IfaceMgr. Therefore a caller should call
+ /// @c IfaceMgr::closeSockets(AF_INET) before calling this function.
+ /// If there are any sockets open, the function may either throw an
+ /// exception or invoke an error handler on attempt to bind the new socket
+ /// to the same address and port.
///
/// @param port specifies port number (usually DHCP4_SERVER_PORT)
/// @param use_bcast configure sockets to support broadcast messages.
+ /// @param error_handler A pointer to an error handler function which is
+ /// called by the openSockets4 when it fails to open a socket. This
+ /// parameter can be NULL to indicate that the callback should not be used.
///
- /// @throw SocketOpenFailure if tried and failed to open socket.
+ /// @throw SocketOpenFailure if tried and failed to open socket and callback
+ /// function hasn't been specified.
/// @return true if any sockets were open
bool openSockets4(const uint16_t port = DHCP4_SERVER_PORT,
- const bool use_bcast = true);
+ const bool use_bcast = true,
+ IfaceMgrErrorMsgCallback error_handler = NULL);
/// @brief Closes all open sockets.
- /// Is used in destructor, but also from Dhcpv4_srv and Dhcpv6_srv classes.
+ /// Is used in destructor, but also from Dhcpv4Srv and Dhcpv6Srv classes.
void closeSockets();
- /// @brief returns number of detected interfaces
+ /// @brief Closes all IPv4 or IPv6 sockets.
+ ///
+ /// This function closes sockets of the specific 'type' and closes them.
+ /// The 'type' of the socket indicates whether it is used to send IPv4
+ /// or IPv6 packets. The allowed values of the parameter are AF_INET and
+ /// AF_INET6 for IPv4 and IPv6 packets respectively. It is important
+ /// to realize that the actual types of sockets may be different than
+ /// AF_INET for IPv4 packets. This is because, historically the IfaceMgr
+ /// always used AF_INET sockets for IPv4 traffic. This is no longer the
+ /// case when the Direct IPv4 traffic must be supported. In order to support
+ /// direct traffic, the IfaceMgr operates on raw sockets, e.g. AF_PACKET
+ /// family sockets on Linux.
+ ///
+ /// @todo Replace the AF_INET and AF_INET6 values with an enum
+ /// which will not be confused with the actual socket type.
+ ///
+ /// @param family type of the sockets to be closed (AF_INET or AF_INET6)
+ ///
+ /// @throw BadValue if family value is different than AF_INET or AF_INET6.
+ void closeSockets(const uint16_t family);
+
+ /// @brief Returns number of detected interfaces.
///
/// @return number of detected interfaces
uint16_t countIfaces() { return ifaces_.size(); }
@@ -562,24 +796,90 @@ public:
session_callback_ = callback;
}
- /// @brief Set Packet Filter object to handle send/receive packets.
+ /// @brief Set packet filter object to handle sending and receiving DHCPv4
+ /// messages.
///
- /// Packet Filters expose low-level functions handling sockets opening
- /// and sending/receiving packets through those sockets. This function
- /// sets custom Packet Filter (represented by a class derived from PktFilter)
- /// to be used by IfaceMgr.
+ /// Packet filter objects provide means for the @c IfaceMgr to open sockets
+ /// for IPv4 packets reception and sending. This function sets custom packet
+ /// filter (represented by a class derived from PktFilter) to be used by
+ /// @c IfaceMgr. Note that there must be no IPv4 sockets open when this
+ /// function is called. Call closeSockets(AF_INET) to close all hanging IPv4
+ /// sockets opened by the current packet filter object.
///
- /// @param packet_filter new packet filter to be used by IfaceMgr to send/receive
- /// packets and open sockets.
+ /// @param packet_filter A pointer to the new packet filter object to be
+ /// used by @c IfaceMgr.
///
/// @throw InvalidPacketFilter if provided packet filter object is NULL.
- void setPacketFilter(const boost::shared_ptr<PktFilter>& packet_filter) {
- if (!packet_filter) {
- isc_throw(InvalidPacketFilter, "NULL packet filter object specified");
- }
- packet_filter_ = packet_filter;
+ /// @throw PacketFilterChangeDenied if there are open IPv4 sockets.
+ void setPacketFilter(const PktFilterPtr& packet_filter);
+
+ /// @brief Set packet filter object to handle sending and receving DHCPv6
+ /// messages.
+ ///
+ /// Packet filter objects provide means for the @c IfaceMgr to open sockets
+ /// for IPv6 packets reception and sending. This function sets the new
+ /// instance of the packet filter which will be used by @c IfaceMgr to send
+ /// and receive DHCPv6 messages, until replaced by another packet filter.
+ ///
+ /// It is required that DHCPv6 messages are send and received using methods
+ /// of the same object that was used to open socket. Therefore, it is
+ /// required that all IPv6 sockets are closed prior to calling this
+ /// function. Call closeSockets(AF_INET6) to close all hanging IPv6 sockets
+ /// opened by the current packet filter object.
+ ///
+ /// @param packet_filter A pointer to the new packet filter object to be
+ /// used by @c IfaceMgr.
+ ///
+ /// @throw isc::dhcp::InvalidPacketFilter if specified object is NULL.
+ /// @throw isc::dhcp::PacketFilterChangeDenied if there are open IPv6
+ /// sockets.
+ void setPacketFilter(const PktFilter6Ptr& packet_filter);
+
+ /// @brief Set Packet Filter object to handle send/receive packets.
+ ///
+ /// This function sets Packet Filter object to be used by IfaceMgr,
+ /// appropriate for the current OS. Setting the argument to 'true'
+ /// indicates that function should set a packet filter class
+ /// which supports direct responses to clients having no address
+ /// assigned yet. Filters picked by this function will vary, depending
+ /// on the OS being used. There is no guarantee that there is an
+ /// implementation that supports this feature on a particular OS.
+ /// If there isn't, the PktFilterInet object will be set. If the
+ /// argument is set to 'false', PktFilterInet object instance will
+ /// be set as the Packet Filter regrdaless of the OS type.
+ ///
+ /// @param direct_response_desired specifies whether the Packet Filter
+ /// object being set should support direct traffic to the host
+ /// not having address assigned.
+ void setMatchingPacketFilter(const bool direct_response_desired = false);
+
+ /// @brief Adds an interface to list of known interfaces.
+ ///
+ /// @param iface reference to Iface object.
+ /// @note This function must be public because it has to be callable
+ /// from unit tests.
+ void addInterface(const Iface& iface) {
+ ifaces_.push_back(iface);
}
+ /// @brief Checks if there is at least one socket of the specified family
+ /// open.
+ ///
+ /// @param family A socket family.
+ ///
+ /// @return true if there is at least one socket open, false otherwise.
+ bool hasOpenSocket(const uint16_t family) const;
+
+ /// @brief Checks if there is a socket open and bound to an address.
+ ///
+ /// This function checks if one of the sockets opened by the IfaceMgr is
+ /// bound to the IP address specified as the method parameter.
+ ///
+ /// @param addr Address of the socket being searched.
+ ///
+ /// @return true if there is a socket bound to the specified address.
+ bool hasOpenSocket(const isc::asiolink::IOAddress& addr) const;
+
/// A value of socket descriptor representing "not specified" state.
static const int INVALID_SOCKET = -1;
@@ -620,24 +920,13 @@ protected:
/// @param iface reference to interface structure.
/// @param addr an address the created socket should be bound to
/// @param port a port that created socket should be bound to
+ /// @param join_multicast A boolean parameter which indicates whether
+ /// socket should join All_DHCP_Relay_Agents_and_servers multicast
+ /// group.
///
/// @return socket descriptor
- int openSocket6(Iface& iface, const isc::asiolink::IOAddress& addr, uint16_t port);
-
- /// @brief Adds an interface to list of known interfaces.
- ///
- /// @param iface reference to Iface object.
- void addInterface(const Iface& iface) {
- ifaces_.push_back(iface);
- }
-
- /// @brief Detects network interfaces.
- ///
- /// This method will eventually detect available interfaces. For now
- /// it offers stub implementation. First interface name and link-local
- /// IPv6 address is read from interfaces.txt file.
- void
- detectIfaces();
+ int openSocket6(Iface& iface, const isc::asiolink::IOAddress& addr,
+ uint16_t port, const bool join_multicast);
/// @brief Stub implementation of network interface detection.
///
@@ -660,14 +949,14 @@ protected:
//int recvsock_; // TODO: should be fd_set eventually, but we have only
//int sendsock_; // 2 sockets for now. Will do for until next release
- // we can't use the same socket, as receiving socket
+ // We can't use the same socket, as receiving socket
// is bound to multicast address. And we all know what happens
// to people who try to use multicast as source address.
- /// length of the control_buf_ array
+ /// Length of the control_buf_ array
size_t control_buf_len_;
- /// control-buffer, used in transmission and reception
+ /// Control-buffer, used in transmission and reception.
boost::scoped_array<char> control_buf_;
/// @brief A wrapper for OS-specific operations before sending IPv4 packet
@@ -687,30 +976,13 @@ protected:
/// @return true if successful, false otherwise
bool os_receive4(struct msghdr& m, Pkt4Ptr& pkt);
- /// socket descriptor of the session socket
+ /// Socket descriptor of the session socket.
int session_socket_;
- /// a callback that will be called when data arrives over session_socket_
+ /// A callback that will be called when data arrives over session_socket_.
SessionCallback session_callback_;
private:
- /// @brief Joins IPv6 multicast group on a socket.
- ///
- /// Socket must be created and bound to an address. Note that this
- /// address is different than the multicast address. For example DHCPv6
- /// server should bind its socket to link-local address (fe80::1234...)
- /// and later join ff02::1:2 multicast group.
- ///
- /// @param sock socket fd (socket must be bound)
- /// @param ifname interface name (for link-scoped multicast groups)
- /// @param mcast multicast address to join (e.g. "ff02::1:2")
- ///
- /// @return true if multicast join was successful
- ///
- bool
- joinMulticast(int sock, const std::string& ifname,
- const std::string& mcast);
-
/// @brief Identifies local network address to be used to
/// connect to remote address.
///
@@ -729,15 +1001,22 @@ private:
getLocalAddress(const isc::asiolink::IOAddress& remote_addr,
const uint16_t port);
+
/// Holds instance of a class derived from PktFilter, used by the
/// IfaceMgr to open sockets and send/receive packets through these
/// sockets. It is possible to supply custom object using
- /// setPacketFilter class. Various Packet Filters differ mainly by using
+ /// setPacketFilter method. Various Packet Filters differ mainly by using
/// different types of sockets, e.g. SOCK_DGRAM, SOCK_RAW and different
/// families, e.g. AF_INET, AF_PACKET etc. Another possible type of
/// Packet Filter is the one used for unit testing, which doesn't
/// open sockets but rather mimics their behavior (mock object).
- boost::shared_ptr<PktFilter> packet_filter_;
+ PktFilterPtr packet_filter_;
+
+ /// Holds instance of a class derived from PktFilter6, used by the
+ /// IfaceMgr to manage sockets used to send and receive DHCPv6
+ /// messages. It is possible to supply a custom object using
+ /// setPacketFilter method.
+ PktFilter6Ptr packet_filter6_;
};
}; // namespace isc::dhcp
diff --git a/src/lib/dhcp/iface_mgr_bsd.cc b/src/lib/dhcp/iface_mgr_bsd.cc
index afd97bb..be73bcf 100644
--- a/src/lib/dhcp/iface_mgr_bsd.cc
+++ b/src/lib/dhcp/iface_mgr_bsd.cc
@@ -17,8 +17,15 @@
#if defined(OS_BSD)
#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter_inet.h>
#include <exceptions/exceptions.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/if_dl.h>
+#include <net/if.h>
+#include <ifaddrs.h>
+
using namespace std;
using namespace isc;
using namespace isc::asiolink;
@@ -27,16 +34,97 @@ using namespace isc::dhcp;
namespace isc {
namespace dhcp {
+/// This is a BSD specific interface detection method.
void
IfaceMgr::detectIfaces() {
- /// @todo do the actual detection on BSDs. Currently just calling
- /// stub implementation.
- stubDetectIfaces();
+ struct ifaddrs* iflist = 0;// The whole interface list
+ struct ifaddrs* ifptr = 0; // The interface we're processing now
+
+ // Gets list of ifaddrs struct
+ if(getifaddrs(&iflist) != 0) {
+ isc_throw(Unexpected, "Network interfaces detection failed.");
+ }
+
+ typedef map<string, Iface> ifaceLst;
+ ifaceLst::iterator iface_iter;
+ ifaceLst ifaces;
+
+ // First lookup for getting interfaces ...
+ for (ifptr = iflist; ifptr != 0; ifptr = ifptr->ifa_next) {
+ const char * ifname = ifptr->ifa_name;
+ uint ifindex = 0;
+
+ if (!(ifindex = if_nametoindex(ifname))) {
+ // Interface name does not have corresponding index ...
+ freeifaddrs(iflist);
+ isc_throw(Unexpected, "Interface " << ifname << " has no index");
+ }
+
+ if ((iface_iter = ifaces.find(ifname)) != ifaces.end()) {
+ continue;
+ }
+
+ Iface iface(ifname, ifindex);
+ iface.setFlags(ifptr->ifa_flags);
+ ifaces.insert(pair<string, Iface>(ifname, iface));
+ }
+
+ // Second lookup to get MAC and IP addresses
+ for (ifptr = iflist; ifptr != 0; ifptr = ifptr->ifa_next) {
+ if ((iface_iter = ifaces.find(ifptr->ifa_name)) == ifaces.end()) {
+ continue;
+ }
+ // Common byte pointer for following data
+ const uint8_t * ptr = 0;
+ if(ifptr->ifa_addr->sa_family == AF_LINK) {
+ // HWAddr
+ struct sockaddr_dl * ldata =
+ reinterpret_cast<struct sockaddr_dl *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(LLADDR(ldata));
+
+ iface_iter->second.setHWType(ldata->sdl_type);
+ iface_iter->second.setMac(ptr, ldata->sdl_alen);
+ } else if(ifptr->ifa_addr->sa_family == AF_INET6) {
+ // IPv6 Addr
+ struct sockaddr_in6 * adata =
+ reinterpret_cast<struct sockaddr_in6 *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(&adata->sin6_addr);
+
+ IOAddress a = IOAddress::fromBytes(AF_INET6, ptr);
+ iface_iter->second.addAddress(a);
+ } else {
+ // IPv4 Addr
+ struct sockaddr_in * adata =
+ reinterpret_cast<struct sockaddr_in *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(&adata->sin_addr);
+
+ IOAddress a = IOAddress::fromBytes(AF_INET, ptr);
+ iface_iter->second.addAddress(a);
+ }
+ }
+
+ freeifaddrs(iflist);
+
+ // Interfaces registering
+ for(ifaceLst::const_iterator iface_iter = ifaces.begin();
+ iface_iter != ifaces.end(); ++iface_iter) {
+ ifaces_.push_back(iface_iter->second);
+ }
}
-bool
-IfaceMgr::isDirectResponseSupported() {
- return (false);
+/// @brief sets flag_*_ fields
+///
+/// Like Linux version, os specific flags
+///
+/// @params flags
+void Iface::setFlags(uint64_t flags) {
+ flags_ = flags;
+
+ flag_loopback_ = flags & IFF_LOOPBACK;
+ flag_up_ = flags & IFF_UP;
+ flag_running_ = flags & IFF_RUNNING;
+ flag_multicast_ = flags & IFF_MULTICAST;
+ flag_broadcast_ = flags & IFF_BROADCAST;
}
void IfaceMgr::os_send4(struct msghdr& /*m*/,
@@ -54,6 +142,14 @@ bool IfaceMgr::os_receive4(struct msghdr& /*m*/, Pkt4Ptr& /*pkt*/) {
return (true); // pretend that we have everything set up for reception.
}
+void
+IfaceMgr::setMatchingPacketFilter(const bool /* direct_response_desired */) {
+ // @todo Currently we ignore the preference to use direct traffic
+ // because it hasn't been implemented for BSD systems.
+ setPacketFilter(PktFilterPtr(new PktFilterInet()));
+}
+
+
} // end of isc::dhcp namespace
} // end of dhcp namespace
diff --git a/src/lib/dhcp/iface_mgr_linux.cc b/src/lib/dhcp/iface_mgr_linux.cc
index 71a32d8..dddeb52 100644
--- a/src/lib/dhcp/iface_mgr_linux.cc
+++ b/src/lib/dhcp/iface_mgr_linux.cc
@@ -33,6 +33,8 @@
#include <asiolink/io_address.h>
#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcp/pkt_filter_lpf.h>
#include <exceptions/exceptions.h>
#include <util/io/sockaddr_util.h>
@@ -494,18 +496,13 @@ void IfaceMgr::detectIfaces() {
nl.release_list(addr_info);
}
-bool
-IfaceMgr::isDirectResponseSupported() {
- return (false);
-}
-
/// @brief sets flag_*_ fields.
///
/// This implementation is OS-specific as bits have different meaning
/// on different OSes.
///
/// @param flags flags bitfield read from OS
-void Iface::setFlags(uint32_t flags) {
+void Iface::setFlags(uint64_t flags) {
flags_ = flags;
flag_loopback_ = flags & IFF_LOOPBACK;
@@ -515,6 +512,16 @@ void Iface::setFlags(uint32_t flags) {
flag_broadcast_ = flags & IFF_BROADCAST;
}
+void
+IfaceMgr::setMatchingPacketFilter(const bool direct_response_desired) {
+ if (direct_response_desired) {
+ setPacketFilter(PktFilterPtr(new PktFilterLPF()));
+
+ } else {
+ setPacketFilter(PktFilterPtr(new PktFilterInet()));
+
+ }
+}
void IfaceMgr::os_send4(struct msghdr&, boost::scoped_array<char>&,
size_t, const Pkt4Ptr&) {
diff --git a/src/lib/dhcp/iface_mgr_sun.cc b/src/lib/dhcp/iface_mgr_sun.cc
index 1556b70..fe2b0b3 100644
--- a/src/lib/dhcp/iface_mgr_sun.cc
+++ b/src/lib/dhcp/iface_mgr_sun.cc
@@ -17,8 +17,15 @@
#if defined(OS_SUN)
#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter_inet.h>
#include <exceptions/exceptions.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <net/if_dl.h>
+#include <net/if.h>
+#include <ifaddrs.h>
+
using namespace std;
using namespace isc;
using namespace isc::asiolink;
@@ -27,16 +34,100 @@ using namespace isc::dhcp;
namespace isc {
namespace dhcp {
+/// This is a Solaris specific interface detection code. It works on Solaris 11
+/// only, as earlier versions did not support getifaddrs() API.
void
IfaceMgr::detectIfaces() {
- /// @todo do the actual detection on Solaris. Currently just calling
- /// stub implementation.
- stubDetectIfaces();
+ struct ifaddrs * iflist = 0, * ifptr = 0;
+
+ // Gets list of ifaddrs struct
+ if(getifaddrs(& iflist) != 0) {
+ isc_throw(Unexpected, "Network interfaces detection failed.");
+ }
+
+ typedef std::map<string, Iface> ifaceLst;
+ ifaceLst::iterator iface_iter;
+ ifaceLst ifaces;
+
+ // First lookup for getting interfaces ...
+ for(ifptr = iflist; ifptr != 0; ifptr = ifptr->ifa_next) {
+ const char * ifname = ifptr->ifa_name;
+ uint ifindex = 0;
+
+ if (!(ifindex = if_nametoindex(ifname))) {
+ // Interface name does not have corresponding index ...
+ freeifaddrs(iflist);
+ isc_throw(Unexpected, "Interface " << ifname << " has no index");
+ }
+
+ iface_iter = ifaces.find(ifname);
+ if (iface_iter != ifaces.end()) {
+ continue;
+ }
+
+ Iface iface(ifname, ifindex);
+ iface.setFlags(ifptr->ifa_flags);
+ ifaces.insert(pair<string, Iface>(ifname, iface));
+ }
+
+ // Second lookup to get MAC and IP addresses
+ for (ifptr = iflist; ifptr != 0; ifptr = ifptr->ifa_next) {
+
+ iface_iter = ifaces.find(ifptr->ifa_name);
+ if (iface_iter == ifaces.end()) {
+ continue;
+ }
+ // Common byte pointer for following data
+ const uint8_t * ptr = 0;
+ if (ifptr->ifa_addr->sa_family == AF_LINK) {
+ // HWAddr
+ struct sockaddr_dl * ldata =
+ reinterpret_cast<struct sockaddr_dl *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(LLADDR(ldata));
+
+ iface_iter->second.setHWType(ldata->sdl_type);
+ iface_iter->second.setMac(ptr, ldata->sdl_alen);
+ } else if (ifptr->ifa_addr->sa_family == AF_INET6) {
+ // IPv6 Addr
+ struct sockaddr_in6 * adata =
+ reinterpret_cast<struct sockaddr_in6 *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(& adata->sin6_addr);
+
+ IOAddress a = IOAddress::fromBytes(AF_INET6, ptr);
+ iface_iter->second.addAddress(a);
+ } else {
+ // IPv4 Addr
+ struct sockaddr_in * adata =
+ reinterpret_cast<struct sockaddr_in *>(ifptr->ifa_addr);
+ ptr = reinterpret_cast<uint8_t *>(& adata->sin_addr);
+
+ IOAddress a = IOAddress::fromBytes(AF_INET, ptr);
+ iface_iter->second.addAddress(a);
+ }
+ }
+
+ freeifaddrs(iflist);
+
+ // Interfaces registering
+ for (ifaceLst::const_iterator iface_iter = ifaces.begin();
+ iface_iter != ifaces.end(); ++iface_iter) {
+ ifaces_.push_back(iface_iter->second);
+ }
}
-bool
-IfaceMgr::isDirectResponseSupported() {
- return (false);
+/// @brief sets flag_*_ fields
+///
+/// Like Linux version, os specific flags
+///
+/// @params flags
+void Iface::setFlags(uint64_t flags) {
+ flags_ = flags;
+
+ flag_loopback_ = flags & IFF_LOOPBACK;
+ flag_up_ = flags & IFF_UP;
+ flag_running_ = flags & IFF_RUNNING;
+ flag_multicast_ = flags & IFF_MULTICAST;
+ flag_broadcast_ = flags & IFF_BROADCAST;
}
void IfaceMgr::os_send4(struct msghdr& /*m*/,
@@ -44,7 +135,8 @@ void IfaceMgr::os_send4(struct msghdr& /*m*/,
size_t /*control_buf_len*/,
const Pkt4Ptr& /*pkt*/) {
// @todo: Are there any specific actions required before sending IPv4 packet
- // on BSDs? See iface_mgr_linux.cc for working Linux implementation.
+ // on Solaris based systems? See iface_mgr_linux.cc
+ // for working Linux implementation.
}
bool IfaceMgr::os_receive4(struct msghdr& /*m*/, Pkt4Ptr& /*pkt*/) {
@@ -54,6 +146,13 @@ bool IfaceMgr::os_receive4(struct msghdr& /*m*/, Pkt4Ptr& /*pkt*/) {
return (true); // pretend that we have everything set up for reception.
}
+void
+IfaceMgr::setMatchingPacketFilter(const bool /* direct_response_desired */) {
+ // @todo Currently we ignore the preference to use direct traffic
+ // because it hasn't been implemented for Solaris.
+ setPacketFilter(PktFilterPtr(new PktFilterInet()));
+}
+
} // end of isc::dhcp namespace
} // end of dhcp namespace
diff --git a/src/lib/dhcp/libdhcp++.cc b/src/lib/dhcp/libdhcp++.cc
index 697c33e..f6ba978 100644
--- a/src/lib/dhcp/libdhcp++.cc
+++ b/src/lib/dhcp/libdhcp++.cc
@@ -18,13 +18,16 @@
#include <dhcp/dhcp6.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option.h>
+#include <dhcp/option_vendor.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_int_array.h>
#include <dhcp/std_option_defs.h>
+#include <dhcp/docsis3_option_defs.h>
#include <exceptions/exceptions.h>
#include <util/buffer.h>
+#include <dhcp/option_definition.h>
#include <boost/shared_array.hpp>
#include <boost/shared_ptr.hpp>
@@ -45,17 +48,37 @@ OptionDefContainer LibDHCP::v4option_defs_;
// Static container with DHCPv6 option definitions.
OptionDefContainer LibDHCP::v6option_defs_;
+VendorOptionDefContainers LibDHCP::vendor4_defs_;
+
+VendorOptionDefContainers LibDHCP::vendor6_defs_;
+
+// Those two vendor classes are used for cable modems:
+
+/// DOCSIS3.0 compatible cable modem
+const char* isc::dhcp::DOCSIS3_CLASS_MODEM = "docsis3.0";
+
+/// DOCSIS3.0 cable modem that has router built-in
+const char* isc::dhcp::DOCSIS3_CLASS_EROUTER = "eRouter1.0";
+
+// Let's keep it in .cc file. Moving it to .h would require including optionDefParams
+// definitions there
+void initOptionSpace(OptionDefContainer& defs,
+ const OptionDefParams* params,
+ size_t params_size);
+
const OptionDefContainer&
LibDHCP::getOptionDefs(const Option::Universe u) {
switch (u) {
case Option::V4:
if (v4option_defs_.empty()) {
initStdOptionDefs4();
+ initVendorOptsDocsis4();
}
return (v4option_defs_);
case Option::V6:
if (v6option_defs_.empty()) {
initStdOptionDefs6();
+ initVendorOptsDocsis6();
}
return (v6option_defs_);
default:
@@ -63,6 +86,38 @@ LibDHCP::getOptionDefs(const Option::Universe u) {
}
}
+const OptionDefContainer*
+LibDHCP::getVendorOption4Defs(const uint32_t vendor_id) {
+
+ if (vendor_id == VENDOR_ID_CABLE_LABS &&
+ vendor4_defs_.find(VENDOR_ID_CABLE_LABS) == vendor4_defs_.end()) {
+ initVendorOptsDocsis4();
+ }
+
+ VendorOptionDefContainers::const_iterator def = vendor4_defs_.find(vendor_id);
+ if (def == vendor4_defs_.end()) {
+ // No such vendor-id space
+ return (NULL);
+ }
+ return (&(def->second));
+}
+
+const OptionDefContainer*
+LibDHCP::getVendorOption6Defs(const uint32_t vendor_id) {
+
+ if (vendor_id == VENDOR_ID_CABLE_LABS &&
+ vendor6_defs_.find(VENDOR_ID_CABLE_LABS) == vendor6_defs_.end()) {
+ initVendorOptsDocsis6();
+ }
+
+ VendorOptionDefContainers::const_iterator def = vendor6_defs_.find(vendor_id);
+ if (def == vendor6_defs_.end()) {
+ // No such vendor-id space
+ return (NULL);
+ }
+ return (&(def->second));
+}
+
OptionDefinitionPtr
LibDHCP::getOptionDef(const Option::Universe u, const uint16_t code) {
const OptionDefContainer& defs = getOptionDefs(u);
@@ -74,6 +129,31 @@ LibDHCP::getOptionDef(const Option::Universe u, const uint16_t code) {
return (OptionDefinitionPtr());
}
+OptionDefinitionPtr
+LibDHCP::getVendorOptionDef(const Option::Universe u, const uint32_t vendor_id,
+ const uint16_t code) {
+ const OptionDefContainer* defs = NULL;
+ if (u == Option::V4) {
+ defs = getVendorOption4Defs(vendor_id);
+ } else if (u == Option::V6) {
+ defs = getVendorOption6Defs(vendor_id);
+ }
+
+ if (!defs) {
+ // Weird universe or unknown vendor_id. We don't care. No definitions
+ // one way or another
+ // What is it anyway?
+ return (OptionDefinitionPtr());
+ }
+
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ const OptionDefContainerTypeRange& range = idx.equal_range(code);
+ if (range.first != range.second) {
+ return (*range.first);
+ }
+ return (OptionDefinitionPtr());
+}
+
bool
LibDHCP::isStandardOption(const Option::Universe u, const uint16_t code) {
if (u == Option::V6) {
@@ -91,9 +171,9 @@ LibDHCP::isStandardOption(const Option::Universe u, const uint16_t code) {
code == 126 ||
code == 127 ||
(code > 146 && code < 150) ||
- (code > 177 && code < 208) ||
+ (code > 177 && code < 208) ||
(code > 213 && code < 220) ||
- (code > 221 && code < 224))) {
+ (code > 221 && code < 255))) {
return (true);
}
@@ -128,14 +208,22 @@ LibDHCP::optionFactory(Option::Universe u,
size_t LibDHCP::unpackOptions6(const OptionBuffer& buf,
- isc::dhcp::Option::OptionCollection& options,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
size_t* relay_msg_offset /* = 0 */,
size_t* relay_msg_len /* = 0 */) {
size_t offset = 0;
size_t length = buf.size();
- // Get the list of stdandard option definitions.
- const OptionDefContainer& option_defs = LibDHCP::getOptionDefs(Option::V6);
+ // Get the list of standard option definitions.
+ OptionDefContainer option_defs;
+ if (option_space == "dhcp6") {
+ option_defs = LibDHCP::getOptionDefs(Option::V6);
+ }
+ // @todo Once we implement other option spaces we should add else clause
+ // here and gather option definitions for them. For now leaving option_defs
+ // empty will imply creation of generic Option.
+
// Get the search index #1. It allows to search for option definitions
// using option code.
const OptionDefContainerTypeIndex& idx = option_defs.get<1>();
@@ -164,6 +252,23 @@ size_t LibDHCP::unpackOptions6(const OptionBuffer& buf,
continue;
}
+ if (opt_type == D6O_VENDOR_OPTS) {
+ if (offset + 4 > length) {
+ // Truncated vendor-option. There is expected at least 4 bytes
+ // long enterprise-id field
+ return (offset);
+ }
+
+ // Parse this as vendor option
+ OptionPtr vendor_opt(new OptionVendor(Option::V6, buf.begin() + offset,
+ buf.begin() + offset + opt_len));
+ options.insert(std::make_pair(opt_type, vendor_opt));
+
+ offset += opt_len;
+ continue;
+ }
+
+
// Get all definitions with the particular option code. Note that option
// code is non-unique within this container however at this point we
// expect to get one option definition with the particular code. If more
@@ -206,11 +311,19 @@ size_t LibDHCP::unpackOptions6(const OptionBuffer& buf,
}
size_t LibDHCP::unpackOptions4(const OptionBuffer& buf,
- isc::dhcp::Option::OptionCollection& options) {
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options) {
size_t offset = 0;
// Get the list of stdandard option definitions.
- const OptionDefContainer& option_defs = LibDHCP::getOptionDefs(Option::V4);
+ OptionDefContainer option_defs;
+ if (option_space == "dhcp4") {
+ option_defs = LibDHCP::getOptionDefs(Option::V4);
+ }
+ // @todo Once we implement other option spaces we should add else clause
+ // here and gather option definitions for them. For now leaving option_defs
+ // empty will imply creation of generic Option.
+
// Get the search index #1. It allows to search for option definitions
// using option code.
const OptionDefContainerTypeIndex& idx = option_defs.get<1>();
@@ -280,10 +393,195 @@ size_t LibDHCP::unpackOptions4(const OptionBuffer& buf,
return (offset);
}
+size_t LibDHCP::unpackVendorOptions6(const uint32_t vendor_id,
+ const OptionBuffer& buf,
+ isc::dhcp::OptionCollection& options) {
+ size_t offset = 0;
+ size_t length = buf.size();
+
+ // Get the list of option definitions for this particular vendor-id
+ const OptionDefContainer* option_defs = LibDHCP::getVendorOption6Defs(vendor_id);
+
+ // Get the search index #1. It allows to search for option definitions
+ // using option code. If there's no such vendor-id space, we're out of luck
+ // anyway.
+ const OptionDefContainerTypeIndex* idx = NULL;
+ if (option_defs) {
+ idx = &(option_defs->get<1>());
+ }
+
+ // The buffer being read comprises a set of options, each starting with
+ // a two-byte type code and a two-byte length field.
+ while (offset + 4 <= length) {
+ uint16_t opt_type = isc::util::readUint16(&buf[offset]);
+ offset += 2;
+
+ uint16_t opt_len = isc::util::readUint16(&buf[offset]);
+ offset += 2;
+
+ if (offset + opt_len > length) {
+ // @todo: consider throwing exception here.
+ return (offset);
+ }
+
+ OptionPtr opt;
+ opt.reset();
+
+ // If there is a definition for such a vendor option...
+ if (idx) {
+ // Get all definitions with the particular option code. Note that option
+ // code is non-unique within this container however at this point we
+ // expect to get one option definition with the particular code. If more
+ // are returned we report an error.
+ const OptionDefContainerTypeRange& range = idx->equal_range(opt_type);
+ // Get the number of returned option definitions for the option code.
+ size_t num_defs = distance(range.first, range.second);
+
+ if (num_defs > 1) {
+ // Multiple options of the same code are not supported right now!
+ isc_throw(isc::Unexpected, "Internal error: multiple option definitions"
+ " for option type " << opt_type << " returned. Currently it is not"
+ " supported to initialize multiple option definitions"
+ " for the same option code. This will be supported once"
+ " support for option spaces is implemented");
+ } else if (num_defs == 1) {
+ // The option definition has been found. Use it to create
+ // the option instance from the provided buffer chunk.
+ const OptionDefinitionPtr& def = *(range.first);
+ assert(def);
+ opt = def->optionFactory(Option::V6, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len);
+ }
+ }
+
+ // This can happen in one of 2 cases:
+ // 1. we do not have definitions for that vendor-space
+ // 2. we do have definitions, but that particular option was not defined
+ if (!opt) {
+ opt = OptionPtr(new Option(Option::V6, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len));
+ }
+
+ // add option to options
+ if (opt) {
+ options.insert(std::make_pair(opt_type, opt));
+ }
+ offset += opt_len;
+ }
+
+ return (offset);
+}
+
+size_t LibDHCP::unpackVendorOptions4(const uint32_t vendor_id, const OptionBuffer& buf,
+ isc::dhcp::OptionCollection& options) {
+ size_t offset = 0;
+
+ // Get the list of stdandard option definitions.
+ const OptionDefContainer* option_defs = LibDHCP::getVendorOption4Defs(vendor_id);
+ // Get the search index #1. It allows to search for option definitions
+ // using option code.
+ const OptionDefContainerTypeIndex* idx = NULL;
+ if (option_defs) {
+ idx = &(option_defs->get<1>());
+ }
+
+ // The buffer being read comprises a set of options, each starting with
+ // a one-byte type code and a one-byte length field.
+ while (offset + 1 <= buf.size()) {
+
+ // Note that Vendor-Specific info option (RFC3925) has a different option
+ // format than Vendor-Spec info for DHCPv6. (there's additional layer of
+ // data-length
+ uint8_t data_len = buf[offset++];
+
+ if (offset + data_len > buf.size()) {
+ // Truncated data-option
+ return (offset);
+ }
+
+ uint8_t offset_end = offset + data_len;
+
+ // beginning of data-chunk parser
+ while (offset + 1 <= offset_end) {
+ uint8_t opt_type = buf[offset++];
+
+ // DHO_END is a special, one octet long option
+ if (opt_type == DHO_END)
+ return (offset); // just return. Don't need to add DHO_END option
+
+ // DHO_PAD is just a padding after DHO_END. Let's continue parsing
+ // in case we receive a message without DHO_END.
+ if (opt_type == DHO_PAD)
+ continue;
+
+ if (offset + 1 >= buf.size()) {
+ // opt_type must be cast to integer so as it is not treated as
+ // unsigned char value (a number is presented in error message).
+ isc_throw(OutOfRange, "Attempt to parse truncated option "
+ << static_cast<int>(opt_type));
+ }
+
+ uint8_t opt_len = buf[offset++];
+ if (offset + opt_len > buf.size()) {
+ isc_throw(OutOfRange, "Option parse failed. Tried to parse "
+ << offset + opt_len << " bytes from " << buf.size()
+ << "-byte long buffer.");
+ }
+
+ OptionPtr opt;
+ opt.reset();
+
+ if (idx) {
+ // Get all definitions with the particular option code. Note that option code
+ // is non-unique within this container however at this point we expect
+ // to get one option definition with the particular code. If more are
+ // returned we report an error.
+ const OptionDefContainerTypeRange& range = idx->equal_range(opt_type);
+ // Get the number of returned option definitions for the option code.
+ size_t num_defs = distance(range.first, range.second);
+
+ if (num_defs > 1) {
+ // Multiple options of the same code are not supported right now!
+ isc_throw(isc::Unexpected, "Internal error: multiple option definitions"
+ " for option type " << static_cast<int>(opt_type)
+ << " returned. Currently it is not supported to initialize"
+ << " multiple option definitions for the same option code."
+ << " This will be supported once support for option spaces"
+ << " is implemented");
+ } else if (num_defs == 1) {
+ // The option definition has been found. Use it to create
+ // the option instance from the provided buffer chunk.
+ const OptionDefinitionPtr& def = *(range.first);
+ assert(def);
+ opt = def->optionFactory(Option::V4, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len);
+ }
+ }
+
+ if (!opt) {
+ opt = OptionPtr(new Option(Option::V4, opt_type,
+ buf.begin() + offset,
+ buf.begin() + offset + opt_len));
+ }
+
+ options.insert(std::make_pair(opt_type, opt));
+ offset += opt_len;
+
+ } // end of data-chunk
+
+ }
+ return (offset);
+}
+
+
+
void
LibDHCP::packOptions(isc::util::OutputBuffer& buf,
- const Option::OptionCollection& options) {
- for (Option::OptionCollection::const_iterator it = options.begin();
+ const OptionCollection& options) {
+ for (OptionCollection::const_iterator it = options.begin();
it != options.end(); ++it) {
it->second->pack(buf);
}
@@ -330,68 +628,35 @@ void LibDHCP::OptionFactoryRegister(Option::Universe u,
void
LibDHCP::initStdOptionDefs4() {
- v4option_defs_.clear();
-
- // Now let's add all option definitions.
- for (int i = 0; i < OPTION_DEF_PARAMS_SIZE4; ++i) {
- std::string encapsulates(OPTION_DEF_PARAMS4[i].encapsulates);
- if (!encapsulates.empty() && OPTION_DEF_PARAMS4[i].array) {
- isc_throw(isc::BadValue, "invalid standard option definition: "
- << "option with code '" << OPTION_DEF_PARAMS4[i].code
- << "' may not encapsulate option space '"
- << encapsulates << "' because the definition"
- << " indicates that this option comprises an array"
- << " of values");
- }
-
- // Depending whether the option encapsulates an option space or not
- // we pick different constructor to create an instance of the option
- // definition.
- OptionDefinitionPtr definition;
- if (encapsulates.empty()) {
- // Option does not encapsulate any option space.
- definition.reset(new OptionDefinition(OPTION_DEF_PARAMS4[i].name,
- OPTION_DEF_PARAMS4[i].code,
- OPTION_DEF_PARAMS4[i].type,
- OPTION_DEF_PARAMS4[i].array));
-
- } else {
- // Option does encapsulate an option space.
- definition.reset(new OptionDefinition(OPTION_DEF_PARAMS4[i].name,
- OPTION_DEF_PARAMS4[i].code,
- OPTION_DEF_PARAMS4[i].type,
- OPTION_DEF_PARAMS4[i].encapsulates));
-
- }
+ initOptionSpace(v4option_defs_, OPTION_DEF_PARAMS4, OPTION_DEF_PARAMS_SIZE4);
+}
- for (int rec = 0; rec < OPTION_DEF_PARAMS4[i].records_size; ++rec) {
- definition->addRecordField(OPTION_DEF_PARAMS4[i].records[rec]);
- }
+void
+LibDHCP::initStdOptionDefs6() {
+ initOptionSpace(v6option_defs_, OPTION_DEF_PARAMS6, OPTION_DEF_PARAMS_SIZE6);
+}
- // Sanity check if the option is valid.
- try {
- definition->validate();
- } catch (const Exception& ex) {
- // This is unlikely event that validation fails and may
- // be only caused by programming error. To guarantee the
- // data consistency we clear all option definitions that
- // have been added so far and pass the exception forward.
- v4option_defs_.clear();
- throw;
- }
- v4option_defs_.push_back(definition);
- }
+void
+LibDHCP::initVendorOptsDocsis4() {
+ initOptionSpace(vendor4_defs_[VENDOR_ID_CABLE_LABS], DOCSIS3_V4_DEFS, DOCSIS3_V4_DEFS_SIZE);
}
void
-LibDHCP::initStdOptionDefs6() {
- v6option_defs_.clear();
+LibDHCP::initVendorOptsDocsis6() {
+ vendor6_defs_[VENDOR_ID_CABLE_LABS] = OptionDefContainer();
+ initOptionSpace(vendor6_defs_[VENDOR_ID_CABLE_LABS], DOCSIS3_V6_DEFS, DOCSIS3_V6_DEFS_SIZE);
+}
+
+void initOptionSpace(OptionDefContainer& defs,
+ const OptionDefParams* params,
+ size_t params_size) {
+ defs.clear();
- for (int i = 0; i < OPTION_DEF_PARAMS_SIZE6; ++i) {
- std::string encapsulates(OPTION_DEF_PARAMS6[i].encapsulates);
- if (!encapsulates.empty() && OPTION_DEF_PARAMS6[i].array) {
+ for (int i = 0; i < params_size; ++i) {
+ std::string encapsulates(params[i].encapsulates);
+ if (!encapsulates.empty() && params[i].array) {
isc_throw(isc::BadValue, "invalid standard option definition: "
- << "option with code '" << OPTION_DEF_PARAMS6[i].code
+ << "option with code '" << params[i].code
<< "' may not encapsulate option space '"
<< encapsulates << "' because the definition"
<< " indicates that this option comprises an array"
@@ -404,33 +669,33 @@ LibDHCP::initStdOptionDefs6() {
OptionDefinitionPtr definition;
if (encapsulates.empty()) {
// Option does not encapsulate any option space.
- definition.reset(new OptionDefinition(OPTION_DEF_PARAMS6[i].name,
- OPTION_DEF_PARAMS6[i].code,
- OPTION_DEF_PARAMS6[i].type,
- OPTION_DEF_PARAMS6[i].array));
+ definition.reset(new OptionDefinition(params[i].name,
+ params[i].code,
+ params[i].type,
+ params[i].array));
} else {
// Option does encapsulate an option space.
- definition.reset(new OptionDefinition(OPTION_DEF_PARAMS6[i].name,
- OPTION_DEF_PARAMS6[i].code,
- OPTION_DEF_PARAMS6[i].type,
- OPTION_DEF_PARAMS6[i].encapsulates));
+ definition.reset(new OptionDefinition(params[i].name,
+ params[i].code,
+ params[i].type,
+ params[i].encapsulates));
}
- for (int rec = 0; rec < OPTION_DEF_PARAMS6[i].records_size; ++rec) {
- definition->addRecordField(OPTION_DEF_PARAMS6[i].records[rec]);
+ for (int rec = 0; rec < params[i].records_size; ++rec) {
+ definition->addRecordField(params[i].records[rec]);
}
try {
definition->validate();
- } catch (const Exception& ex) {
+ } catch (const isc::Exception& ex) {
// This is unlikely event that validation fails and may
// be only caused by programming error. To guarantee the
// data consistency we clear all option definitions that
// have been added so far and pass the exception forward.
- v6option_defs_.clear();
+ defs.clear();
throw;
}
- v6option_defs_.push_back(definition);
+ defs.push_back(definition);
}
}
diff --git a/src/lib/dhcp/libdhcp++.dox b/src/lib/dhcp/libdhcp++.dox
index 194175a..052c07c 100644
--- a/src/lib/dhcp/libdhcp++.dox
+++ b/src/lib/dhcp/libdhcp++.dox
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -19,8 +19,9 @@
libdhcp++ is an all-purpose DHCP-manipulation library, written in
C++. It offers packet parsing and assembly, DHCPv4 and DHCPv6
-options parsing and ssembly, interface detection (currently on
-Linux systems only) and socket operations. It is a generic purpose library that
+options parsing and assembly, interface detection (currently on
+Linux, FreeBSD, NetBSD, OpenBSD, Max OS X, and Solaris 11) and socket operations.
+It is a generic purpose library that
can be used by server, client, relay, performance tools and other DHCP-related
tools. For server specific library, see \ref libdhcpsrv. Please do not
add any server-specific code to libdhcp++ and use \ref libdhcpsrv instead.
@@ -57,6 +58,53 @@ DHCPv6, but is rarely used in DHCPv4. isc::dhcp::Option::addOption(),
isc::dhcp::Option::delOption(), isc::dhcp::Option::getOption() can be used
for that purpose.
+ at section libdhcpRelay Relay v6 support in Pkt6
+
+DHCPv6 clients that are not connected to the same link as DHCPv6
+servers need relays to reach the server. Each relay receives a message
+on a client facing interface, encapsulates it into RELAY_MSG option
+and sends as RELAY_FORW message towards the server (or the next relay,
+which is closer to the server). This procedure can be repeated up to
+32 times. Kea is able to support up to 32 relays. Each traversed relay
+may add certain options. The most obvious example is interface-id
+option, but there may be other options as well. Each relay may add such
+an option, regardless of whether other relays added it before. Thanks
+to encapsulation, those options are separated and it is possible to
+differentiate which relay inserted specific instance of an option.
+
+Interface-id is used to identify a subnet (or interface) the original message
+came from and is used for that purpose on two occasions. First, the server
+uses the interface-id included by the first relay (the one closest to
+the client) to select appropriate subnet for a given request. Server includes
+that interface-id in its copy, when sending data back to the client.
+This will be used by the relay to choose proper interface when forwarding
+response towards the client.
+
+Pkt6 class has a public Pkt6::relay_info_ field, which is of type Pkt6::RelayInfo.
+This is a simple structure that represents the information in each RELAY_FORW
+or RELAY_REPL message. It is important to understand the order in which
+the data appear here. Consider the following network:
+
+\verbatim
+client-------relay1-----relay2-----relay3----server
+\endverbatim
+
+Client will transmit SOLICIT message. Relay1 will forward it as
+RELAY_FORW with SOLICIT in it. Relay2 forward it as RELAY_FORW with
+RELAY_FORW with SOLICIT in it. Finally the third relay will add yet
+another RELAY_FORW around it. The server will parse the packet and
+create Pkt6 object for it. Its relay_info_ will have 3
+elements. Packet parsing is done in reverse order, compare to the
+order the packet traversed in the network. The first element
+(relay_info_[0]) will represent relay3 information (the "last" relay or
+in other words the one closest to the server). The second element
+will represent relay2. The third element (relay_info_[2]) will represent
+the first relay (relay1) or in other words the one closest to the client.
+
+Packets sent by the server must maintain the same encapsulation order.
+This is easy to do - just copy data from client's message object into
+server's response object. See Pkt6::coyRelayInfo for details.
+
@section libdhcpIfaceMgr Interface Manager
Interface Manager (or IfaceMgr) is an abstraction layer about low-level
@@ -90,4 +138,126 @@ Another useful methods are dedicated to transmission
Note that receive4() and receive6() methods may return NULL, e.g.
when timeout is reached or if dhcp daemon receives a signal.
+ at section libdhcpPktFilter Switchable Packet Filter objects used by Interface Manager
+
+The well known problem of DHCPv4 implementation is that it must be able to
+provision devices which don't have an IPv4 address yet (the IPv4 address is
+one of the configuration parameters provided by DHCP server to a client).
+One way to communicate with such a device is to send server's response to
+a broadcast address. An obvious drawback of this approach is that the server's
+response will be received and processed by all clients in the particular
+network. Therefore, the preferred approach is that the server unicasts its
+response to a new address being assigned for the client. This client will
+identify itself as a target of this message by checking chaddr and/or
+Client Identifier value. At the same time, the other clients in the network
+will not receive the unicast message. The major problem that arises with this
+approach is that the client without an IP address doesn't respond to ARP
+messages. As a result, server's response will not be sent over IP/UDP
+socket because the system kernel will fail to resolve client's link-layer
+address.
+
+Kea supports the use of raw sockets to create a complete Data-link/IP/UDP/DHCPv4
+stack. By creating each layer of the outgoing packet, the Kea logic has full
+control over the frame contents and it may bypass the use of ARP to inject the
+link layer address into the frame. The raw socket is bound to a specific interface,
+not to the IP address/UDP port. Therefore, the system kernel doesn't have
+means to verify that Kea is listening to the DHCP traffic on the specific address
+and port. This has two major implications:
+- It is possible to run another DHCPv4 sever instance which will bind socket to the
+same address and port.
+- An attempt to send a unicast message to the DHCPv4 server will result in ICMP
+"Port Unreachable" message being sent by the kernel (which is unaware that the
+DHCPv4 service is actually running).
+In order to overcome these issues, the isc::dhcp::PktFilterLPF opens a
+regular IP/UDP socket which coexists with the raw socket. The socket is referred
+to as "fallback socket" in the Kea code. All packets received through this socket
+are discarded.
+
+In general, the use of datagram sockets is preferred over raw sockets.
+For convenience, the switchable Packet Filter objects are used to manage
+sockets for different purposes. These objects implement the socket opening
+operation and sending/receiving messages over this socket. For example:
+the isc::dhcp::PktFilterLPF object opens a raw socket.
+The isc::dhcp::PktFilterLPF::send and isc::dhcp::PktFilterLPF::receive
+methods encode/decode full data-link/IP/UDP/DHCPv4 stack. The
+isc::dhcp::PktFilterInet supports sending and receiving messages over
+the regular IP/UDP socket. The isc::dhcp::PktFilterInet should be used in all
+cases when an application using the libdhcp++ doesn't require sending
+DHCP messages to a device which doesn't have an address yet.
+
+ at section libdhcpPktFilter6 Switchable Packet Filters for DHCPv6
+
+The DHCPv6 implementation doesn't suffer from the problems described in \ref
+libdhcpPktFilter. Therefore, the socket creation and methods used to send
+and receive DHCPv6 messages are common for all OSes. However, there is
+still a need to customize the operations on the sockets to reliably unit test
+the \ref isc::dhcp::IfaceMgr logic.
+
+The \ref isc::dhcp::IfaceMgr::openSockets6 function examines configuration
+of detected interfaces for their availability to listen DHCPv6 traffic. For
+all running interfaces (except local loopback) it will try to open a socket
+and bind it to the link local or global unicast address. The socket will
+not be opened on the interface which is down or for which it was explicitly
+specified that it should not be used to listen to DHCPv6 messages. There is
+a substantial amount of logic in this function that has to be unit tested for
+various interface configurations, e.g.:
+- multiple interfaces with link-local addresses only
+- multiple interfaces, some of them having global unicast addresses,
+- multiple interfaces, some of them disabled
+- no interfaces
+
+The \ref isc::dhcp::IfaceMgr::openSockets6 function attempts to open
+sockets on detected interfaces. At the same time, the number of interfaces,
+and their configuration is specific to OS where the tests are being run.
+So the test doesn't have any means to configure interfaces for the test case
+being run. Moreover, a unit test should not change the configuration of the
+system. For example, a change to the configuration of the interface which
+is used to access the machine running a test, may effectively break the
+access to this machine.
+
+In order to overcome the problem described above, the unit tests use
+fake interfaces which can be freely added, configured and removed from the
+\ref isc::dhcp::IfaceMgr. Obviously, it is not possible to open a socket
+on a fake interface, nor use it to send or receive IP packets. To mimic
+socket operations on fake interfaces it is required that the functions
+which open sockets, send messages and receive messages have to be
+customizable. This is achieved by implementation of replaceable packet
+filter objects which derive from the \ref isc::dhcp::PktFilter6 class.
+The default implementation of this class is \ref isc::dhcp::PktFilterInet6
+which creates a regular datagram IPv6/UDPv6 socket. The unit tests use a
+stub implementation isc::dhcp::test::PktFilter6Stub which contains no-op
+functions.
+
+Use \ref isc::dhcp::IfaceMgr::setPacketFilter function to set the custom packet
+filter object to be used by Interface Manager.
+
+ at section libdhcpErrorLogging Logging non-fatal errors in IfaceMgr
+
+The libdhcp++ is a common library, meant to be used by various components,
+such as DHCP servers, relays and clients. It is also used by a perfdhcp
+benchmarking application. It provides a basic capabilities for these
+applications to perform operations on DHCP messages such as encoding
+or decoding them. It also provides capabilities to perform low level
+operations on sockets. Since libdhcp++ is a common library, its dependency
+on other BINDX modules should be minimal. In particular, errors occurring
+in the libdhcp++ are reported using exceptions, not a BINDX logger. This
+works well in most cases, but there are some cases in which it is
+undesired for a function to throw an exception in case of non-fatal error.
+
+The typical case, when exception should not be thrown, is when the \ref
+isc::dhcp::IfaceMgr::openSockets4 or \ref isc::dhcp::IfaceMgr::openSockets6
+fails to open a socket on one of the interfaces. This should not preclude
+the function from attempting to open sockets on other interfaces, which
+would be the case if exception was thrown.
+
+In such cases the IfaceMgr makes use of error handler callback function
+which may be installed by a caller. This function must implement the
+isc::dhcp::IfaceMgrErrorMsgCallback. Note that it is allowed to pass a NULL
+value instead, which would result falling back to a default behavior and
+exception will be thrown. If non-NULL value is provided, the
+\ref isc::dhcp::IfaceMgr will call error handler function and pass an
+error string as an argument. The handler function may use its logging
+mechanism to log this error message. In particular, the DHCP server
+will use BINDX logger to log the error message.
+
*/
diff --git a/src/lib/dhcp/libdhcp++.h b/src/lib/dhcp/libdhcp++.h
index 9d8bcab..230ba94 100644
--- a/src/lib/dhcp/libdhcp++.h
+++ b/src/lib/dhcp/libdhcp++.h
@@ -55,6 +55,17 @@ public:
static OptionDefinitionPtr getOptionDef(const Option::Universe u,
const uint16_t code);
+ /// @brief Returns vendor option definition for a given vendor-id and code
+ ///
+ /// @param u universe (V4 or V6)
+ /// @param vendor_id enterprise-id for a given vendor
+ /// @param code option code
+ /// @return reference to an option definition being requested
+ /// or NULL pointer if option definition has not been found.
+ static OptionDefinitionPtr getVendorOptionDef(const Option::Universe u,
+ const uint32_t vendor_id,
+ const uint16_t code);
+
/// @brief Check if the specified option is a standard option.
///
/// @param u universe (V4 or V6)
@@ -100,7 +111,7 @@ public:
/// @param buf output buffer (assembled options will be stored here)
/// @param options collection of options to store to
static void packOptions(isc::util::OutputBuffer& buf,
- const isc::dhcp::Option::OptionCollection& options);
+ const isc::dhcp::OptionCollection& options);
/// @brief Parses provided buffer as DHCPv4 options and creates Option objects.
///
@@ -108,10 +119,13 @@ public:
/// in options container.
///
/// @param buf Buffer to be parsed.
+ /// @param option_space A name of the option space which holds definitions
+ /// of to be used to parse options in the packets.
/// @param options Reference to option container. Options will be
/// put here.
static size_t unpackOptions4(const OptionBuffer& buf,
- isc::dhcp::Option::OptionCollection& options);
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options);
/// @brief Parses provided buffer as DHCPv6 options and creates Option objects.
///
@@ -125,6 +139,8 @@ public:
/// iteration its content will be treated as buffer to be parsed.
///
/// @param buf Buffer to be parsed.
+ /// @param option_space A name of the option space which holds definitions
+ /// of to be used to parse options in the packets.
/// @param options Reference to option container. Options will be
/// put here.
/// @param relay_msg_offset reference to a size_t structure. If specified,
@@ -133,7 +149,8 @@ public:
/// length of the relay_msg option will be stored in it.
/// @return offset to the first byte after last parsed option
static size_t unpackOptions6(const OptionBuffer& buf,
- isc::dhcp::Option::OptionCollection& options,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
size_t* relay_msg_offset = 0,
size_t* relay_msg_len = 0);
@@ -149,6 +166,49 @@ public:
uint16_t type,
Option::Factory * factory);
+ /// @brief Returns v4 option definitions for a given vendor
+ ///
+ /// @param vendor_id enterprise-id of a given vendor
+ /// @return a container for a given vendor (or NULL if not option
+ /// definitions are defined)
+ static const OptionDefContainer*
+ getVendorOption4Defs(const uint32_t vendor_id);
+
+ /// @brief Returns v6 option definitions for a given vendor
+ ///
+ /// @param vendor_id enterprise-id of a given vendor
+ /// @return a container for a given vendor (or NULL if not option
+ /// definitions are defined)
+ static const OptionDefContainer*
+ getVendorOption6Defs(const uint32_t vendor_id);
+
+ /// @brief Parses provided buffer as DHCPv6 vendor options and creates
+ /// Option objects.
+ ///
+ /// Parses provided buffer and stores created Option objects
+ /// in options container.
+ ///
+ /// @param vendor_id enterprise-id of the vendor
+ /// @param buf Buffer to be parsed.
+ /// @param options Reference to option container. Options will be
+ /// put here.
+ static size_t unpackVendorOptions6(const uint32_t vendor_id,
+ const OptionBuffer& buf,
+ isc::dhcp::OptionCollection& options);
+
+ /// @brief Parses provided buffer as DHCPv4 vendor options and creates
+ /// Option objects.
+ ///
+ /// Parses provided buffer and stores created Option objects
+ /// in options container.
+ ///
+ /// @param vendor_id enterprise-id of the vendor
+ /// @param buf Buffer to be parsed.
+ /// @param options Reference to option container. Options will be
+ /// put here.
+ static size_t unpackVendorOptions4(const uint32_t vendor_id, const OptionBuffer& buf,
+ isc::dhcp::OptionCollection& options);
+
private:
/// Initialize standard DHCPv4 option definitions.
@@ -170,6 +230,10 @@ private:
/// is incorrect. This is a programming error.
static void initStdOptionDefs6();
+ static void initVendorOptsDocsis4();
+
+ static void initVendorOptsDocsis6();
+
/// pointers to factories that produce DHCPv6 options
static FactoryMap v4factories_;
@@ -181,6 +245,12 @@ private:
/// Container with DHCPv6 option definitions.
static OptionDefContainer v6option_defs_;
+
+ /// Container for v4 vendor option definitions
+ static VendorOptionDefContainers vendor4_defs_;
+
+ /// Container for v6 vendor option definitions
+ static VendorOptionDefContainers vendor6_defs_;
};
}
diff --git a/src/lib/dhcp/option.cc b/src/lib/dhcp/option.cc
index e06b163..f5ab75e 100644
--- a/src/lib/dhcp/option.cc
+++ b/src/lib/dhcp/option.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -55,7 +55,7 @@ Option::Option(Universe u, uint16_t type, const OptionBuffer& data)
Option::Option(Universe u, uint16_t type, OptionBufferConstIter first,
OptionBufferConstIter last)
- :universe_(u), type_(type), data_(OptionBuffer(first,last)) {
+ :universe_(u), type_(type), data_(first, last) {
check();
}
@@ -121,17 +121,24 @@ Option::packOptions(isc::util::OutputBuffer& buf) {
void Option::unpack(OptionBufferConstIter begin,
OptionBufferConstIter end) {
- data_ = OptionBuffer(begin, end);
+ setData(begin, end);
}
void
Option::unpackOptions(const OptionBuffer& buf) {
+ // If custom option parsing function has been set, use this function
+ // to parse options. Otherwise, use standard function from libdhcp++.
+ if (!callback_.empty()) {
+ callback_(buf, getEncapsulatedSpace(), options_, 0, 0);
+ return;
+ }
+
switch (universe_) {
case V4:
- LibDHCP::unpackOptions4(buf, options_);
+ LibDHCP::unpackOptions4(buf, getEncapsulatedSpace(), options_);
return;
case V6:
- LibDHCP::unpackOptions6(buf, options_);
+ LibDHCP::unpackOptions6(buf, getEncapsulatedSpace(), options_);
return;
default:
isc_throw(isc::BadValue, "Invalid universe type " << universe_);
@@ -146,7 +153,7 @@ uint16_t Option::len() {
int length = getHeaderLen() + data_.size();
// ... and sum of lengths of all suboptions
- for (Option::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
@@ -169,7 +176,7 @@ Option::valid() {
}
OptionPtr Option::getOption(uint16_t opt_type) {
- isc::dhcp::Option::OptionCollection::const_iterator x =
+ isc::dhcp::OptionCollection::const_iterator x =
options_.find(opt_type);
if ( x != options_.end() ) {
return (*x).second;
@@ -178,7 +185,7 @@ OptionPtr Option::getOption(uint16_t opt_type) {
}
bool Option::delOption(uint16_t opt_type) {
- isc::dhcp::Option::OptionCollection::iterator x = options_.find(opt_type);
+ isc::dhcp::OptionCollection::iterator x = options_.find(opt_type);
if ( x != options_.end() ) {
options_.erase(x);
return true; // delete successful
@@ -274,13 +281,6 @@ void Option::setUint32(uint32_t value) {
writeUint32(value, &data_[0]);
}
-void Option::setData(const OptionBufferConstIter first,
- const OptionBufferConstIter last) {
- // We will copy entire option buffer, so we have to resize data_.
- data_.resize(std::distance(first, last));
- std::copy(first, last, data_.begin());
-}
-
bool Option::equal(const OptionPtr& other) const {
return ( (getType() == other->getType()) &&
(getData() == other->getData()) );
diff --git a/src/lib/dhcp/option.h b/src/lib/dhcp/option.h
index 28e65a9..dbdb3af 100644
--- a/src/lib/dhcp/option.h
+++ b/src/lib/dhcp/option.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -17,6 +17,7 @@
#include <util/buffer.h>
+#include <boost/function.hpp>
#include <boost/shared_ptr.hpp>
#include <map>
@@ -44,6 +45,34 @@ typedef boost::shared_ptr<OptionBuffer> OptionBufferPtr;
class Option;
typedef boost::shared_ptr<Option> OptionPtr;
+/// A collection of DHCP (v4 or v6) options
+typedef std::multimap<unsigned int, OptionPtr> OptionCollection;
+
+/// @brief This type describes a callback function to parse options from buffer.
+///
+/// @note The last two parameters should be specified in the callback function
+/// parameters list only if DHCPv6 options are parsed. Exclude these parameters
+/// from the callback function defined to parse DHCPv4 options.
+///
+/// @param buffer A buffer holding options to be parsed.
+/// @param encapsulated_space A name of the option space to which options being
+/// parsed belong.
+/// @param [out] options A container to which parsed options should be appended.
+/// @param relay_msg_offset A pointer to a size_t value. It indicates the
+/// offset to beginning of relay_msg option. This parameter should be specified
+/// for DHCPv6 options only.
+/// @param relay_msg_len A pointer to a size_t value. It holds the length of
+/// of the relay_msg option. This parameter should be specified for DHCPv6
+/// options only.
+///
+/// @return An offset to the first byte after last parsed option.
+typedef boost::function< size_t(const OptionBuffer& buffer,
+ const std::string encapsulated_space,
+ OptionCollection& options,
+ size_t* relay_msg_offset,
+ size_t* relay_msg_len)
+ > UnpackOptionsCallback;
+
class Option {
public:
@@ -56,8 +85,6 @@ public:
/// defines option universe DHCPv4 or DHCPv6
enum Universe { V4, V6 };
- /// a collection of DHCPv6 options
- typedef std::multimap<unsigned int, OptionPtr> OptionCollection;
/// @brief a factory function prototype
///
@@ -282,8 +309,36 @@ public:
///
/// @param first iterator pointing to beginning of buffer to copy.
/// @param last iterator pointing to end of buffer to copy.
- void setData(const OptionBufferConstIter first,
- const OptionBufferConstIter last);
+ ///
+ /// @tparam InputIterator type of the iterator representing the
+ /// limits of the buffer to be assigned to a data_ buffer.
+ template<typename InputIterator>
+ void setData(InputIterator first, InputIterator last) {
+ data_.assign(first, last);
+ }
+
+ /// @brief Sets the name of the option space encapsulated by this option.
+ ///
+ /// @param encapsulated_space name of the option space encapsulated by
+ /// this option.
+ void setEncapsulatedSpace(const std::string& encapsulated_space) {
+ encapsulated_space_ = encapsulated_space;
+ }
+
+ /// @brief Returns the name of the option space encapsulated by this option.
+ ///
+ /// @return name of the option space encapsulated by this option.
+ std::string getEncapsulatedSpace() const {
+ return (encapsulated_space_);
+ }
+
+ /// @brief Set callback function to be used to parse options.
+ ///
+ /// @param callback An instance of the callback function or NULL to
+ /// uninstall callback.
+ void setCallback(UnpackOptionsCallback callback) {
+ callback_ = callback;
+ }
/// just to force that every option has virtual dtor
virtual ~Option();
@@ -367,6 +422,12 @@ protected:
/// collection for storing suboptions
OptionCollection options_;
+ /// Name of the option space being encapsulated by this option.
+ std::string encapsulated_space_;
+
+ /// A callback to be called to unpack options from the packet.
+ UnpackOptionsCallback callback_;
+
/// @todo probably 2 different containers have to be used for v4 (unique
/// options) and v6 (options with the same type can repeat)
};
diff --git a/src/lib/dhcp/option4_client_fqdn.cc b/src/lib/dhcp/option4_client_fqdn.cc
new file mode 100644
index 0000000..7f93a50
--- /dev/null
+++ b/src/lib/dhcp/option4_client_fqdn.cc
@@ -0,0 +1,525 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dns/labelsequence.h>
+#include <util/buffer.h>
+#include <util/io_utilities.h>
+#include <util/strutil.h>
+#include <sstream>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Implements the logic for the Option6ClientFqdn class.
+///
+/// The purpose of the class is to separate the implementation details
+/// of the Option4ClientFqdn class from the interface. This implementation
+/// uses libdns classes to process FQDNs. At some point it may be
+/// desired to split libdhcp++ from libdns. In such case the
+/// implementation of this class may be changed. The declaration of the
+/// Option6ClientFqdn class holds the pointer to implementation, so
+/// the transition to a different implementation would not affect the
+/// header file.
+class Option4ClientFqdnImpl {
+public:
+ /// Holds flags carried by the option.
+ uint8_t flags_;
+ /// Holds RCODE1 and RCODE2 values.
+ Option4ClientFqdn::Rcode rcode1_;
+ Option4ClientFqdn::Rcode rcode2_;
+ /// Holds the pointer to a domain name carried in the option.
+ boost::shared_ptr<isc::dns::Name> domain_name_;
+ /// Indicates whether domain name is partial or fully qualified.
+ Option4ClientFqdn::DomainNameType domain_name_type_;
+
+ /// @brief Constructor, from domain name.
+ ///
+ /// @param flags A value of the flags option field.
+ /// @param rcode An object representing the RCODE1 and RCODE2 values.
+ /// @param domain_name A domain name carried by the option given in the
+ /// textual format.
+ /// @param name_type A value which indicates whether domain-name is partial
+ /// or fully qualified.
+ Option4ClientFqdnImpl(const uint8_t flags,
+ const Option4ClientFqdn::Rcode& rcode,
+ const std::string& domain_name,
+ const Option4ClientFqdn::DomainNameType name_type);
+
+ /// @brief Constructor, from wire data.
+ ///
+ /// @param first An iterator pointing to the begining of the option data
+ /// in the wire format.
+ /// @param last An iterator poiting to the end of the option data in the
+ /// wire format.
+ Option4ClientFqdnImpl(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Copy constructor.
+ ///
+ /// @param source An object being copied.
+ Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source);
+
+ /// @brief Assignment operator.
+ ///
+ /// @param source An object which is being assigned.
+ Option4ClientFqdnImpl& operator=(const Option4ClientFqdnImpl& source);
+
+ /// @brief Set a new domain name for the option.
+ ///
+ /// @param domain_name A new domain name to be assigned.
+ /// @param name_type A value which indicates whether the domain-name is
+ /// partial or fully qualified.
+ void setDomainName(const std::string& domain_name,
+ const Option4ClientFqdn::DomainNameType name_type);
+
+ /// @brief Check if flags are valid.
+ ///
+ /// In particular, this function checks if the N and S bits are not
+ /// set to 1 in the same time.
+ ///
+ /// @param flags A value carried by the flags field of the option.
+ /// @param check_mbz A boolean value which indicates if this function should
+ /// check if the MBZ bits are set (if true). This parameter should be set
+ /// to false when validating flags in the received message. This is because
+ /// server should ignore MBZ bits in received messages.
+ /// @throw InvalidOption6FqdnFlags if flags are invalid.
+ static void checkFlags(const uint8_t flags, const bool check_mbz);
+
+ /// @brief Parse the Option provided in the wire format.
+ ///
+ /// @param first An iterator pointing to the begining of the option data
+ /// in the wire format.
+ /// @param last An iterator poiting to the end of the option data in the
+ /// wire format.
+ void parseWireData(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Parse domain-name encoded in the canonical format.
+ ///
+ void parseCanonicalDomainName(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Parse domain-name emcoded in the deprecated ASCII format.
+ ///
+ /// @param first An iterator pointing to the begining of the option data
+ /// where domain-name is stored.
+ /// @param last An iterator poiting to the end of the option data where
+ /// domain-name is stored.
+ void parseASCIIDomainName(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+};
+
+Option4ClientFqdnImpl::
+Option4ClientFqdnImpl(const uint8_t flags,
+ const Option4ClientFqdn::Rcode& rcode,
+ const std::string& domain_name,
+ // cppcheck 1.57 complains that const enum value is not passed
+ // by reference. Note that, it accepts the non-const enum value
+ // to be passed by value. In both cases it is unneccessary to
+ // pass the enum by reference.
+ // cppcheck-suppress passedByValue
+ const Option4ClientFqdn::DomainNameType name_type)
+ : flags_(flags),
+ rcode1_(rcode),
+ rcode2_(rcode),
+ domain_name_(),
+ domain_name_type_(name_type) {
+
+ // Check if flags are correct. Also, check that MBZ bits are not set. If
+ // they are, throw exception.
+ checkFlags(flags_, true);
+ // Set domain name. It may throw an exception if domain name has wrong
+ // format.
+ setDomainName(domain_name, name_type);
+}
+
+Option4ClientFqdnImpl::Option4ClientFqdnImpl(OptionBufferConstIter first,
+ OptionBufferConstIter last)
+ : rcode1_(Option4ClientFqdn::RCODE_CLIENT()),
+ rcode2_(Option4ClientFqdn::RCODE_CLIENT()) {
+ parseWireData(first, last);
+ // Verify that flags value was correct. This constructor is used to parse
+ // incoming packet, so don't check MBZ bits. They are ignored because we
+ // don't want to discard the whole option because MBZ bits are set.
+ checkFlags(flags_, false);
+}
+
+Option4ClientFqdnImpl::
+Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source)
+ : flags_(source.flags_),
+ rcode1_(source.rcode1_),
+ rcode2_(source.rcode2_),
+ domain_name_(),
+ domain_name_type_(source.domain_name_type_) {
+ if (source.domain_name_) {
+ domain_name_.reset(new isc::dns::Name(*source.domain_name_));
+ }
+}
+
+Option4ClientFqdnImpl&
+// This assignment operator handles assignment to self, it copies all
+// required values.
+// cppcheck-suppress operatorEqToSelf
+Option4ClientFqdnImpl::operator=(const Option4ClientFqdnImpl& source) {
+ if (source.domain_name_) {
+ domain_name_.reset(new isc::dns::Name(*source.domain_name_));
+
+ } else {
+ domain_name_.reset();
+ }
+
+ // Assignment is exception safe.
+ flags_ = source.flags_;
+ rcode1_ = source.rcode1_;
+ rcode2_ = source.rcode2_;
+ domain_name_type_ = source.domain_name_type_;
+
+ return (*this);
+}
+
+void
+Option4ClientFqdnImpl::
+setDomainName(const std::string& domain_name,
+ // cppcheck 1.57 complains that const enum value is not passed
+ // by reference. Note that, it accepts the non-const enum
+ // to be passed by value. In both cases it is unneccessary to
+ // pass the enum by reference.
+ // cppcheck-suppress passedByValue
+ const Option4ClientFqdn::DomainNameType name_type) {
+ // domain-name must be trimmed. Otherwise, string comprising spaces only
+ // would be treated as a fully qualified name.
+ std::string name = isc::util::str::trim(domain_name);
+ if (name.empty()) {
+ if (name_type == Option4ClientFqdn::FULL) {
+ isc_throw(InvalidOption4FqdnDomainName,
+ "fully qualified domain-name must not be empty"
+ << " when setting new domain-name for DHCPv4 Client"
+ << " FQDN Option");
+ }
+ // The special case when domain-name is empty is marked by setting the
+ // pointer to the domain-name object to NULL.
+ domain_name_.reset();
+
+ } else {
+ try {
+ domain_name_.reset(new isc::dns::Name(name));
+ domain_name_type_ = name_type;
+
+ } catch (const Exception& ex) {
+ isc_throw(InvalidOption4FqdnDomainName,
+ "invalid domain-name value '"
+ << domain_name << "' when setting new domain-name for"
+ << " DHCPv4 Client FQDN Option");
+
+ }
+ }
+}
+
+void
+Option4ClientFqdnImpl::checkFlags(const uint8_t flags, const bool check_mbz) {
+ // The Must Be Zero (MBZ) bits must not be set.
+ if (check_mbz && ((flags & ~Option4ClientFqdn::FLAG_MASK) != 0)) {
+ isc_throw(InvalidOption4FqdnFlags,
+ "invalid DHCPv4 Client FQDN Option flags: 0x"
+ << std::hex << static_cast<int>(flags) << std::dec);
+ }
+
+ // According to RFC 4702, section 2.1. if the N bit is 1, the S bit
+ // MUST be 0. Checking it here.
+ if ((flags & (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S))
+ == (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S)) {
+ isc_throw(InvalidOption4FqdnFlags,
+ "both N and S flag of the DHCPv4 Client FQDN Option are set."
+ << " According to RFC 4702, if the N bit is 1 the S bit"
+ << " MUST be 0");
+ }
+}
+
+void
+Option4ClientFqdnImpl::parseWireData(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+
+ // Buffer must comprise at least one byte with the flags.
+ // The domain-name may be empty.
+ if (std::distance(first, last) < Option4ClientFqdn::FIXED_FIELDS_LEN) {
+ isc_throw(OutOfRange, "DHCPv4 Client FQDN Option ("
+ << DHO_FQDN << ") is truncated");
+ }
+
+ // Parse flags
+ flags_ = *(first++);
+
+ // Parse RCODE1 and RCODE2.
+ rcode1_ = Option4ClientFqdn::Rcode(*(first++));
+ rcode2_ = Option4ClientFqdn::Rcode(*(first++));
+
+ try {
+ if ((flags_ & Option4ClientFqdn::FLAG_E) != 0) {
+ parseCanonicalDomainName(first, last);
+
+ } else {
+ parseASCIIDomainName(first, last);
+
+ }
+ } catch (const Exception& ex) {
+ isc_throw(InvalidOption4FqdnDomainName,
+ "failed to parse the domain-name in DHCPv4 Client FQDN"
+ << " Option: " << ex.what());
+ }
+}
+
+void
+Option4ClientFqdnImpl::parseCanonicalDomainName(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+ // Parse domain-name if any.
+ if (std::distance(first, last) > 0) {
+ // The FQDN may comprise a partial domain-name. In this case it lacks
+ // terminating 0. If this is the case, we will need to add zero at
+ // the end because Name object constructor requires it.
+ if (*(last - 1) != 0) {
+ // Create temporary buffer and add terminating zero.
+ OptionBuffer buf(first, last);
+ buf.push_back(0);
+ // Reset domain name.
+ isc::util::InputBuffer name_buf(&buf[0], buf.size());
+ domain_name_.reset(new isc::dns::Name(name_buf));
+ // Terminating zero was missing, so set the domain-name type
+ // to partial.
+ domain_name_type_ = Option4ClientFqdn::PARTIAL;
+ } else {
+ // We are dealing with fully qualified domain name so there is
+ // no need to add terminating zero. Simply pass the buffer to
+ // Name object constructor.
+ isc::util::InputBuffer name_buf(&(*first),
+ std::distance(first, last));
+ domain_name_.reset(new isc::dns::Name(name_buf));
+ // Set the domain-type to fully qualified domain name.
+ domain_name_type_ = Option4ClientFqdn::FULL;
+ }
+ }
+}
+
+void
+Option4ClientFqdnImpl::parseASCIIDomainName(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+ if (std::distance(first, last) > 0) {
+ std::string domain_name(first, last);
+ domain_name_.reset(new isc::dns::Name(domain_name));
+ domain_name_type_ = domain_name[domain_name.length() - 1] == '.' ?
+ Option4ClientFqdn::FULL : Option4ClientFqdn::PARTIAL;
+ }
+}
+
+Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag, const Rcode& rcode)
+ : Option(Option::V4, DHO_FQDN),
+ impl_(new Option4ClientFqdnImpl(flag, rcode, "", PARTIAL)) {
+}
+
+Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag,
+ const Rcode& rcode,
+ const std::string& domain_name,
+ const DomainNameType domain_name_type)
+ : Option(Option::V4, DHO_FQDN),
+ impl_(new Option4ClientFqdnImpl(flag, rcode, domain_name,
+ domain_name_type)) {
+}
+
+Option4ClientFqdn::Option4ClientFqdn(OptionBufferConstIter first,
+ OptionBufferConstIter last)
+ : Option(Option::V4, DHO_FQDN, first, last),
+ impl_(new Option4ClientFqdnImpl(first, last)) {
+}
+
+Option4ClientFqdn::~Option4ClientFqdn() {
+ delete(impl_);
+}
+
+Option4ClientFqdn::Option4ClientFqdn(const Option4ClientFqdn& source)
+ : Option(source),
+ impl_(new Option4ClientFqdnImpl(*source.impl_)) {
+}
+
+Option4ClientFqdn&
+// This assignment operator handles assignment to self, it uses copy
+// constructor of Option4ClientFqdnImpl to copy all required values.
+// cppcheck-suppress operatorEqToSelf
+Option4ClientFqdn::operator=(const Option4ClientFqdn& source) {
+ Option4ClientFqdnImpl* old_impl = impl_;
+ impl_ = new Option4ClientFqdnImpl(*source.impl_);
+ delete(old_impl);
+ return (*this);
+}
+
+bool
+Option4ClientFqdn::getFlag(const uint8_t flag) const {
+ // Caller should query for one of the: E, N, S or O flags. Any other value
+ /// is invalid and results in the exception.
+ if (flag != FLAG_S && flag != FLAG_O && flag != FLAG_N && flag != FLAG_E) {
+ isc_throw(InvalidOption4FqdnFlags, "invalid DHCPv4 Client FQDN"
+ << " Option flag specified, expected E, N, S or O");
+ }
+
+ return ((impl_->flags_ & flag) != 0);
+}
+
+void
+Option4ClientFqdn::setFlag(const uint8_t flag, const bool set_flag) {
+ // Check that flag is in range between 0x1 and 0x7. Although it is
+ // discouraged this check doesn't preclude the caller from setting
+ // multiple flags concurrently.
+ if (((flag & ~FLAG_MASK) != 0) || (flag == 0)) {
+ isc_throw(InvalidOption4FqdnFlags, "invalid DHCPv4 Client FQDN"
+ << " Option flag " << std::hex
+ << static_cast<int>(flag) << std::dec
+ << "is being set. Expected combination of E, N, S and O");
+ }
+
+ // Copy the current flags into local variable. That way we will be able
+ // to test new flags settings before applying them.
+ uint8_t new_flag = impl_->flags_;
+ if (set_flag) {
+ new_flag |= flag;
+ } else {
+ new_flag &= ~flag;
+ }
+
+ // Check new flags. If they are valid, apply them. Also, check that MBZ
+ // bits are not set.
+ Option4ClientFqdnImpl::checkFlags(new_flag, true);
+ impl_->flags_ = new_flag;
+}
+
+void
+Option4ClientFqdn::setRcode(const Rcode& rcode) {
+ impl_->rcode1_ = rcode;
+ impl_->rcode2_ = rcode;
+}
+
+void
+Option4ClientFqdn::resetFlags() {
+ impl_->flags_ = 0;
+}
+
+std::string
+Option4ClientFqdn::getDomainName() const {
+ if (impl_->domain_name_) {
+ return (impl_->domain_name_->toText(impl_->domain_name_type_ ==
+ PARTIAL));
+ }
+ // If an object holding domain-name is NULL it means that the domain-name
+ // is empty.
+ return ("");
+}
+
+void
+Option4ClientFqdn::packDomainName(isc::util::OutputBuffer& buf) const {
+ // If domain-name is empty, do nothing.
+ if (!impl_->domain_name_) {
+ return;
+ }
+
+ if (getFlag(FLAG_E)) {
+ // Domain name, encoded as a set of labels.
+ isc::dns::LabelSequence labels(*impl_->domain_name_);
+ if (labels.getDataLength() > 0) {
+ size_t read_len = 0;
+ const uint8_t* data = labels.getData(&read_len);
+ if (impl_->domain_name_type_ == PARTIAL) {
+ --read_len;
+ }
+ buf.writeData(data, read_len);
+ }
+
+ } else {
+ std::string domain_name = impl_->domain_name_->toText();
+ buf.writeData(&domain_name[0], domain_name.size());
+
+ }
+}
+
+void
+Option4ClientFqdn::setDomainName(const std::string& domain_name,
+ const DomainNameType domain_name_type) {
+ impl_->setDomainName(domain_name, domain_name_type);
+}
+
+void
+Option4ClientFqdn::resetDomainName() {
+ setDomainName("", PARTIAL);
+}
+
+Option4ClientFqdn::DomainNameType
+Option4ClientFqdn::getDomainNameType() const {
+ return (impl_->domain_name_type_);
+}
+
+void
+Option4ClientFqdn::pack(isc::util::OutputBuffer& buf) {
+ // Header = option code and length.
+ packHeader(buf);
+ // Flags field.
+ buf.writeUint8(impl_->flags_);
+ // RCODE1 and RCODE2
+ buf.writeUint8(impl_->rcode1_.getCode());
+ buf.writeUint8(impl_->rcode2_.getCode());
+ // Domain name.
+ packDomainName(buf);
+}
+
+void
+Option4ClientFqdn::unpack(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+ setData(first, last);
+ impl_->parseWireData(first, last);
+ // Check that the flags in the received option are valid. Ignore MBZ bits,
+ // because we don't want to discard the whole option because of MBZ bits
+ // being set.
+ impl_->checkFlags(impl_->flags_, false);
+}
+
+std::string
+Option4ClientFqdn::toText(int indent) {
+ std::ostringstream stream;
+ std::string in(indent, ' '); // base indentation
+ stream << in << "type=" << type_ << " (CLIENT_FQDN), "
+ << "flags: ("
+ << "N=" << (getFlag(FLAG_N) ? "1" : "0") << ", "
+ << "E=" << (getFlag(FLAG_E) ? "1" : "0") << ", "
+ << "O=" << (getFlag(FLAG_O) ? "1" : "0") << ", "
+ << "S=" << (getFlag(FLAG_S) ? "1" : "0") << "), "
+ << "domain-name='" << getDomainName() << "' ("
+ << (getDomainNameType() == PARTIAL ? "partial" : "full")
+ << ")";
+
+ return (stream.str());
+}
+
+uint16_t
+Option4ClientFqdn::len() {
+ uint16_t domain_name_length = 0;
+ // If domain name is partial, the NULL terminating character
+ // is not included and the option length have to be adjusted.
+ if (impl_->domain_name_) {
+ domain_name_length = impl_->domain_name_type_ == FULL ?
+ impl_->domain_name_->getLength() : impl_->domain_name_->getLength() - 1;
+ }
+
+ return (getHeaderLen() + FIXED_FIELDS_LEN + domain_name_length);
+}
+
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/option4_client_fqdn.h b/src/lib/dhcp/option4_client_fqdn.h
new file mode 100644
index 0000000..fc0fb66
--- /dev/null
+++ b/src/lib/dhcp/option4_client_fqdn.h
@@ -0,0 +1,370 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION4_CLIENT_FQDN_H
+#define OPTION4_CLIENT_FQDN_H
+
+#include <dhcp/option.h>
+#include <dns/name.h>
+
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Exception thrown when invalid flags have been specified for
+/// DHCPv4 Client FQDN %Option.
+class InvalidOption4FqdnFlags : public Exception {
+public:
+ InvalidOption4FqdnFlags(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Exception thrown when invalid domain name is specified.
+class InvalidOption4FqdnDomainName : public Exception {
+public:
+ InvalidOption4FqdnDomainName(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// Forward declaration to implementation of @c Option4ClientFqdn class.
+class Option4ClientFqdnImpl;
+
+/// @brief Represents DHCPv4 Client FQDN %Option (code 81).
+///
+/// This option has been defined in the RFC 4702 and it has a following
+/// structure:
+/// - Code (1 octet) - option code (always equal to 81).
+/// - Len (1 octet) - a length of the option.
+/// - Flags (1 octet) - a field carrying "NEOS" flags described below.
+/// - RCODE1 (1 octet) - deprecated field which should be set to 0 by the client
+/// and set to 255 by the server.
+/// - RCODE2 (1 octet) - deprecated, should be used in the same way as RCODE1.
+/// - Domain Name - variable length field comprising partial or fully qualified
+/// domain name.
+///
+/// The flags field has the following structure:
+/// @code
+/// 0 1 2 3 4 5 6 7
+/// +-+-+-+-+-+-+-+-+
+/// | MBZ |N|E|O|S|
+/// +-+-+-+-+-+-+-+-+
+/// @endcode
+/// where:
+/// - N flag specifies whether server should (0) or should not (1) perform DNS
+/// Update,
+/// - E flag specifies encoding of the Domain Name field. If this flag is set
+/// to 1 it indicates canonical wire format without compression. 0 indicates
+/// the deprecated ASCII format.
+/// - O flag is set by the server to indicate that it has overridden client's
+/// preference set with the S bit.
+/// - S flag specifies whether server should (1) or should not (0) perform
+/// forward (FQDN-to-address) updates.
+///
+/// This class exposes a set of functions to modify flags and check their
+/// correctness.
+///
+/// Domain names being carried by DHCPv4 Client Fqdn %Option can be fully
+/// qualified or partial. Partial domain names are encoded similar to the
+/// fully qualified domain names, except that they lack terminating zero
+/// at the end of their wire representation (or lack of dot at the end, in
+/// case of ASCII encoding). It is also accepted to create an instance of
+/// this option which has empty domain-name. Clients use empty domain-names
+/// to indicate that server should generate complete fully qualified
+/// domain-name.
+///
+/// @warning: The RFC4702 section 2.3.1 states that the clients and servers
+/// should use character sets specified in RFC952, section 2.1 for ASCII-encoded
+/// domain-names. This class doesn't detect the character set violation for
+/// ASCII-encoded domain-name. It could be implemented in the future but it is
+/// not important now for two reasons:
+/// - ASCII encoding is deprecated
+/// - clients SHOULD obey restrictions but if they don't, server may still
+/// process the option
+///
+/// RFC 4702 mandates that the DHCP client sets RCODE1 and RCODE2 to 0 and that
+/// server sets them to 255. This class allows to set the value for these
+/// fields and both fields are always set to the same value. There is no way
+/// to set them separately (e.g. set different value for RCODE1 and RCODE2).
+/// However, there are no use cases which would require it.
+///
+/// <b>Design choice:</b> This class uses pimpl idiom to separate the interface
+/// from implementation specifics. Implementations may use different approaches
+/// to handle domain names (mostly validation of the domain-names). The existing
+/// @c isc::dns::Name class is a natural (and the simplest) choice to handle
+/// domain-names. Use of this class however, implies that libdhcp must be linked
+/// with libdns. At some point these libraries may need to be separated, i.e. to
+/// support compilation and use of standalone DHCP server. This will require
+/// that the part of implementation which deals with domain-names is modified to
+/// not use classes from libdns. These changes will be transparent for this
+/// interface.
+class Option4ClientFqdn : public Option {
+public:
+
+ ///
+ /// @name A set of constants used to identify and set bits in the flags field
+ //@{
+ static const uint8_t FLAG_S = 0x01; ///< Bit S
+ static const uint8_t FLAG_O = 0x02; ///< Bit O
+ static const uint8_t FLAG_E = 0x04; ///< Bit E
+ static const uint8_t FLAG_N = 0x08; ///< Bit N
+ //@}
+
+ /// @brief Mask which zeroes MBZ flag bits.
+ static const uint8_t FLAG_MASK = 0xF;
+
+ /// @brief Represents the value of one of the RCODE1 or RCODE2 fields.
+ ///
+ /// Typically, RCODE values are set to 255 by the server and to 0 by the
+ /// clients (as per RFC 4702).
+ class Rcode {
+ public:
+ Rcode(const uint8_t rcode)
+ : rcode_(rcode) { }
+
+ /// @brief Returns the value of the RCODE.
+ ///
+ /// Returned value can be directly used to create the on-wire format
+ /// of the DHCPv4 Client FQDN %Option.
+ uint8_t getCode() const {
+ return (rcode_);
+ }
+
+ private:
+ uint8_t rcode_;
+ };
+
+
+ /// @brief Type of the domain-name: partial or full.
+ enum DomainNameType {
+ PARTIAL,
+ FULL
+ };
+
+ /// @brief The size in bytes of the fixed fields within DHCPv4 Client Fqdn
+ /// %Option.
+ ///
+ /// The fixed fields are:
+ /// - Flags
+ /// - RCODE1
+ /// - RCODE2
+ static const uint16_t FIXED_FIELDS_LEN = 3;
+
+ /// @brief Constructor, creates option instance using flags and domain name.
+ ///
+ /// This constructor is used to create an instance of the option which will
+ /// be included in outgoing messages.
+ ///
+ /// Note that the RCODE values are encapsulated by the Rcode object (not a
+ /// simple uint8_t value). This helps to prevent a caller from confusing the
+ /// flags value with rcode value (both are uint8_t values). For example:
+ /// if caller swaps the two, it will be detected in the compilation time.
+ /// Also, this API encourages the caller to use two predefined functions:
+ /// @c RCODE_SERVER and @c RCODE_CLIENT to set the value of RCODE. These
+ /// functions generate objects which represent the only valid values to be
+ /// be passed to the constructor (255 and 0 respectively). Other
+ /// values should not be used. However, it is still possible that the other
+ /// entity (client or server) sends the option with invalid value. Although,
+ /// the RCODE values are ignored, there should be a way to represent such
+ /// invalid RCODE value. The Rcode class is capable of representing it.
+ ///
+ /// @param flags a combination of flags to be stored in flags field.
+ /// @param rcode @c Rcode object representing a value for RCODE1 and RCODE2
+ /// fields of the option. Both fields are assigned the same value
+ /// encapsulated by the parameter.
+ /// @param domain_name a name to be stored in the domain-name field.
+ /// @param domain_name_type indicates if the domain name is partial
+ /// or full.
+ /// @throw InvalidOption4FqdnFlags if value of the flags field is wrong.
+ /// @throw InvalidOption4FqdnDomainName if the domain-name is invalid.
+ explicit Option4ClientFqdn(const uint8_t flags,
+ const Rcode& rcode,
+ const std::string& domain_name,
+ const DomainNameType domain_name_type = FULL);
+
+ /// @brief Constructor, creates option instance with empty domain name.
+ ///
+ /// This constructor creates an instance of the option with empty
+ /// domain-name. This domain-name is marked partial.
+ ///
+ /// @param flags a combination of flags to be stored in flags field.
+ /// @param rcode @c Rcode object representing a value for RCODE1 and RCODE2
+ /// fields. Both fields are assigned the same value encapsulated by this
+ /// parameter.
+ /// @throw InvalidOption4FqdnFlags if value of the flags field is invalid.
+ Option4ClientFqdn(const uint8_t flags, const Rcode& rcode);
+
+ /// @brief Constructor, creates an option instance from part of the buffer.
+ ///
+ /// This constructor is mainly used to parse options in the received
+ /// messages. Function parameters specify buffer bounds from which the
+ /// option should be created. The size of the buffer chunk, specified by
+ /// the constructor's parameters should be equal or larger than the size
+ /// of the option. Otherwise, constructor will throw an exception.
+ ///
+ /// @param first the lower bound of the buffer to create option from.
+ /// @param last the upper bound of the buffer to create option from.
+ /// @throw InvalidOption4FqdnFlags if value of the flags field is invalid.
+ /// @throw InvalidOption4FqdnDomainName if the domain-name carried by the
+ /// option is invalid.
+ /// @throw OutOfRange if the option is truncated.
+ explicit Option4ClientFqdn(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Copy constructor
+ Option4ClientFqdn(const Option4ClientFqdn& source);
+
+ /// @brief Destructor
+ virtual ~Option4ClientFqdn();
+
+ /// @brief Assignment operator
+ Option4ClientFqdn& operator=(const Option4ClientFqdn& source);
+
+ /// @brief Checks if the specified flag of the DHCPv4 Client FQDN %Option
+ /// is set.
+ ///
+ /// @param flag A value specifying a bit within flags field to be checked.
+ /// It must be one of the following @c FLAG_S, @c FLAG_E, @c FLAG_O,
+ /// @c FLAG_N.
+ ///
+ /// @return true if the bit of the specified flags bit is set, false
+ /// otherwise.
+ /// @throw InvalidOption4ClientFlags if specified flag which value is to be
+ /// returned is invalid (is not one of the FLAG_S, FLAG_N, FLAG_O).
+ bool getFlag(const uint8_t flag) const;
+
+ /// @brief Modifies the value of the specified DHCPv4 Client Fqdn %Option
+ /// flag.
+ ///
+ /// @param flag A value specifying a bit within flags field to be set. It
+ /// must be one of the following @c FLAG_S, @c FLAG_E, @c FLAG_O, @c FLAG_N.
+ /// @param set a boolean value which indicates whether flag should be
+ /// set (true), or cleared (false).
+ /// @throw InvalidOption4ClientFlags if specified flag which value is to be
+ /// set is invalid (is not one of the FLAG_S, FLAG_N, FLAG_O).
+ void setFlag(const uint8_t flag, const bool set);
+
+ /// @brief Sets the flag field value to 0.
+ void resetFlags();
+
+ /// @brief Set Rcode value.
+ ///
+ /// @param rcode An @c Rcode object representing value of RCODE1 and RCODE2.
+ /// Both fields are assigned the same value.
+ void setRcode(const Rcode& rcode);
+
+ /// @brief Returns the domain-name in the text format.
+ ///
+ /// If domain-name is partial, it lacks the dot at the end (e.g. myhost).
+ /// If domain-name is fully qualified, it has the dot at the end (e.g.
+ /// myhost.example.com.).
+ ///
+ /// @return domain-name in the text format.
+ std::string getDomainName() const;
+
+ /// @brief Writes domain-name in the wire format into a buffer.
+ ///
+ /// The data being written are appended at the end of the buffer.
+ ///
+ /// @param [out] buf buffer where domain-name will be written.
+ void packDomainName(isc::util::OutputBuffer& buf) const;
+
+ /// @brief Set new domain-name.
+ ///
+ /// @param domain_name domain name field value in the text format.
+ /// @param domain_name_type type of the domain name: partial or fully
+ /// qualified.
+ /// @throw InvalidOption4FqdnDomainName if the specified domain-name is
+ /// invalid.
+ void setDomainName(const std::string& domain_name,
+ const DomainNameType domain_name_type);
+
+ /// @brief Set empty domain-name.
+ ///
+ /// This function is equivalent to @c Option6ClientFqdn::setDomainName
+ /// with empty partial domain-name. It is exception safe.
+ void resetDomainName();
+
+ /// @brief Returns enumerator value which indicates whether domain-name is
+ /// partial or full.
+ ///
+ /// @return An enumerator value indicating whether domain-name is partial
+ /// or full.
+ DomainNameType getDomainNameType() const;
+
+ /// @brief Writes option in the wire format into a buffer.
+ ///
+ /// @param [out] buf output buffer where option data will be stored.
+ virtual void pack(isc::util::OutputBuffer& buf);
+
+ /// @brief Parses option from the received buffer.
+ ///
+ /// Method creates an instance of the DHCPv4 Client FQDN %Option from the
+ /// wire format. Parameters specify the bounds of the buffer to read option
+ /// data from. The size of the buffer limited by the specified parameters
+ /// should be equal or larger than size of the option (including its
+ /// header). Otherwise exception will be thrown.
+ ///
+ /// @param first lower bound of the buffer to parse option from.
+ /// @param last upper bound of the buffer to parse option from.
+ virtual void unpack(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Returns string representation of the option.
+ ///
+ /// The string returned by the method comprises the bit value of each
+ /// option flag and the domain-name.
+ ///
+ /// @param indent number of spaces before printed text.
+ ///
+ /// @return string with text representation.
+ virtual std::string toText(int indent = 0);
+
+ /// @brief Returns length of the complete option (data length +
+ /// DHCPv4 option header).
+ ///
+ /// @return length of the option.
+ virtual uint16_t len();
+
+ ///
+ /// @name Well known Rcode declarations for DHCPv4 Client FQDN %Option
+ ///
+ //@{
+ /// @brief Rcode being set by the server.
+ inline static const Rcode& RCODE_SERVER() {
+ static Rcode rcode(255);
+ return (rcode);
+ }
+
+ /// @brief Rcode being set by the client.
+ inline static const Rcode& RCODE_CLIENT() {
+ static Rcode rcode(0);
+ return (rcode);
+ }
+ //@}
+
+private:
+
+ /// @brief A pointer to the implementation.
+ Option4ClientFqdnImpl* impl_;
+};
+
+/// A pointer to the @c Option4ClientFqdn object.
+typedef boost::shared_ptr<Option4ClientFqdn> Option4ClientFqdnPtr;
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // OPTION4_CLIENT_FQDN_H
diff --git a/src/lib/dhcp/option6_addrlst.cc b/src/lib/dhcp/option6_addrlst.cc
index cb14070..45c2262 100644
--- a/src/lib/dhcp/option6_addrlst.cc
+++ b/src/lib/dhcp/option6_addrlst.cc
@@ -102,9 +102,9 @@ std::string Option6AddrLst::toText(int indent /* =0 */) {
tmp << "type=" << type_ << " " << addrs_.size() << "addr(s): ";
- for (AddressContainer::const_iterator addr=addrs_.begin();
- addr!=addrs_.end(); ++addr) {
- tmp << addr->toText() << " ";
+ for (AddressContainer::const_iterator addr = addrs_.begin();
+ addr != addrs_.end(); ++addr) {
+ tmp << *addr << " ";
}
return tmp.str();
}
diff --git a/src/lib/dhcp/option6_client_fqdn.cc b/src/lib/dhcp/option6_client_fqdn.cc
new file mode 100644
index 0000000..761acae
--- /dev/null
+++ b/src/lib/dhcp/option6_client_fqdn.cc
@@ -0,0 +1,455 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/dhcp6.h>
+#include <dhcp/option6_client_fqdn.h>
+#include <dns/labelsequence.h>
+#include <util/buffer.h>
+#include <util/io_utilities.h>
+#include <util/strutil.h>
+#include <sstream>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Implements the logic for the Option6ClientFqdn class.
+///
+/// The purpose of the class is to separate the implementation details
+/// of the Option6ClientFqdn class from the interface. This implementation
+/// uses b10-libdns classes to process FQDNs. At some point it may be
+/// desired to split b10-libdhcp++ from b10-libdns. In such case the
+/// implementation of this class may be changed. The declaration of the
+/// Option6ClientFqdn class holds the pointer to implementation, so
+/// the transition to a different implementation would not affect the
+/// header file.
+class Option6ClientFqdnImpl {
+public:
+ /// Holds flags carried by the option.
+ uint8_t flags_;
+ /// Holds the pointer to a domain name carried in the option.
+ boost::shared_ptr<isc::dns::Name> domain_name_;
+ /// Indicates whether domain name is partial or fully qualified.
+ Option6ClientFqdn::DomainNameType domain_name_type_;
+
+ /// @brief Constructor, from domain name.
+ ///
+ /// @param flags A value of the flags option field.
+ /// @param domain_name A domain name carried by the option given in the
+ /// textual format.
+ /// @param name_type A value which indicates whether domain-name
+ /// is partial of fully qualified.
+ Option6ClientFqdnImpl(const uint8_t flags,
+ const std::string& domain_name,
+ const Option6ClientFqdn::DomainNameType name_type);
+
+ /// @brief Constructor, from wire data.
+ ///
+ /// @param first An iterator pointing to the begining of the option data
+ /// in the wire format.
+ /// @param last An iterator poiting to the end of the option data in the
+ /// wire format.
+ Option6ClientFqdnImpl(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Copy constructor.
+ ///
+ /// @param source An object being copied.
+ Option6ClientFqdnImpl(const Option6ClientFqdnImpl& source);
+
+ /// @brief Assignment operator.
+ ///
+ /// @param source An object which is being assigned.
+ Option6ClientFqdnImpl& operator=(const Option6ClientFqdnImpl& source);
+
+ /// @brief Set a new domain name for the option.
+ ///
+ /// @param domain_name A new domain name to be assigned.
+ /// @param name_type A value which indicates whether the domain-name is
+ /// partial or fully qualified.
+ void setDomainName(const std::string& domain_name,
+ const Option6ClientFqdn::DomainNameType name_type);
+
+ /// @brief Check if flags are valid.
+ ///
+ /// In particular, this function checks if the N and S bits are not
+ /// set to 1 in the same time.
+ ///
+ /// @param flags A value carried by the flags field of the option.
+ /// @param check_mbz A boolean value which indicates if this function should
+ /// check if the MBZ bits are set (if true). This parameter should be set
+ /// to false when validating flags in the received message. This is because
+ /// server should ignore MBZ bits in received messages.
+ /// @throw InvalidOption6FqdnFlags if flags are invalid.
+ static void checkFlags(const uint8_t flags, const bool check_mbz);
+
+ /// @brief Parse the Option provided in the wire format.
+ ///
+ /// @param first An iterator pointing to the begining of the option data
+ /// in the wire format.
+ /// @param last An iterator poiting to the end of the option data in the
+ /// wire format.
+ void parseWireData(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+};
+
+Option6ClientFqdnImpl::
+Option6ClientFqdnImpl(const uint8_t flags,
+ const std::string& domain_name,
+ // cppcheck 1.57 complains that const enum value is not
+ // passed by reference. Note that it accepts the non-const
+ // enum to be passed by value. In both cases it is
+ // unnecessary to pass the enum by reference.
+ // cppcheck-suppress passedByValue
+ const Option6ClientFqdn::DomainNameType name_type)
+ : flags_(flags),
+ domain_name_(),
+ domain_name_type_(name_type) {
+
+ // Check if flags are correct. Also check if MBZ bits are set.
+ checkFlags(flags_, true);
+ // Set domain name. It may throw an exception if domain name has wrong
+ // format.
+ setDomainName(domain_name, name_type);
+}
+
+Option6ClientFqdnImpl::Option6ClientFqdnImpl(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+ parseWireData(first, last);
+ // Verify that flags value was correct. Do not check if MBZ bits are
+ // set because we should ignore those bits in received message.
+ checkFlags(flags_, false);
+}
+
+Option6ClientFqdnImpl::
+Option6ClientFqdnImpl(const Option6ClientFqdnImpl& source)
+ : flags_(source.flags_),
+ domain_name_(),
+ domain_name_type_(source.domain_name_type_) {
+ if (source.domain_name_) {
+ domain_name_.reset(new isc::dns::Name(*source.domain_name_));
+ }
+}
+
+Option6ClientFqdnImpl&
+// This assignment operator handles assignment to self, it copies all
+// required values.
+// cppcheck-suppress operatorEqToSelf
+Option6ClientFqdnImpl::operator=(const Option6ClientFqdnImpl& source) {
+ if (source.domain_name_) {
+ domain_name_.reset(new isc::dns::Name(*source.domain_name_));
+
+ } else {
+ domain_name_.reset();
+
+ }
+
+ // This assignment should be exception safe.
+ flags_ = source.flags_;
+ domain_name_type_ = source.domain_name_type_;
+
+ return (*this);
+}
+
+void
+Option6ClientFqdnImpl::
+setDomainName(const std::string& domain_name,
+ // cppcheck 1.57 complains that const enum value is not
+ // passed by reference. Note that it accepts the non-const
+ // enum to be passed by value. In both cases it is
+ // unnecessary to pass the enum by reference.
+ // cppcheck-suppress passedByValue
+ const Option6ClientFqdn::DomainNameType name_type) {
+ // domain-name must be trimmed. Otherwise, string comprising spaces only
+ // would be treated as a fully qualified name.
+ std::string name = isc::util::str::trim(domain_name);
+ if (name.empty()) {
+ if (name_type == Option6ClientFqdn::FULL) {
+ isc_throw(InvalidOption6FqdnDomainName,
+ "fully qualified domain-name must not be empty"
+ << " when setting new domain-name for DHCPv6 Client"
+ << " FQDN Option");
+ }
+ // The special case when domain-name is empty is marked by setting the
+ // pointer to the domain-name object to NULL.
+ domain_name_.reset();
+
+ } else {
+ try {
+ domain_name_.reset(new isc::dns::Name(name, true));
+ domain_name_type_ = name_type;
+
+ } catch (const Exception& ex) {
+ isc_throw(InvalidOption6FqdnDomainName, "invalid domain-name value '"
+ << domain_name << "' when setting new domain-name for"
+ << " DHCPv6 Client FQDN Option");
+
+ }
+ }
+}
+
+void
+Option6ClientFqdnImpl::checkFlags(const uint8_t flags, const bool check_mbz) {
+ // The Must Be Zero (MBZ) bits must not be set.
+ if (check_mbz && ((flags & ~Option6ClientFqdn::FLAG_MASK) != 0)) {
+ isc_throw(InvalidOption6FqdnFlags,
+ "invalid DHCPv6 Client FQDN Option flags: 0x"
+ << std::hex << static_cast<int>(flags) << std::dec);
+ }
+
+ // According to RFC 4704, section 4.1. if the N bit is 1, the S bit
+ // MUST be 0. Checking it here.
+ if ((flags & (Option6ClientFqdn::FLAG_N | Option6ClientFqdn::FLAG_S))
+ == (Option6ClientFqdn::FLAG_N | Option6ClientFqdn::FLAG_S)) {
+ isc_throw(InvalidOption6FqdnFlags,
+ "both N and S flag of the DHCPv6 Client FQDN Option are set."
+ << " According to RFC 4704, if the N bit is 1 the S bit"
+ << " MUST be 0");
+ }
+}
+
+void
+Option6ClientFqdnImpl::parseWireData(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+
+ // Buffer must comprise at least one byte with the flags.
+ // The domain-name may be empty.
+ if (std::distance(first, last) < Option6ClientFqdn::FLAG_FIELD_LEN) {
+ isc_throw(OutOfRange, "DHCPv6 Client FQDN Option ("
+ << D6O_CLIENT_FQDN << ") is truncated. Minimal option"
+ << " size is " << Option6ClientFqdn::FLAG_FIELD_LEN
+ << ", got option with size " << std::distance(first, last));
+ }
+
+ // Parse flags
+ flags_ = *(first++);
+
+ // Parse domain-name if any.
+ if (std::distance(first, last) > 0) {
+ // The FQDN may comprise a partial domain-name. In this case it lacks
+ // terminating 0. If this is the case, we will need to add zero at
+ // the end because Name object constructor requires it.
+ if (*(last - 1) != 0) {
+ // Create temporary buffer and add terminating zero.
+ OptionBuffer buf(first, last);
+ buf.push_back(0);
+ // Reset domain name.
+ isc::util::InputBuffer name_buf(&buf[0], buf.size());
+ try {
+ domain_name_.reset(new isc::dns::Name(name_buf, true));
+ } catch (const Exception& ex) {
+ isc_throw(InvalidOption6FqdnDomainName, "failed to parse"
+ "partial domain-name from wire format");
+ }
+ // Terminating zero was missing, so set the domain-name type
+ // to partial.
+ domain_name_type_ = Option6ClientFqdn::PARTIAL;
+ } else {
+ // We are dealing with fully qualified domain name so there is
+ // no need to add terminating zero. Simply pass the buffer to
+ // Name object constructor.
+ isc::util::InputBuffer name_buf(&(*first),
+ std::distance(first, last));
+ try {
+ domain_name_.reset(new isc::dns::Name(name_buf, true));
+ } catch (const Exception& ex) {
+ isc_throw(InvalidOption6FqdnDomainName, "failed to parse"
+ "fully qualified domain-name from wire format");
+ }
+ // Set the domain-type to fully qualified domain name.
+ domain_name_type_ = Option6ClientFqdn::FULL;
+ }
+ }
+}
+
+Option6ClientFqdn::Option6ClientFqdn(const uint8_t flag)
+ : Option(Option::V6, D6O_CLIENT_FQDN),
+ impl_(new Option6ClientFqdnImpl(flag, "", PARTIAL)) {
+}
+
+Option6ClientFqdn::Option6ClientFqdn(const uint8_t flag,
+ const std::string& domain_name,
+ const DomainNameType domain_name_type)
+ : Option(Option::V6, D6O_CLIENT_FQDN),
+ impl_(new Option6ClientFqdnImpl(flag, domain_name, domain_name_type)) {
+}
+
+Option6ClientFqdn::Option6ClientFqdn(OptionBufferConstIter first,
+ OptionBufferConstIter last)
+ : Option(Option::V6, D6O_CLIENT_FQDN, first, last),
+ impl_(new Option6ClientFqdnImpl(first, last)) {
+}
+
+Option6ClientFqdn::~Option6ClientFqdn() {
+ delete(impl_);
+}
+
+Option6ClientFqdn::Option6ClientFqdn(const Option6ClientFqdn& source)
+ : Option(source),
+ impl_(new Option6ClientFqdnImpl(*source.impl_)) {
+}
+
+Option6ClientFqdn&
+// This assignment operator handles assignment to self, it uses copy
+// constructor of Option6ClientFqdnImpl to copy all required values.
+// cppcheck-suppress operatorEqToSelf
+Option6ClientFqdn::operator=(const Option6ClientFqdn& source) {
+ Option6ClientFqdnImpl* old_impl = impl_;
+ impl_ = new Option6ClientFqdnImpl(*source.impl_);
+ delete(old_impl);
+ return (*this);
+}
+
+bool
+Option6ClientFqdn::getFlag(const uint8_t flag) const {
+ // Caller should query for one of the: N, S or O flags. Any other
+ // value is invalid.
+ if (flag != FLAG_S && flag != FLAG_O && flag != FLAG_N) {
+ isc_throw(InvalidOption6FqdnFlags, "invalid DHCPv6 Client FQDN"
+ << " Option flag specified, expected N, S or O");
+ }
+
+ return ((impl_->flags_ & flag) != 0);
+}
+
+void
+Option6ClientFqdn::setFlag(const uint8_t flag, const bool set_flag) {
+ // Check that flag is in range between 0x1 and 0x7. Note that this
+ // allows to set or clear multiple flags concurrently. Setting
+ // concurrent bits is discouraged (see header file) but it is not
+ // checked here so it will work.
+ if (((flag & ~FLAG_MASK) != 0) || (flag == 0)) {
+ isc_throw(InvalidOption6FqdnFlags, "invalid DHCPv6 Client FQDN"
+ << " Option flag " << std::hex
+ << static_cast<int>(flag) << std::dec
+ << "is being set. Expected: N, S or O");
+ }
+
+ // Copy the current flags into local variable. That way we will be able
+ // to test new flags settings before applying them.
+ uint8_t new_flag = impl_->flags_;
+ if (set_flag) {
+ new_flag |= flag;
+ } else {
+ new_flag &= ~flag;
+ }
+
+ // Check new flags. If they are valid, apply them.
+ Option6ClientFqdnImpl::checkFlags(new_flag, true);
+ impl_->flags_ = new_flag;
+}
+
+void
+Option6ClientFqdn::resetFlags() {
+ impl_->flags_ = 0;
+}
+
+std::string
+Option6ClientFqdn::getDomainName() const {
+ if (impl_->domain_name_) {
+ return (impl_->domain_name_->toText(impl_->domain_name_type_ ==
+ PARTIAL));
+ }
+ // If an object holding domain-name is NULL it means that the domain-name
+ // is empty.
+ return ("");
+}
+
+void
+Option6ClientFqdn::packDomainName(isc::util::OutputBuffer& buf) const {
+ // There is nothing to do if domain-name is empty.
+ if (!impl_->domain_name_) {
+ return;
+ }
+
+ // Domain name, encoded as a set of labels.
+ isc::dns::LabelSequence labels(*impl_->domain_name_);
+ if (labels.getDataLength() > 0) {
+ size_t read_len = 0;
+ const uint8_t* data = labels.getData(&read_len);
+ if (impl_->domain_name_type_ == PARTIAL) {
+ --read_len;
+ }
+ buf.writeData(data, read_len);
+ }
+}
+
+void
+Option6ClientFqdn::setDomainName(const std::string& domain_name,
+ const DomainNameType domain_name_type) {
+ impl_->setDomainName(domain_name, domain_name_type);
+}
+
+void
+Option6ClientFqdn::resetDomainName() {
+ setDomainName("", PARTIAL);
+}
+
+Option6ClientFqdn::DomainNameType
+Option6ClientFqdn::getDomainNameType() const {
+ return (impl_->domain_name_type_);
+}
+
+void
+Option6ClientFqdn::pack(isc::util::OutputBuffer& buf) {
+ // Header = option code and length.
+ packHeader(buf);
+ // Flags field.
+ buf.writeUint8(impl_->flags_);
+ // Domain name.
+ packDomainName(buf);
+}
+
+void
+Option6ClientFqdn::unpack(OptionBufferConstIter first,
+ OptionBufferConstIter last) {
+ setData(first, last);
+ impl_->parseWireData(first, last);
+ // Check that the flags in the received option are valid. Ignore MBZ bits
+ // because we don't want to discard the whole option because of MBZ bits
+ // being set.
+ impl_->checkFlags(impl_->flags_, false);
+}
+
+std::string
+Option6ClientFqdn::toText(int indent) {
+ std::ostringstream stream;
+ std::string in(indent, ' '); // base indentation
+ stream << in << "type=" << type_ << "(CLIENT_FQDN)" << ", "
+ << "flags: ("
+ << "N=" << (getFlag(FLAG_N) ? "1" : "0") << ", "
+ << "O=" << (getFlag(FLAG_O) ? "1" : "0") << ", "
+ << "S=" << (getFlag(FLAG_S) ? "1" : "0") << "), "
+ << "domain-name='" << getDomainName() << "' ("
+ << (getDomainNameType() == PARTIAL ? "partial" : "full")
+ << ")";
+
+ return (stream.str());
+}
+
+uint16_t
+Option6ClientFqdn::len() {
+ uint16_t domain_name_length = 0;
+ if (impl_->domain_name_) {
+ // If domain name is partial, the NULL terminating character
+ // is not included and the option. Length has to be adjusted.
+ domain_name_length = impl_->domain_name_type_ == FULL ?
+ impl_->domain_name_->getLength() :
+ impl_->domain_name_->getLength() - 1;
+ }
+ return (getHeaderLen() + FLAG_FIELD_LEN + domain_name_length);
+}
+
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/option6_client_fqdn.h b/src/lib/dhcp/option6_client_fqdn.h
new file mode 100644
index 0000000..f67e7d5
--- /dev/null
+++ b/src/lib/dhcp/option6_client_fqdn.h
@@ -0,0 +1,271 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION6_CLIENT_FQDN_H
+#define OPTION6_CLIENT_FQDN_H
+
+#include <dhcp/option.h>
+#include <dns/name.h>
+
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Exception thrown when invalid flags have been specified for
+/// DHCPv6 Client Fqdn %Option.
+class InvalidOption6FqdnFlags : public Exception {
+public:
+ InvalidOption6FqdnFlags(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Exception thrown when invalid domain name is specified.
+class InvalidOption6FqdnDomainName : public Exception {
+public:
+ InvalidOption6FqdnDomainName(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// Forward declaration to implementation of @c Option6ClientFqdn class.
+class Option6ClientFqdnImpl;
+
+/// @brief Represents DHCPv6 Client FQDN %Option (code 39).
+///
+/// This option has been defined in the RFC 4704 and it has a following
+/// structure:
+/// - option-code = 39 (2 octets)
+/// - option-len (2 octets)
+/// - flags (1 octet)
+/// - domain-name - variable length field comprising partial or fully qualified
+/// domain name.
+///
+/// The flags field has the following structure:
+/// @code
+/// 0 1 2 3 4 5 6 7
+/// +-+-+-+-+-+-+-+-+
+/// | MBZ |N|O|S|
+/// +-+-+-+-+-+-+-+-+
+/// @endcode
+/// where:
+/// - N flag specifies whether server should (0) or should not (1) perform DNS
+/// Update,
+/// - O flag is set by the server to indicate that it has overridden client's
+/// preference set with the S bit.
+/// - S flag specifies whether server should (1) or should not (0) perform
+/// forward (FQDN-to-address) updates.
+///
+/// This class exposes a set of functions to modify flags and check their
+/// correctness.
+///
+/// Domain names being carried by DHCPv6 Client Fqdn %Option can be fully
+/// qualified or partial. Partial domain names are encoded similar to the
+/// fully qualified domain names, except that they lack terminating zero
+/// at the end of their wire representation. It is also accepted to create an
+/// instance of this option which has empty domain-name. Clients use empty
+/// domain-names to indicate that server should generate complete fully
+/// qualified domain-name.
+///
+/// <b>Design choice:</b> This class uses pimpl idiom to separate the interface
+/// from implementation specifics. Implementations may use different approaches
+/// to handle domain names (mostly validation of the domain-names). The existing
+/// @c isc::dns::Name class is a natural (and the simplest) choice to handle
+/// domain-names. Use of this class however, implies that libdhcp must be linked
+/// with libdns. At some point these libraries may need to be separated, i.e. to
+/// support compilation and use of standalone DHCP server. This will require
+/// that the part of implementation which deals with domain-names is modified to
+/// not use classes from libdns. These changes will be transparent for this
+/// interface.
+class Option6ClientFqdn : public Option {
+public:
+
+ ///
+ ///@name A set of constants setting respective bits in 'flags' field
+ //@{
+ static const uint8_t FLAG_S = 0x01; ///< S bit.
+ static const uint8_t FLAG_O = 0x02; ///< O bit.
+ static const uint8_t FLAG_N = 0x04; ///< N bit.
+ //@}
+
+ /// @brief Mask which zeroes MBZ flag bits.
+ static const uint8_t FLAG_MASK = 0x7;
+
+ /// @brief The length of the flag field within DHCPv6 Client Fqdn %Option.
+ static const uint16_t FLAG_FIELD_LEN = 1;
+
+ /// @brief Type of the domain-name: partial or full.
+ enum DomainNameType {
+ PARTIAL,
+ FULL
+ };
+
+ /// @brief Constructor, creates option instance using flags and domain name.
+ ///
+ /// This constructor is used to create instance of the option which will be
+ /// included in outgoing messages.
+ ///
+ /// @param flags a combination of flag bits to be stored in flags field.
+ /// @param domain_name a name to be stored in the domain-name field.
+ /// @param domain_name_type indicates if the domain name is partial
+ /// or full.
+ explicit Option6ClientFqdn(const uint8_t flags,
+ const std::string& domain_name,
+ const DomainNameType domain_name_type = FULL);
+
+ /// @brief Constructor, creates option instance using flags.
+ ///
+ /// This constructor creates an instance of the option with empty
+ /// domain-name. This domain-name is marked partial.
+ ///
+ /// @param flags A combination of flag bits to be stored in flags field.
+ Option6ClientFqdn(const uint8_t flags);
+
+ /// @brief Constructor, creates an option instance from part of the buffer.
+ ///
+ /// This constructor is mainly used to parse options in the received
+ /// messages. Function parameters specify buffer bounds from which the
+ /// option should be created. The size of the buffer chunk, specified by
+ /// the constructor's parameters should be equal or larger than the size
+ /// of the option. Otherwise, constructor will throw an exception.
+ ///
+ /// @param first the lower bound of the buffer to create option from.
+ /// @param last the upper bound of the buffer to create option from.
+ explicit Option6ClientFqdn(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Copy constructor
+ Option6ClientFqdn(const Option6ClientFqdn& source);
+
+ /// @brief Destructor
+ virtual ~Option6ClientFqdn();
+
+ /// @brief Assignment operator
+ Option6ClientFqdn& operator=(const Option6ClientFqdn& source);
+
+ /// @brief Checks if the specified flag of the DHCPv6 Client FQDN %Option
+ /// is set.
+ ///
+ /// This method checks the single bit of flags field. Therefore, a caller
+ /// should use one of the: @c FLAG_S, @c FLAG_N, @c FLAG_O constants as
+ /// an argument of the function. Attempt to use any other value (including
+ /// combinations of these constants) will result in exception.
+ ///
+ /// @param flag A value specifying the flags bit to be checked. It can be
+ /// one of the following: @c FLAG_S, @c FLAG_N, @c FLAG_O.
+ ///
+ /// @return true if the bit of the specified flag is set, false otherwise.
+ bool getFlag(const uint8_t flag) const;
+
+ /// @brief Modifies the value of the specified DHCPv6 Client Fqdn %Option
+ /// flag.
+ ///
+ /// This method sets the single bit of flags field. Therefore, a caller
+ /// should use one of the: @c FLAG_S, @c FLAG_N, @c FLAG_O constants as
+ /// an argument of the function. Attempt to use any other value (including
+ /// combinations of these constants) will result in exception.
+ ///
+ /// @param flag A value specifying the flags bit to be modified. It can
+ /// be one of the following: @c FLAG_S, @c FLAG_N, @c FLAG_O.
+ /// @param set a boolean value which indicates whether flag should be
+ /// set (true), or cleared (false).
+ void setFlag(const uint8_t flag, const bool set);
+
+ /// @brief Sets the flag field value to 0.
+ void resetFlags();
+
+ /// @brief Returns the domain-name in the text format.
+ ///
+ /// If domain-name is partial, it lacks the dot at the end (e.g. myhost).
+ /// If domain-name is fully qualified, it has the dot at the end (e.g.
+ /// myhost.example.com.).
+ ///
+ /// @return domain-name in the text format.
+ std::string getDomainName() const;
+
+ /// @brief Writes domain-name in the wire format into a buffer.
+ ///
+ /// The data being written are appended at the end of the buffer.
+ ///
+ /// @param [out] buf buffer where domain-name will be written.
+ void packDomainName(isc::util::OutputBuffer& buf) const;
+
+ /// @brief Set new domain-name.
+ ///
+ /// @param domain_name domain name field value in the text format.
+ /// @param domain_name_type type of the domain name: partial or fully
+ /// qualified.
+ void setDomainName(const std::string& domain_name,
+ const DomainNameType domain_name_type);
+
+ /// @brief Set empty domain-name.
+ ///
+ /// This function is equivalent to @c Option6ClientFqdn::setDomainName
+ /// with empty partial domain-name. It is exception safe.
+ void resetDomainName();
+
+ /// @brief Returns enumerator value which indicates whether domain-name is
+ /// partial or full.
+ ///
+ /// @return An enumerator value indicating whether domain-name is partial
+ /// or full.
+ DomainNameType getDomainNameType() const;
+
+ /// @brief Writes option in the wire format into a buffer.
+ ///
+ /// @param [out] buf output buffer where option data will be stored.
+ virtual void pack(isc::util::OutputBuffer& buf);
+
+ /// @brief Parses option from the received buffer.
+ ///
+ /// Method creates an instance of the DHCPv6 Client FQDN %Option from the
+ /// wire format. Parameters specify the bounds of the buffer to read option
+ /// data from. The size of the buffer limited by the specified parameters
+ /// should be equal or larger than size of the option (including its
+ /// header). Otherwise exception will be thrown.
+ ///
+ /// @param first lower bound of the buffer to parse option from.
+ /// @param last upper bound of the buffer to parse option from.
+ virtual void unpack(OptionBufferConstIter first,
+ OptionBufferConstIter last);
+
+ /// @brief Returns string representation of the option.
+ ///
+ /// The string returned by the method comprises the bit value of each
+ /// option flag and the domain-name.
+ ///
+ /// @param indent number of spaces before printed text.
+ ///
+ /// @return string with text representation.
+ virtual std::string toText(int indent = 0);
+
+ /// @brief Returns length of the complete option (data length +
+ /// DHCPv6 option header).
+ ///
+ /// @return length of the option.
+ virtual uint16_t len();
+
+private:
+
+ /// @brief A pointer to the implementation.
+ Option6ClientFqdnImpl* impl_;
+};
+
+/// A pointer to the @c Option6ClientFqdn object.
+typedef boost::shared_ptr<Option6ClientFqdn> Option6ClientFqdnPtr;
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // OPTION6_CLIENT_FQDN_H
diff --git a/src/lib/dhcp/option6_ia.cc b/src/lib/dhcp/option6_ia.cc
index 64c2936..825a5bf 100644
--- a/src/lib/dhcp/option6_ia.cc
+++ b/src/lib/dhcp/option6_ia.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -30,10 +30,28 @@ namespace dhcp {
Option6IA::Option6IA(uint16_t type, uint32_t iaid)
:Option(Option::V6, type), iaid_(iaid), t1_(0), t2_(0) {
+
+ // IA_TA has different layout than IA_NA and IA_PD. We can't sue this class
+ if (type == D6O_IA_TA) {
+ isc_throw(BadValue, "Can't use Option6IA for IA_TA as it has "
+ "a different layout");
+ }
+
+ setEncapsulatedSpace("dhcp6");
}
-Option6IA::Option6IA(uint16_t type, OptionBufferConstIter begin, OptionBufferConstIter end)
+Option6IA::Option6IA(uint16_t type, OptionBufferConstIter begin,
+ OptionBufferConstIter end)
:Option(Option::V6, type) {
+
+ // IA_TA has different layout than IA_NA and IA_PD. We can't use this class
+ if (type == D6O_IA_TA) {
+ isc_throw(BadValue, "Can't use Option6IA for IA_TA as it has "
+ "a different layout");
+ }
+
+ setEncapsulatedSpace("dhcp6");
+
unpack(begin, end);
}
@@ -99,7 +117,7 @@ uint16_t Option6IA::len() {
OPTION6_IA_LEN /* option content (12) */;
// length of all suboptions
- for (Option::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
diff --git a/src/lib/dhcp/option6_ia.h b/src/lib/dhcp/option6_ia.h
index 32f3667..e4e4d11 100644
--- a/src/lib/dhcp/option6_ia.h
+++ b/src/lib/dhcp/option6_ia.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,12 +16,17 @@
#define OPTION_IA_H
#include <dhcp/option.h>
-
+#include <boost/shared_ptr.hpp>
#include <stdint.h>
namespace isc {
namespace dhcp {
+class Option6IA;
+
+/// A pointer to the @c Option6IA object.
+typedef boost::shared_ptr<Option6IA> Option6IAPtr;
+
class Option6IA: public Option {
public:
diff --git a/src/lib/dhcp/option6_iaaddr.cc b/src/lib/dhcp/option6_iaaddr.cc
index d0ba087..cbcd555 100644
--- a/src/lib/dhcp/option6_iaaddr.cc
+++ b/src/lib/dhcp/option6_iaaddr.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -35,11 +35,16 @@ Option6IAAddr::Option6IAAddr(uint16_t type, const isc::asiolink::IOAddress& addr
uint32_t pref, uint32_t valid)
:Option(V6, type), addr_(addr), preferred_(pref),
valid_(valid) {
+ setEncapsulatedSpace("dhcp6");
+ if (!addr.isV6()) {
+ isc_throw(isc::BadValue, addr_ << " is not an IPv6 address");
+ }
}
Option6IAAddr::Option6IAAddr(uint32_t type, OptionBuffer::const_iterator begin,
OptionBuffer::const_iterator end)
:Option(V6, type), addr_("::") {
+ setEncapsulatedSpace("dhcp6");
unpack(begin, end);
}
@@ -52,8 +57,7 @@ void Option6IAAddr::pack(isc::util::OutputBuffer& buf) {
buf.writeUint16(len() - getHeaderLen());
if (!addr_.isV6()) {
- isc_throw(isc::BadValue, addr_.toText()
- << " is not an IPv6 address");
+ isc_throw(isc::BadValue, addr_ << " is not an IPv6 address");
}
buf.writeData(&addr_.toBytes()[0], isc::asiolink::V6ADDRESS_LEN);
@@ -88,7 +92,7 @@ std::string Option6IAAddr::toText(int indent /* =0 */) {
for (int i=0; i<indent; i++)
tmp << " ";
- tmp << "type=" << type_ << "(IAADDR) addr=" << addr_.toText()
+ tmp << "type=" << type_ << "(IAADDR) addr=" << addr_
<< ", preferred-lft=" << preferred_ << ", valid-lft="
<< valid_ << endl;
@@ -107,7 +111,7 @@ uint16_t Option6IAAddr::len() {
// length of all suboptions
// TODO implement:
// protected: unsigned short Option::lenHelper(int header_size);
- for (Option::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
diff --git a/src/lib/dhcp/option6_iaaddr.h b/src/lib/dhcp/option6_iaaddr.h
index 28c5abc..c188f88 100644
--- a/src/lib/dhcp/option6_iaaddr.h
+++ b/src/lib/dhcp/option6_iaaddr.h
@@ -17,17 +17,25 @@
#include <asiolink/io_address.h>
#include <dhcp/option.h>
+#include <boost/shared_ptr.hpp>
namespace isc {
namespace dhcp {
+class Option6IAAddr;
+
+/// A pointer to the @c isc::dhcp::Option6IAAddr object.
+typedef boost::shared_ptr<Option6IAAddr> Option6IAAddrPtr;
+
class Option6IAAddr: public Option {
public:
/// length of the fixed part of the IAADDR option
static const size_t OPTION6_IAADDR_LEN = 24;
- /// @brief Ctor, used for options constructed (during transmission).
+ /// @brief Constructor, used for options constructed (during transmission).
+ ///
+ /// @throw BadValue if specified addr is not IPv6
///
/// @param type option type
/// @param addr reference to an address
@@ -36,7 +44,9 @@ public:
Option6IAAddr(uint16_t type, const isc::asiolink::IOAddress& addr,
uint32_t preferred, uint32_t valid);
- /// @brief ctor, used for received options.
+ /// @brief Constructor, used for received options.
+ ///
+ /// @throw OutOfRange if specified option is truncated
///
/// @param type option type
/// @param begin iterator to first byte of option data
diff --git a/src/lib/dhcp/option6_iaprefix.cc b/src/lib/dhcp/option6_iaprefix.cc
new file mode 100644
index 0000000..d28b4f7
--- /dev/null
+++ b/src/lib/dhcp/option6_iaprefix.cc
@@ -0,0 +1,126 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option6_iaprefix.h>
+#include <exceptions/exceptions.h>
+#include <util/io_utilities.h>
+
+#include <sstream>
+
+#include <stdint.h>
+#include <arpa/inet.h>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+
+Option6IAPrefix::Option6IAPrefix(uint16_t type, const isc::asiolink::IOAddress& prefix,
+ uint8_t prefix_len, uint32_t pref, uint32_t valid)
+ :Option6IAAddr(type, prefix, pref, valid), prefix_len_(prefix_len) {
+ setEncapsulatedSpace("dhcp6");
+ // Option6IAAddr will check if prefix is IPv6 and will throw if it is not
+ if (prefix_len > 128) {
+ isc_throw(BadValue, prefix_len << " is not a valid prefix length. "
+ << "Allowed range is 0..128");
+ }
+}
+
+Option6IAPrefix::Option6IAPrefix(uint32_t type, OptionBuffer::const_iterator begin,
+ OptionBuffer::const_iterator end)
+ :Option6IAAddr(type, begin, end) {
+ setEncapsulatedSpace("dhcp6");
+ unpack(begin, end);
+}
+
+void Option6IAPrefix::pack(isc::util::OutputBuffer& buf) {
+ if (!addr_.isV6()) {
+ isc_throw(isc::BadValue, addr_ << " is not an IPv6 address");
+ }
+
+ buf.writeUint16(type_);
+
+ // len() returns complete option length. len field contains
+ // length without 4-byte option header
+ buf.writeUint16(len() - getHeaderLen());
+
+ buf.writeUint32(preferred_);
+ buf.writeUint32(valid_);
+ buf.writeUint8(prefix_len_);
+
+ buf.writeData(&addr_.toBytes()[0], isc::asiolink::V6ADDRESS_LEN);
+
+ // store encapsulated options (the only defined so far is PD_EXCLUDE)
+ packOptions(buf);
+}
+
+void Option6IAPrefix::unpack(OptionBuffer::const_iterator begin,
+ OptionBuffer::const_iterator end) {
+ if ( distance(begin, end) < OPTION6_IAPREFIX_LEN) {
+ isc_throw(OutOfRange, "Option " << type_ << " truncated");
+ }
+
+ preferred_ = readUint32( &(*begin) );
+ begin += sizeof(uint32_t);
+
+ valid_ = readUint32( &(*begin) );
+ begin += sizeof(uint32_t);
+
+ prefix_len_ = *begin;
+ begin += sizeof(uint8_t);
+
+ // 16 bytes: IPv6 address
+ addr_ = IOAddress::fromBytes(AF_INET6, &(*begin));
+ begin += V6ADDRESS_LEN;
+
+ // unpack encapsulated options (the only defined so far is PD_EXCLUDE)
+ unpackOptions(OptionBuffer(begin, end));
+}
+
+std::string Option6IAPrefix::toText(int indent /* =0 */) {
+ stringstream tmp;
+ for (int i=0; i<indent; i++)
+ tmp << " ";
+
+ tmp << "type=" << type_ << "(IAPREFIX) prefix=" << addr_ << "/"
+ << prefix_len_ << ", preferred-lft=" << preferred_ << ", valid-lft="
+ << valid_ << endl;
+
+ for (OptionCollection::const_iterator opt=options_.begin();
+ opt!=options_.end();
+ ++opt) {
+ tmp << (*opt).second->toText(indent + 2);
+ }
+ return tmp.str();
+}
+
+uint16_t Option6IAPrefix::len() {
+
+ uint16_t length = OPTION6_HDR_LEN + OPTION6_IAPREFIX_LEN;
+
+ // length of all suboptions
+ for (OptionCollection::const_iterator it = options_.begin();
+ it != options_.end(); ++it) {
+ length += (*it).second->len();
+ }
+ return (length);
+}
+
+} // end of namespace isc::dhcp
+} // end of namespace isc
diff --git a/src/lib/dhcp/option6_iaprefix.h b/src/lib/dhcp/option6_iaprefix.h
new file mode 100644
index 0000000..63ab9f7
--- /dev/null
+++ b/src/lib/dhcp/option6_iaprefix.h
@@ -0,0 +1,110 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION6_IAPREFIX_H
+#define OPTION6_IAPREFIX_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option.h>
+
+namespace isc {
+namespace dhcp {
+
+
+/// @brief Class that represents IAPREFIX option in DHCPv6
+///
+/// It is based on a similar class that handles addresses. There are major
+/// differences, though. The fields are in different order. There is also
+/// additional prefix length field.
+///
+/// It should be noted that to get a full prefix (2 values: base prefix, and
+/// a prefix length) 2 methods are used: getAddress() and getLength(). Although
+/// using getAddress() to obtain base prefix is somewhat counter-intuitive at
+/// first, it becomes obvious when one realizes that an address is a special
+/// case of a prefix with /128. It makes everyone's like much easier, because
+/// the base prefix doubles as a regular address in many cases, e.g. when
+/// searching for a lease.
+class Option6IAPrefix : public Option6IAAddr {
+
+public:
+ /// length of the fixed part of the IAPREFIX option
+ static const size_t OPTION6_IAPREFIX_LEN = 25;
+
+ /// @brief Constructor, used for options constructed (during transmission).
+ ///
+ /// @param type option type
+ /// @param addr reference to an address
+ /// @param prefix_length length (1-128)
+ /// @param preferred address preferred lifetime (in seconds)
+ /// @param valid address valid lifetime (in seconds)
+ Option6IAPrefix(uint16_t type, const isc::asiolink::IOAddress& addr,
+ uint8_t prefix_length, uint32_t preferred, uint32_t valid);
+
+ /// @brief Constructor, used for received options.
+ ///
+ /// @throw OutOfRange if buffer is too short
+ ///
+ /// @param type option type
+ /// @param begin iterator to first byte of option data
+ /// @param end iterator to end of option data (first byte after option end)
+ Option6IAPrefix(uint32_t type, OptionBuffer::const_iterator begin,
+ OptionBuffer::const_iterator end);
+
+ /// @brief Writes option in wire-format.
+ ///
+ /// Writes option in wire-format to buf, returns pointer to first unused
+ /// byte after stored option.
+ ///
+ /// @throw BadValue if the address is not IPv6
+ ///
+ /// @param buf pointer to a buffer
+ void pack(isc::util::OutputBuffer& buf);
+
+ /// @brief Parses received buffer.
+ ///
+ /// @throw OutOfRange when buffer is shorter than 25 bytes
+ ///
+ /// @param begin iterator to first byte of option data
+ /// @param end iterator to end of option data (first byte after option end)
+ virtual void unpack(OptionBufferConstIter begin,
+ OptionBufferConstIter end);
+
+ /// Returns string representation of the option.
+ ///
+ /// @param indent number of spaces before printing text
+ ///
+ /// @return string with text representation.
+ virtual std::string toText(int indent = 0);
+
+ /// sets address in this option.
+ ///
+ /// @param prefix prefix to be sent in this option
+ /// @param length prefix length
+ void setPrefix(const isc::asiolink::IOAddress& prefix,
+ uint8_t length) { addr_ = prefix; prefix_len_ = length; }
+
+ uint8_t getLength() const { return prefix_len_; }
+
+ /// returns data length (data length + DHCPv4/DHCPv6 option header)
+ virtual uint16_t len();
+
+protected:
+ uint8_t prefix_len_;
+};
+
+} // isc::dhcp namespace
+} // isc namespace
+
+#endif // OPTION_IA_H
diff --git a/src/lib/dhcp/option_custom.cc b/src/lib/dhcp/option_custom.cc
index e807928..0709d20 100644
--- a/src/lib/dhcp/option_custom.cc
+++ b/src/lib/dhcp/option_custom.cc
@@ -24,34 +24,27 @@ OptionCustom::OptionCustom(const OptionDefinition& def,
Universe u)
: Option(u, def.getCode(), OptionBuffer()),
definition_(def) {
+ setEncapsulatedSpace(def.getEncapsulatedSpace());
createBuffers();
}
OptionCustom::OptionCustom(const OptionDefinition& def,
- Universe u,
- const OptionBuffer& data)
+ Universe u,
+ const OptionBuffer& data)
: Option(u, def.getCode(), data.begin(), data.end()),
definition_(def) {
- // It is possible that no data is provided if an option
- // is being created on a server side. In such case a bunch
- // of buffers with default values is first created and then
- // the values are replaced using writeXXX functions. Thus
- // we need to detect that no data has been specified and
- // take a different code path.
- if (!data_.empty()) {
- createBuffers(data_);
- } else {
- createBuffers();
- }
+ setEncapsulatedSpace(def.getEncapsulatedSpace());
+ createBuffers(getData());
}
OptionCustom::OptionCustom(const OptionDefinition& def,
- Universe u,
- OptionBufferConstIter first,
- OptionBufferConstIter last)
+ Universe u,
+ OptionBufferConstIter first,
+ OptionBufferConstIter last)
: Option(u, def.getCode(), first, last),
definition_(def) {
- createBuffers(data_);
+ setEncapsulatedSpace(def.getEncapsulatedSpace());
+ createBuffers(getData());
}
void
@@ -61,7 +54,7 @@ OptionCustom::addArrayDataField(const asiolink::IOAddress& address) {
if ((address.isV4() && definition_.getType() != OPT_IPV4_ADDRESS_TYPE) ||
(address.isV6() && definition_.getType() != OPT_IPV6_ADDRESS_TYPE)) {
isc_throw(BadDataTypeCast, "invalid address specified "
- << address.toText() << ". Expected a valid IPv"
+ << address << ". Expected a valid IPv"
<< (definition_.getType() == OPT_IPV4_ADDRESS_TYPE ?
"4" : "6") << " address.");
}
@@ -256,6 +249,12 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
// Proceed to the next data field.
data += data_size;
}
+
+ // Unpack suboptions if any.
+ if (data != data_buf.end() && !getEncapsulatedSpace().empty()) {
+ unpackOptions(OptionBuffer(data, data_buf.end()));
+ }
+
} else if (data_type != OPT_EMPTY_TYPE) {
// If data_type value is other than OPT_RECORD_TYPE, our option is
// empty (have no data at all) or it comprises one or more
@@ -323,9 +322,20 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
}
if (data_size > 0) {
buffers.push_back(OptionBuffer(data, data + data_size));
+ data += data_size;
} else {
isc_throw(OutOfRange, "option buffer truncated");
}
+
+ // Unpack suboptions if any.
+ if (data != data_buf.end() && !getEncapsulatedSpace().empty()) {
+ unpackOptions(OptionBuffer(data, data_buf.end()));
+ }
+ }
+ } else if (data_type == OPT_EMPTY_TYPE) {
+ // Unpack suboptions if any.
+ if (data != data_buf.end() && !getEncapsulatedSpace().empty()) {
+ unpackOptions(OptionBuffer(data, data_buf.end()));
}
}
// If everything went ok we can replace old buffer set with new ones.
@@ -365,7 +375,7 @@ OptionCustom::dataFieldToText(const OptionDataType data_type,
break;
case OPT_IPV4_ADDRESS_TYPE:
case OPT_IPV6_ADDRESS_TYPE:
- text << readAddress(index).toText();
+ text << readAddress(index);
break;
case OPT_FQDN_TYPE:
text << readFqdn(index);
@@ -433,7 +443,7 @@ OptionCustom::writeAddress(const asiolink::IOAddress& address,
if ((address.isV4() && buffers_[index].size() != V4ADDRESS_LEN) ||
(address.isV6() && buffers_[index].size() != V6ADDRESS_LEN)) {
isc_throw(BadDataTypeCast, "invalid address specified "
- << address.toText() << ". Expected a valid IPv"
+ << address << ". Expected a valid IPv"
<< (buffers_[index].size() == V4ADDRESS_LEN ? "4" : "6")
<< " address.");
}
@@ -517,9 +527,7 @@ OptionCustom::writeString(const std::string& text, const uint32_t index) {
void
OptionCustom::unpack(OptionBufferConstIter begin,
OptionBufferConstIter end) {
- data_ = OptionBuffer(begin, end);
- // Chop the buffer stored in data_ into set of sub buffers.
- createBuffers(data_);
+ initialize(begin, end);
}
uint16_t
@@ -534,7 +542,7 @@ OptionCustom::len() {
}
// ... and lengths of all suboptions
- for (OptionCustom::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
@@ -543,15 +551,13 @@ OptionCustom::len() {
return (length);
}
-void OptionCustom::setData(const OptionBufferConstIter first,
- const OptionBufferConstIter last) {
- // We will copy entire option buffer, so we have to resize data_.
- data_.resize(std::distance(first, last));
- std::copy(first, last, data_.begin());
+void OptionCustom::initialize(const OptionBufferConstIter first,
+ const OptionBufferConstIter last) {
+ setData(first, last);
// Chop the data_ buffer into set of buffers that represent
// option fields data.
- createBuffers(data_);
+ createBuffers(getData());
}
std::string OptionCustom::toText(int indent) {
diff --git a/src/lib/dhcp/option_custom.h b/src/lib/dhcp/option_custom.h
index 5766cb4..6ae0b18 100644
--- a/src/lib/dhcp/option_custom.h
+++ b/src/lib/dhcp/option_custom.h
@@ -105,7 +105,6 @@ public:
template<typename T>
void addArrayDataField(const T value) {
checkArrayType();
-
OptionDataType data_type = definition_.getType();
if (OptionDataTypeTraits<T>::type != data_type) {
isc_throw(isc::dhcp::InvalidDataType,
@@ -280,8 +279,8 @@ public:
///
/// @param first iterator pointing to beginning of buffer to copy.
/// @param last iterator pointing to end of buffer to copy.
- void setData(const OptionBufferConstIter first,
- const OptionBufferConstIter last);
+ void initialize(const OptionBufferConstIter first,
+ const OptionBufferConstIter last);
private:
@@ -310,6 +309,7 @@ private:
///
/// @throw isc::dhcp::InvalidDataType if the type is invalid.
template<typename T>
+ // cppcheck-suppress unusedPrivateFunction
void checkDataType(const uint32_t index) const;
/// @brief Check if data field index is valid.
@@ -336,6 +336,11 @@ private:
std::string dataFieldToText(const OptionDataType data_type,
const uint32_t index) const;
+ /// Make this function private as we don't want it to be invoked
+ /// on OptionCustom object. We rather want that initialize to
+ /// be called instead.
+ using Option::setData;
+
/// Option definition used to create an option.
OptionDefinition definition_;
diff --git a/src/lib/dhcp/option_data_types.cc b/src/lib/dhcp/option_data_types.cc
index a567b7e..4c29596 100644
--- a/src/lib/dhcp/option_data_types.cc
+++ b/src/lib/dhcp/option_data_types.cc
@@ -170,7 +170,7 @@ OptionDataTypeUtil::writeBinary(const std::string& hex_str,
bool
OptionDataTypeUtil::readBool(const std::vector<uint8_t>& buf) {
- if (buf.size() < 1) {
+ if (buf.empty()) {
isc_throw(BadDataTypeCast, "unable to read the buffer as boolean"
<< " value. Invalid buffer size " << buf.size());
}
@@ -212,9 +212,10 @@ OptionDataTypeUtil::readFqdn(const std::vector<uint8_t>& buf) {
void
OptionDataTypeUtil::writeFqdn(const std::string& fqdn,
- std::vector<uint8_t>& buf) {
+ std::vector<uint8_t>& buf,
+ bool downcase) {
try {
- isc::dns::Name name(fqdn);
+ isc::dns::Name name(fqdn, downcase);
isc::dns::LabelSequence labels(name);
if (labels.getDataLength() > 0) {
size_t read_len = 0;
@@ -226,6 +227,24 @@ OptionDataTypeUtil::writeFqdn(const std::string& fqdn,
}
}
+unsigned int
+OptionDataTypeUtil::getLabelCount(const std::string& text_name) {
+ // The isc::dns::Name class doesn't accept empty names. However, in some
+ // cases we may be dealing with empty names (e.g. sent by the DHCP clients).
+ // Empty names should not be sent as hostnames but if they are, for some
+ // reason, we don't want to throw an exception from this function. We
+ // rather want to signal empty name by returning 0 number of labels.
+ if (text_name.empty()) {
+ return (0);
+ }
+ try {
+ isc::dns::Name name(text_name);
+ return (name.getLabelCount());
+ } catch (const isc::Exception& ex) {
+ isc_throw(BadDataTypeCast, ex.what());
+ }
+}
+
std::string
OptionDataTypeUtil::readString(const std::vector<uint8_t>& buf) {
std::string value;
diff --git a/src/lib/dhcp/option_data_types.h b/src/lib/dhcp/option_data_types.h
index 94e7e9e..d9d8a52 100644
--- a/src/lib/dhcp/option_data_types.h
+++ b/src/lib/dhcp/option_data_types.h
@@ -366,11 +366,25 @@ public:
///
/// @param fqdn fully qualified domain name to be written.
/// @param [out] buf output buffer.
+ /// @param downcase indicates if the FQDN should be converted to lower
+ /// case (if true). By default it is not converted.
///
/// @throw isc::dhcp::BadDataTypeCast if provided FQDN
/// is invalid.
static void writeFqdn(const std::string& fqdn,
- std::vector<uint8_t>& buf);
+ std::vector<uint8_t>& buf,
+ const bool downcase = false);
+
+ /// @brief Return the number of labels in the Name.
+ ///
+ /// If the specified name is empty the 0 is returned.
+ ///
+ /// @param text_name A text representation of the name.
+ ///
+ /// @return A number of labels in the provided name or 0 if the
+ /// name string is empty.
+ /// @throw isc::dhcp::BadDataTypeCast if provided name is malformed.
+ static unsigned int getLabelCount(const std::string& text_name);
/// @brief Read string value from a buffer.
///
diff --git a/src/lib/dhcp/option_definition.cc b/src/lib/dhcp/option_definition.cc
index 7f0d578..78cb39b 100644
--- a/src/lib/dhcp/option_definition.cc
+++ b/src/lib/dhcp/option_definition.cc
@@ -12,16 +12,22 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <dhcp/dhcp4.h>
#include <dhcp/dhcp6.h>
#include <dhcp/option4_addrlst.h>
+#include <dhcp/option4_client_fqdn.h>
#include <dhcp/option6_addrlst.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
+#include <dhcp/option6_client_fqdn.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
#include <dhcp/option_space.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor.h>
#include <util/encode/hex.h>
#include <util/strutil.h>
#include <boost/algorithm/string/classification.hpp>
@@ -100,7 +106,8 @@ OptionDefinition::addRecordField(const OptionDataType data_type) {
if (data_type >= OPT_RECORD_TYPE ||
data_type == OPT_ANY_ADDRESS_TYPE ||
data_type == OPT_EMPTY_TYPE) {
- isc_throw(isc::BadValue, "attempted to add invalid data type to the record.");
+ isc_throw(isc::BadValue,
+ "attempted to add invalid data type to the record.");
}
record_fields_.push_back(data_type);
}
@@ -108,38 +115,67 @@ OptionDefinition::addRecordField(const OptionDataType data_type) {
OptionPtr
OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
OptionBufferConstIter begin,
- OptionBufferConstIter end) const {
+ OptionBufferConstIter end,
+ UnpackOptionsCallback callback) const {
+
try {
+ // Some of the options are represented by the specialized classes derived
+ // from Option class (e.g. IA_NA, IAADDR). Although, they can be also
+ // represented by the generic classes, we want the object of the specialized
+ // type to be returned. Therefore, we first check that if we are dealing
+ // with such an option. If the instance is returned we just exit at this
+ // point. If not, we will search for a generic option type to return.
+ OptionPtr option = factorySpecialFormatOption(u, begin, end, callback);
+ if (option) {
+ return (option);
+ }
+
switch(type_) {
case OPT_EMPTY_TYPE:
- return (factoryEmpty(u, type));
+ if (getEncapsulatedSpace().empty()) {
+ return (factoryEmpty(u, type));
+ } else {
+ return (OptionPtr(new OptionCustom(*this, u, begin, end)));
+ }
case OPT_BINARY_TYPE:
return (factoryGeneric(u, type, begin, end));
case OPT_UINT8_TYPE:
- return (array_type_ ? factoryGeneric(u, type, begin, end) :
- factoryInteger<uint8_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<uint8_t>(u, type, begin, end) :
+ factoryInteger<uint8_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_INT8_TYPE:
- return (array_type_ ? factoryGeneric(u, type, begin, end) :
- factoryInteger<int8_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<int8_t>(u, type, begin, end) :
+ factoryInteger<int8_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_UINT16_TYPE:
- return (array_type_ ? factoryIntegerArray<uint16_t>(u, type, begin, end) :
- factoryInteger<uint16_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<uint16_t>(u, type, begin, end) :
+ factoryInteger<uint16_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_INT16_TYPE:
- return (array_type_ ? factoryIntegerArray<uint16_t>(u, type, begin, end) :
- factoryInteger<int16_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<uint16_t>(u, type, begin, end) :
+ factoryInteger<int16_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_UINT32_TYPE:
- return (array_type_ ? factoryIntegerArray<uint32_t>(u, type, begin, end) :
- factoryInteger<uint32_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<uint32_t>(u, type, begin, end) :
+ factoryInteger<uint32_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_INT32_TYPE:
- return (array_type_ ? factoryIntegerArray<uint32_t>(u, type, begin, end) :
- factoryInteger<int32_t>(u, type, begin, end));
+ return (array_type_ ?
+ factoryIntegerArray<uint32_t>(u, type, begin, end) :
+ factoryInteger<int32_t>(u, type, getEncapsulatedSpace(),
+ begin, end, callback));
case OPT_IPV4_ADDRESS_TYPE:
// If definition specifies that an option is an array
@@ -160,30 +196,14 @@ OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
}
break;
+ case OPT_STRING_TYPE:
+ return (OptionPtr(new OptionString(u, type, begin, end)));
+
default:
- if (u == Option::V6) {
- if ((code_ == D6O_IA_NA || code_ == D6O_IA_PD) &&
- haveIA6Format()) {
- // Return Option6IA instance for IA_PD and IA_NA option
- // types only. We don't want to return Option6IA for other
- // options that comprise 3 UINT32 data fields because
- // Option6IA accessors' and modifiers' names are derived
- // from the IA_NA and IA_PD options' field names: IAID,
- // T1, T2. Using functions such as getIAID, getT1 etc. for
- // options other than IA_NA and IA_PD would be bad practice
- // and cause confusion.
- return (factoryIA6(type, begin, end));
-
- } else if (code_ == D6O_IAADDR && haveIAAddr6Format()) {
- // Rerurn Option6IAAddr option instance for the IAADDR
- // option only for the same reasons as described in
- // for IA_NA and IA_PD above.
- return (factoryIAAddr6(type, begin, end));
- }
- }
+ // Do nothing. We will return generic option a few lines down.
+ ;
}
- return (OptionPtr(new OptionCustom(*this, u, OptionBuffer(begin, end))));
-
+ return (OptionPtr(new OptionCustom(*this, u, begin, end)));
} catch (const Exception& ex) {
isc_throw(InvalidOptionValue, ex.what());
}
@@ -191,8 +211,9 @@ OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
OptionPtr
OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
- const OptionBuffer& buf) const {
- return (optionFactory(u, type, buf.begin(), buf.end()));
+ const OptionBuffer& buf,
+ UnpackOptionsCallback callback) const {
+ return (optionFactory(u, type, buf.begin(), buf.end(), callback));
}
OptionPtr
@@ -224,14 +245,6 @@ OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
}
void
-OptionDefinition::sanityCheckUniverse(const Option::Universe expected_universe,
- const Option::Universe actual_universe) {
- if (expected_universe != actual_universe) {
- isc_throw(isc::BadValue, "invalid universe specified for the option");
- }
-}
-
-void
OptionDefinition::validate() const {
using namespace boost::algorithm;
@@ -268,10 +281,12 @@ OptionDefinition::validate() const {
// it no way to tell when other data fields begin.
err_str << "array of strings is not a valid option definition.";
} else if (type_ == OPT_BINARY_TYPE) {
- err_str << "array of binary values is not a valid option definition.";
+ err_str << "array of binary values is not"
+ << " a valid option definition.";
} else if (type_ == OPT_EMPTY_TYPE) {
- err_str << "array of empty value is not a valid option definition.";
+ err_str << "array of empty value is not"
+ << " a valid option definition.";
}
@@ -279,33 +294,34 @@ OptionDefinition::validate() const {
// At least two data fields should be added to the record. Otherwise
// non-record option definition could be used.
if (getRecordFields().size() < 2) {
- err_str << "invalid number of data fields: " << getRecordFields().size()
+ err_str << "invalid number of data fields: "
+ << getRecordFields().size()
<< " specified for the option of type 'record'. Expected at"
<< " least 2 fields.";
} else {
// If the number of fields is valid we have to check if their order
// is valid too. We check that string or binary data fields are not
- // laid before other fields. But we allow that they are laid at the end of
- // an option.
+ // laid before other fields. But we allow that they are laid at the
+ // end of an option.
const RecordFieldsCollection& fields = getRecordFields();
for (RecordFieldsConstIter it = fields.begin();
it != fields.end(); ++it) {
if (*it == OPT_STRING_TYPE &&
it < fields.end() - 1) {
- err_str << "string data field can't be laid before data fields"
- << " of other types.";
+ err_str << "string data field can't be laid before data"
+ << " fields of other types.";
break;
}
if (*it == OPT_BINARY_TYPE &&
it < fields.end() - 1) {
- err_str << "binary data field can't be laid before data fields"
- << " of other types.";
+ err_str << "binary data field can't be laid before data"
+ << " fields of other types.";
}
/// Empty type is not allowed within a record.
if (*it == OPT_EMPTY_TYPE) {
- err_str << "empty data type can't be stored as a field in an"
- << " option record.";
+ err_str << "empty data type can't be stored as a field in"
+ << " an option record.";
break;
}
}
@@ -345,13 +361,54 @@ OptionDefinition::haveIAAddr6Format() const {
return (haveIAx6Format(OPT_IPV6_ADDRESS_TYPE));
}
+bool
+OptionDefinition::haveIAPrefix6Format() const {
+ return (haveType(OPT_RECORD_TYPE) &&
+ record_fields_.size() == 4 &&
+ record_fields_[0] == OPT_UINT32_TYPE &&
+ record_fields_[1] == OPT_UINT32_TYPE &&
+ record_fields_[2] == OPT_UINT8_TYPE &&
+ record_fields_[3] == OPT_IPV6_ADDRESS_TYPE);
+}
+
+bool
+OptionDefinition::haveFqdn4Format() const {
+ return (haveType(OPT_RECORD_TYPE) &&
+ record_fields_.size() == 4 &&
+ record_fields_[0] == OPT_UINT8_TYPE &&
+ record_fields_[1] == OPT_UINT8_TYPE &&
+ record_fields_[2] == OPT_UINT8_TYPE &&
+ record_fields_[3] == OPT_FQDN_TYPE);
+}
+
+bool
+OptionDefinition::haveClientFqdnFormat() const {
+ return (haveType(OPT_RECORD_TYPE) &&
+ (record_fields_.size() == 2) &&
+ (record_fields_[0] == OPT_UINT8_TYPE) &&
+ (record_fields_[1] == OPT_FQDN_TYPE));
+}
+
+bool
+OptionDefinition::haveVendor4Format() const {
+ return (true);
+}
+
+bool
+OptionDefinition::haveVendor6Format() const {
+ return (getType() == OPT_UINT32_TYPE && !getEncapsulatedSpace().empty());
+}
+
template<typename T>
-T OptionDefinition::lexicalCastWithRangeCheck(const std::string& value_str) const {
+T
+OptionDefinition::lexicalCastWithRangeCheck(const std::string& value_str)
+ const {
// Lexical cast in case of our data types make sense only
// for uintX_t, intX_t and bool type.
if (!OptionDataTypeTraits<T>::integer_type &&
OptionDataTypeTraits<T>::type != OPT_BOOLEAN_TYPE) {
- isc_throw(BadDataTypeCast, "unable to do lexical cast to non-integer and"
+ isc_throw(BadDataTypeCast,
+ "unable to do lexical cast to non-integer and"
<< " non-boolean data type");
}
// We use the 64-bit value here because it has wider range than
@@ -368,16 +425,18 @@ T OptionDefinition::lexicalCastWithRangeCheck(const std::string& value_str) cons
if (OptionDataTypeTraits<T>::integer_type) {
data_type_str = "integer";
}
- isc_throw(BadDataTypeCast, "unable to do lexical cast to " << data_type_str
- << " data type for value " << value_str << ": " << ex.what());
+ isc_throw(BadDataTypeCast, "unable to do lexical cast to "
+ << data_type_str << " data type for value "
+ << value_str << ": " << ex.what());
}
// Perform range checks for integer values only (exclude bool values).
if (OptionDataTypeTraits<T>::integer_type) {
if (result > numeric_limits<T>::max() ||
result < numeric_limits<T>::min()) {
isc_throw(BadDataTypeCast, "unable to do lexical cast for value "
- << value_str << ". This value is expected to be in the range of "
- << numeric_limits<T>::min() << ".." << numeric_limits<T>::max());
+ << value_str << ". This value is expected to be"
+ << " in the range of " << numeric_limits<T>::min()
+ << ".." << numeric_limits<T>::max());
}
}
return (static_cast<T>(result));
@@ -399,30 +458,37 @@ OptionDefinition::writeToBuffer(const std::string& value,
// That way we actually waste 7 bits but it seems to be the
// simpler way to encode boolean.
// @todo Consider if any other encode methods can be used.
- OptionDataTypeUtil::writeBool(lexicalCastWithRangeCheck<bool>(value), buf);
+ OptionDataTypeUtil::writeBool(lexicalCastWithRangeCheck<bool>(value),
+ buf);
return;
case OPT_INT8_TYPE:
- OptionDataTypeUtil::writeInt<uint8_t>(lexicalCastWithRangeCheck<int8_t>(value),
+ OptionDataTypeUtil::writeInt<uint8_t>
+ (lexicalCastWithRangeCheck<int8_t>(value),
buf);
return;
case OPT_INT16_TYPE:
- OptionDataTypeUtil::writeInt<uint16_t>(lexicalCastWithRangeCheck<int16_t>(value),
+ OptionDataTypeUtil::writeInt<uint16_t>
+ (lexicalCastWithRangeCheck<int16_t>(value),
buf);
return;
case OPT_INT32_TYPE:
- OptionDataTypeUtil::writeInt<uint32_t>(lexicalCastWithRangeCheck<int32_t>(value),
+ OptionDataTypeUtil::writeInt<uint32_t>
+ (lexicalCastWithRangeCheck<int32_t>(value),
buf);
return;
case OPT_UINT8_TYPE:
- OptionDataTypeUtil::writeInt<uint8_t>(lexicalCastWithRangeCheck<uint8_t>(value),
+ OptionDataTypeUtil::writeInt<uint8_t>
+ (lexicalCastWithRangeCheck<uint8_t>(value),
buf);
return;
case OPT_UINT16_TYPE:
- OptionDataTypeUtil::writeInt<uint16_t>(lexicalCastWithRangeCheck<uint16_t>(value),
+ OptionDataTypeUtil::writeInt<uint16_t>
+ (lexicalCastWithRangeCheck<uint16_t>(value),
buf);
return;
case OPT_UINT32_TYPE:
- OptionDataTypeUtil::writeInt<uint32_t>(lexicalCastWithRangeCheck<uint32_t>(value),
+ OptionDataTypeUtil::writeInt<uint32_t>
+ (lexicalCastWithRangeCheck<uint32_t>(value),
buf);
return;
case OPT_IPV4_ADDRESS_TYPE:
@@ -430,7 +496,8 @@ OptionDefinition::writeToBuffer(const std::string& value,
{
asiolink::IOAddress address(value);
if (!address.isV4() && !address.isV6()) {
- isc_throw(BadDataTypeCast, "provided address " << address.toText()
+ isc_throw(BadDataTypeCast, "provided address "
+ << address
<< " is not a valid IPv4 or IPv6 address.");
}
OptionDataTypeUtil::writeAddress(address, buf);
@@ -458,7 +525,8 @@ OptionPtr
OptionDefinition::factoryAddrList4(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end) {
- boost::shared_ptr<Option4AddrLst> option(new Option4AddrLst(type, begin, end));
+ boost::shared_ptr<Option4AddrLst> option(new Option4AddrLst(type, begin,
+ end));
return (option);
}
@@ -466,7 +534,8 @@ OptionPtr
OptionDefinition::factoryAddrList6(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end) {
- boost::shared_ptr<Option6AddrLst> option(new Option6AddrLst(type, begin, end));
+ boost::shared_ptr<Option6AddrLst> option(new Option6AddrLst(type, begin,
+ end));
return (option);
}
@@ -490,8 +559,9 @@ OptionDefinition::factoryIA6(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end) {
if (std::distance(begin, end) < Option6IA::OPTION6_IA_LEN) {
- isc_throw(isc::OutOfRange, "input option buffer has invalid size, expected "
- "at least " << Option6IA::OPTION6_IA_LEN << " bytes");
+ isc_throw(isc::OutOfRange, "input option buffer has invalid size,"
+ << " expected at least " << Option6IA::OPTION6_IA_LEN
+ << " bytes");
}
boost::shared_ptr<Option6IA> option(new Option6IA(type, begin, end));
return (option);
@@ -502,13 +572,74 @@ OptionDefinition::factoryIAAddr6(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end) {
if (std::distance(begin, end) < Option6IAAddr::OPTION6_IAADDR_LEN) {
- isc_throw(isc::OutOfRange, "input option buffer has invalid size, expected "
- " at least " << Option6IAAddr::OPTION6_IAADDR_LEN << " bytes");
+ isc_throw(isc::OutOfRange,
+ "input option buffer has invalid size, expected at least "
+ << Option6IAAddr::OPTION6_IAADDR_LEN << " bytes");
}
- boost::shared_ptr<Option6IAAddr> option(new Option6IAAddr(type, begin, end));
+ boost::shared_ptr<Option6IAAddr> option(new Option6IAAddr(type, begin,
+ end));
return (option);
}
+OptionPtr
+OptionDefinition::factoryIAPrefix6(uint16_t type,
+ OptionBufferConstIter begin,
+ OptionBufferConstIter end) {
+ if (std::distance(begin, end) < Option6IAPrefix::OPTION6_IAPREFIX_LEN) {
+ isc_throw(isc::OutOfRange,
+ "input option buffer has invalid size, expected at least "
+ << Option6IAPrefix::OPTION6_IAPREFIX_LEN << " bytes");
+ }
+ boost::shared_ptr<Option6IAPrefix> option(new Option6IAPrefix(type, begin,
+ end));
+ return (option);
+}
+
+OptionPtr
+OptionDefinition::factorySpecialFormatOption(Option::Universe u,
+ OptionBufferConstIter begin,
+ OptionBufferConstIter end,
+ UnpackOptionsCallback) const {
+ if (u == Option::V6) {
+ if ((getCode() == D6O_IA_NA || getCode() == D6O_IA_PD) &&
+ haveIA6Format()) {
+ // Return Option6IA instance for IA_PD and IA_NA option
+ // types only. We don't want to return Option6IA for other
+ // options that comprise 3 UINT32 data fields because
+ // Option6IA accessors' and modifiers' names are derived
+ // from the IA_NA and IA_PD options' field names: IAID,
+ // T1, T2. Using functions such as getIAID, getT1 etc. for
+ // options other than IA_NA and IA_PD would be bad practice
+ // and cause confusion.
+ return (factoryIA6(getCode(), begin, end));
+
+ } else if (getCode() == D6O_IAADDR && haveIAAddr6Format()) {
+ // Rerurn Option6IAAddr option instance for the IAADDR
+ // option only for the same reasons as described in
+ // for IA_NA and IA_PD above.
+ return (factoryIAAddr6(getCode(), begin, end));
+ } else if (getCode() == D6O_IAPREFIX && haveIAPrefix6Format()) {
+ return (factoryIAPrefix6(getCode(), begin, end));
+ } else if (getCode() == D6O_CLIENT_FQDN && haveClientFqdnFormat()) {
+ // FQDN option requires special processing. Thus, there is
+ // a specialized class to handle it.
+ return (OptionPtr(new Option6ClientFqdn(begin, end)));
+ } else if (getCode() == D6O_VENDOR_OPTS && haveVendor6Format()) {
+ // Vendor-Specific Information.
+ return (OptionPtr(new OptionVendor(Option::V6, begin, end)));
+ }
+ } else {
+ if ((getCode() == DHO_FQDN) && haveFqdn4Format()) {
+ return (OptionPtr(new Option4ClientFqdn(begin, end)));
+
+ } else if (getCode() == DHO_VIVSO_SUBOPTIONS && haveVendor4Format()) {
+ // Vendor-Specific Information.
+ return (OptionPtr(new OptionVendor(Option::V4, begin, end)));
+
+ }
+ }
+ return (OptionPtr());
+}
} // end of isc::dhcp namespace
} // end of isc namespace
diff --git a/src/lib/dhcp/option_definition.h b/src/lib/dhcp/option_definition.h
index 0aa0e17..25ce5e2 100644
--- a/src/lib/dhcp/option_definition.h
+++ b/src/lib/dhcp/option_definition.h
@@ -23,6 +23,7 @@
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/shared_ptr.hpp>
+#include <map>
namespace isc {
namespace dhcp {
@@ -275,6 +276,54 @@ public:
/// @return true if specified format is IAADDR option format.
bool haveIAAddr6Format() const;
+ /// @brief Check if specified format is IAPREFIX option format.
+ ///
+ /// @return true if specified format is IAPREFIX option format.
+ bool haveIAPrefix6Format() const;
+
+ /// @brief Check if specified format is OPTION_CLIENT_FQDN option format.
+ ///
+ /// @return true of specified format is OPTION_CLIENT_FQDN option format,
+ /// false otherwise.
+ bool haveClientFqdnFormat() const;
+
+ /// @brief Check if option has format of the DHCPv4 Client FQDN
+ /// %Option.
+ ///
+ /// The encoding of the domain-name carried by the FQDN option is
+ /// conditional and is specified in the flags field of the option.
+ /// The domain-name can be encoded in the ASCII format or canonical
+ /// wire format. The ASCII format is deprecated, therefore canonical
+ /// format is selected for the FQDN option definition and this function
+ /// returns true if the option definition comprises the domain-name
+ /// field encoded in canonical format.
+ ///
+ /// @return true if option has the format of DHCPv4 Client FQDN
+ /// %Option.
+ bool haveFqdn4Format() const;
+
+ /// @brief Check if the option has format of Vendor-Identifying Vendor
+ /// Specific Options.
+ ///
+ /// @return Always true.
+ /// @todo The Vendor-Identifying Vendor-Specific Option has a complex format
+ /// which we do not support here. Therefore it is not really possible to
+ /// check that the current definition is valid. We may need to add support
+ /// for such option format or simply do not check the format for certain
+ /// options, e.g. vendor options, IA_NA, IAADDR and always return objects
+ /// of the certain type.
+ bool haveVendor4Format() const;
+
+ /// @brief Check if option has a format of the Vendor-Specific Information
+ /// %Option.
+ ///
+ /// The Vendor-Specific Information %Option comprises 32-bit enterprise id
+ /// and the suboptions.
+ ///
+ /// @return true if option definition conforms to the format of the
+ /// Vendor-Specific Information %Option.
+ bool haveVendor6Format() const;
+
/// @brief Option factory.
///
/// This function creates an instance of DHCP option using
@@ -289,12 +338,17 @@ public:
/// @param type option type.
/// @param begin beginning of the option buffer.
/// @param end end of the option buffer.
+ /// @param callback An instance of the function which parses packet options.
+ /// If this is set to non NULL value this function will be used instead of
+ /// @c isc::dhcp::LibDHCP::unpackOptions6 and
+ /// isc::dhcp::LibDHCP::unpackOptions4.
///
/// @return instance of the DHCP option.
/// @throw InvalidOptionValue if data for the option is invalid.
OptionPtr optionFactory(Option::Universe u, uint16_t type,
OptionBufferConstIter begin,
- OptionBufferConstIter end) const;
+ OptionBufferConstIter end,
+ UnpackOptionsCallback callback = NULL) const;
/// @brief Option factory.
///
@@ -309,11 +363,16 @@ public:
/// @param u option universe (V4 or V6).
/// @param type option type.
/// @param buf option buffer.
+ /// @param callback An instance of the function which parses packet options.
+ /// If this is set to non NULL value this function will be used instead of
+ /// @c isc::dhcp::LibDHCP::unpackOptions6 and
+ /// isc::dhcp::LibDHCP::unpackOptions4.
///
/// @return instance of the DHCP option.
/// @throw InvalidOptionValue if data for the option is invalid.
OptionPtr optionFactory(Option::Universe u, uint16_t type,
- const OptionBuffer& buf = OptionBuffer()) const;
+ const OptionBuffer& buf = OptionBuffer(),
+ UnpackOptionsCallback callback = NULL) const;
/// @brief Option factory.
///
@@ -394,7 +453,6 @@ public:
///
/// @throw isc::OutOfRange if provided option buffer is too short or
/// too long. Expected size is 12 bytes.
- /// @throw isc::BadValue if specified universe value is not V6.
static OptionPtr factoryIA6(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end);
@@ -407,25 +465,48 @@ public:
///
/// @throw isc::OutOfRange if provided option buffer is too short or
/// too long. Expected size is 24 bytes.
- /// @throw isc::BadValue if specified universe value is not V6.
static OptionPtr factoryIAAddr6(uint16_t type,
OptionBufferConstIter begin,
OptionBufferConstIter end);
+ /// @brief Factory for IAPREFIX-type of option.
+ ///
+ /// @param type option type.
+ /// @param begin iterator pointing to the beginning of the buffer.
+ /// @param end iterator pointing to the end of the buffer.
+ ///
+ /// @throw isc::OutOfRange if provided option buffer is too short.
+ /// Expected minimum size is 25 bytes.
+ static OptionPtr factoryIAPrefix6(uint16_t type,
+ OptionBufferConstIter begin,
+ OptionBufferConstIter end);
+
/// @brief Factory function to create option with integer value.
///
/// @param u universe (V4 or V6).
/// @param type option type.
+ /// @param encapsulated_space An option space being encapsulated by the
+ /// options created by this factory function. The options which belong to
+ /// encapsulated option space are sub options of this option.
/// @param begin iterator pointing to the beginning of the buffer.
/// @param end iterator pointing to the end of the buffer.
+ /// @param callback An instance of the function which parses packet options.
+ /// If this is set to non NULL value this function will be used instead of
+ /// @c isc::dhcp::LibDHCP::unpackOptions6 and
+ /// isc::dhcp::LibDHCP::unpackOptions4.
/// @tparam T type of the data field (must be one of the uintX_t or intX_t).
///
/// @throw isc::OutOfRange if provided option buffer length is invalid.
template<typename T>
static OptionPtr factoryInteger(Option::Universe u, uint16_t type,
+ const std::string& encapsulated_space,
OptionBufferConstIter begin,
- OptionBufferConstIter end) {
- OptionPtr option(new OptionInt<T>(u, type, begin, end));
+ OptionBufferConstIter end,
+ UnpackOptionsCallback callback) {
+ OptionPtr option(new OptionInt<T>(u, type, 0));
+ option->setEncapsulatedSpace(encapsulated_space);
+ option->setCallback(callback);
+ option->unpack(begin, end);
return (option);
}
@@ -449,6 +530,31 @@ public:
private:
+ /// @brief Creates an instance of an option having special format.
+ ///
+ /// The option with special formats are encapsulated by the dedicated
+ /// classes derived from @c Option class. In particular these are:
+ /// - IA_NA
+ /// - IAADDR
+ /// - FQDN
+ /// - VIVSO.
+ ///
+ /// @param u A universe (V4 or V6).
+ /// @param begin beginning of the option buffer.
+ /// @param end end of the option buffer.
+ /// @param callback An instance of the function which parses packet options.
+ /// If this is set to non NULL value this function will be used instead of
+ /// @c isc::dhcp::LibDHCP::unpackOptions6 and
+ /// isc::dhcp::LibDHCP::unpackOptions4.
+ ///
+ /// @return An instance of the option having special format or NULL if
+ /// such an option can't be created because an option with the given
+ /// option code hasn't got the special format.
+ OptionPtr factorySpecialFormatOption(Option::Universe u,
+ OptionBufferConstIter begin,
+ OptionBufferConstIter end,
+ UnpackOptionsCallback callback) const;
+
/// @brief Check if specified option format is a record with 3 fields
/// where first one is custom, and two others are uint32.
///
@@ -501,15 +607,6 @@ private:
void writeToBuffer(const std::string& value, const OptionDataType type,
OptionBuffer& buf) const;
- /// @brief Sanity check universe value.
- ///
- /// @param expected_universe expected universe value.
- /// @param actual_universe actual universe value.
- ///
- /// @throw isc::BadValue if expected universe and actual universe don't match.
- static inline void sanityCheckUniverse(const Option::Universe expected_universe,
- const Option::Universe actual_universe);
-
/// Option name.
std::string name_;
/// Option code.
@@ -567,6 +664,9 @@ typedef boost::multi_index_container<
/// Pointer to an option definition container.
typedef boost::shared_ptr<OptionDefContainer> OptionDefContainerPtr;
+/// Container that holds various vendor option containers
+typedef std::map<uint32_t, OptionDefContainer> VendorOptionDefContainers;
+
/// Type of the index #1 - option type.
typedef OptionDefContainer::nth_index<1>::type OptionDefContainerTypeIndex;
/// Pair of iterators to represent the range of options definitions
diff --git a/src/lib/dhcp/option_int.h b/src/lib/dhcp/option_int.h
index 9a4cfb1..cbdbcb0 100644
--- a/src/lib/dhcp/option_int.h
+++ b/src/lib/dhcp/option_int.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -47,11 +47,13 @@ public:
///
/// @throw isc::dhcp::InvalidDataType if data field type provided
/// as template parameter is not a supported integer type.
+ /// @todo Extend constructor to set encapsulated option space name.
OptionInt(Option::Universe u, uint16_t type, T value)
: Option(u, type), value_(value) {
if (!OptionDataTypeTraits<T>::integer_type) {
isc_throw(dhcp::InvalidDataType, "non-integer type");
}
+ setEncapsulatedSpace(u == Option::V4 ? "dhcp4" : "dhcp6");
}
/// @brief Constructor.
@@ -68,12 +70,14 @@ public:
/// @throw isc::OutOfRange if provided buffer is shorter than data size.
/// @throw isc::dhcp::InvalidDataType if data field type provided
/// as template parameter is not a supported integer type.
+ /// @todo Extend constructor to set encapsulated option space name.
OptionInt(Option::Universe u, uint16_t type, OptionBufferConstIter begin,
OptionBufferConstIter end)
: Option(u, type) {
if (!OptionDataTypeTraits<T>::integer_type) {
isc_throw(dhcp::InvalidDataType, "non-integer type");
}
+ setEncapsulatedSpace(u == Option::V4 ? "dhcp4" : "dhcp6");
unpack(begin, end);
}
@@ -175,7 +179,7 @@ public:
// The data length is equal to size of T.
length += sizeof(T);;
// length of all suboptions
- for (Option::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
diff --git a/src/lib/dhcp/option_int_array.h b/src/lib/dhcp/option_int_array.h
index a87e9b5..e0e9356 100644
--- a/src/lib/dhcp/option_int_array.h
+++ b/src/lib/dhcp/option_int_array.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -239,7 +239,7 @@ public:
uint16_t length = (getUniverse() == Option::V4) ? OPTION4_HDR_LEN : OPTION6_HDR_LEN;
length += values_.size() * sizeof(T);
// length of all suboptions
- for (Option::OptionCollection::iterator it = options_.begin();
+ for (OptionCollection::iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
diff --git a/src/lib/dhcp/option_string.cc b/src/lib/dhcp/option_string.cc
new file mode 100644
index 0000000..6a8001a
--- /dev/null
+++ b/src/lib/dhcp/option_string.cc
@@ -0,0 +1,86 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/option_string.h>
+
+namespace isc {
+namespace dhcp {
+
+OptionString::OptionString(const Option::Universe u, const uint16_t type,
+ const std::string& value)
+ : Option(u, type) {
+ // Try to assign the provided string value. This will throw exception
+ // if the provided value is empty.
+ setValue(value);
+}
+
+OptionString::OptionString(const Option::Universe u, const uint16_t type,
+ OptionBufferConstIter begin,
+ OptionBufferConstIter end)
+ : Option(u, type) {
+ // Decode the data. This will throw exception if the buffer is
+ // truncated.
+ unpack(begin, end);
+}
+
+std::string
+OptionString::getValue() const {
+ const OptionBuffer& data = getData();
+ return (std::string(data.begin(), data.end()));
+}
+
+void
+OptionString::setValue(const std::string& value) {
+ // Sanity check that the string value is at least one byte long.
+ // This is a requirement for all currently defined options which
+ // carry a string value.
+ if (value.empty()) {
+ isc_throw(isc::OutOfRange, "string value carried by the option '"
+ << getType() << "' must not be empty");
+ }
+
+ setData(value.begin(), value.end());
+}
+
+
+uint16_t
+OptionString::len() {
+ return (getHeaderLen() + getData().size());
+}
+
+void
+OptionString::pack(isc::util::OutputBuffer& buf) {
+ // Pack option header.
+ packHeader(buf);
+ // Pack data.
+ const OptionBuffer& data = getData();
+ buf.writeData(&data[0], data.size());
+
+ // That's it. We don't pack any sub-options here, because this option
+ // must not contain sub-options.
+}
+
+void
+OptionString::unpack(OptionBufferConstIter begin,
+ OptionBufferConstIter end) {
+ if (std::distance(begin, end) == 0) {
+ isc_throw(isc::OutOfRange, "failed to parse an option '"
+ << getType() << "' holding string value"
+ << " - empty value is not accepted");
+ }
+ setData(begin, end);
+}
+
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/option_string.h b/src/lib/dhcp/option_string.h
new file mode 100644
index 0000000..3d0aa9a
--- /dev/null
+++ b/src/lib/dhcp/option_string.h
@@ -0,0 +1,114 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION_STRING_H
+#define OPTION_STRING_H
+
+#include <dhcp/option.h>
+#include <util/buffer.h>
+
+#include <boost/shared_ptr.hpp>
+#include <string>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Class which represents an option carrying a single string value.
+///
+/// This class represents an option carrying a single string value.
+/// Currently this class imposes that the minimal length of the carried
+/// string is 1.
+///
+/// @todo In the future this class may be extended with some more string
+/// content checks and encoding methods if required.
+class OptionString : public Option {
+public:
+
+ /// @brief Constructor, used to create options to be sent.
+ ///
+ /// This constructor creates an instance of option which carries a
+ /// string value specified as constructor's parameter. This constructor
+ /// is most often used to create an instance of an option which will
+ /// be sent in the outgoing packet.
+ ///
+ /// @param u universe (V4 or V6).
+ /// @param type option code.
+ /// @param value a string value to be carried by the option.
+ ///
+ /// @throw isc::OutOfRange if provided string is empty.
+ OptionString(const Option::Universe u, const uint16_t type,
+ const std::string& value);
+
+ /// @brief Constructor, used for receiving options.
+ ///
+ /// This constructor creates an instance of the option from the provided
+ /// chunk of buffer. This buffer may hold the data received on the wire.
+ ///
+ /// @param u universe (V4 or V6).
+ /// @param type option code.
+ /// @param begin iterator pointing to the first byte of the buffer chunk.
+ /// @param end iterator pointing to the last byte of the buffer chunk.
+ ///
+ /// @throw isc::OutOfRange if provided buffer is truncated.
+ OptionString(const Option::Universe u, const uint16_t type,
+ OptionBufferConstIter begin, OptionBufferConstIter end);
+
+ /// @brief Returns length of the whole option, including header.
+ ///
+ /// @return length of the whole option.
+ virtual uint16_t len();
+
+ /// @brief Returns the string value held by the option.
+ ///
+ /// @return string value held by the option.
+ std::string getValue() const;
+
+ /// @brief Sets the string value to be held by the option.
+ ///
+ /// @param value string value to be set.
+ ///
+ /// @throw isc::OutOfRange if a string value to be set is empty.
+ void setValue(const std::string& value);
+
+ /// @brief Creates on-wire format of the option.
+ ///
+ /// This function creates on-wire format of the option and appends it to
+ /// the data existing in the provided buffer. The internal buffer's pointer
+ /// is moved to the end of stored data.
+ ///
+ /// @param [out] buf output buffer where the option will be stored.
+ virtual void pack(isc::util::OutputBuffer& buf);
+
+ /// @brief Decodes option data from the provided buffer.
+ ///
+ /// This function decodes option data from the provided buffer. Note that
+ /// it does not decode the option code and length, so the iterators must
+ /// point to the begining and end of the option payload respectively.
+ /// The size of the decoded payload must be at least 1 byte.
+ ///
+ /// @param begin the iterator pointing to the option payload.
+ /// @param end the iterator pointing to the end of the option payload.
+ ///
+ /// @throw isc::OutOfRange if provided buffer is truncated.
+ virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end);
+
+};
+
+/// Pointer to the OptionString object.
+typedef boost::shared_ptr<OptionString> OptionStringPtr;
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // OPTION_STRING_H
diff --git a/src/lib/dhcp/option_vendor.cc b/src/lib/dhcp/option_vendor.cc
new file mode 100644
index 0000000..878b534
--- /dev/null
+++ b/src/lib/dhcp/option_vendor.cc
@@ -0,0 +1,85 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/option_vendor.h>
+
+using namespace isc::dhcp;
+
+OptionVendor::OptionVendor(Option::Universe u, const uint32_t vendor_id)
+ :Option(u, u==Option::V4?DHO_VIVSO_SUBOPTIONS:D6O_VENDOR_OPTS), vendor_id_(vendor_id) {
+}
+
+OptionVendor::OptionVendor(Option::Universe u, OptionBufferConstIter begin,
+ OptionBufferConstIter end)
+ :Option(u, u==Option::V4?DHO_VIVSO_SUBOPTIONS:D6O_VENDOR_OPTS), vendor_id_(0) {
+ unpack(begin, end);
+}
+
+
+void OptionVendor::pack(isc::util::OutputBuffer& buf) {
+ packHeader(buf);
+
+ // Store vendor-id
+ buf.writeUint32(vendor_id_);
+
+ // The format is slightly different for v4
+ if (universe_ == Option::V4) {
+ // Calculate and store data-len as follows:
+ // data-len = total option length - header length
+ // - enterprise id field length - data-len field size
+ buf.writeUint8(len() - getHeaderLen() -
+ sizeof(uint32_t) - sizeof(uint8_t));
+ }
+
+ packOptions(buf);
+}
+
+void OptionVendor::unpack(OptionBufferConstIter begin,
+ OptionBufferConstIter end) {
+ if (distance(begin, end) < sizeof(uint32_t)) {
+ isc_throw(OutOfRange, "Truncated vendor-specific information option"
+ << ", length=" << distance(begin, end));
+ }
+
+ vendor_id_ = isc::util::readUint32(&(*begin));
+
+ OptionBuffer vendor_buffer(begin +4, end);
+
+ if (universe_ == Option::V6) {
+ LibDHCP::unpackVendorOptions6(vendor_id_, vendor_buffer, options_);
+ } else {
+ LibDHCP::unpackVendorOptions4(vendor_id_, vendor_buffer, options_);
+ }
+}
+
+uint16_t OptionVendor::len() {
+ uint16_t length = getHeaderLen();
+
+ length += sizeof(uint32_t); // Vendor-id field
+
+ // Data-len field exists in DHCPv4 vendor options only
+ if (universe_ == Option::V4) {
+ length += sizeof(uint8_t); // data-len
+ }
+
+ // length of all suboptions
+ for (OptionCollection::iterator it = options_.begin();
+ it != options_.end();
+ ++it) {
+ length += (*it).second->len();
+ }
+ return (length);
+}
diff --git a/src/lib/dhcp/option_vendor.h b/src/lib/dhcp/option_vendor.h
new file mode 100644
index 0000000..bb8395c
--- /dev/null
+++ b/src/lib/dhcp/option_vendor.h
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef OPTION_VENDOR_H
+#define OPTION_VENDOR_H
+
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option.h>
+#include <dhcp/option_data_types.h>
+#include <util/io_utilities.h>
+
+#include <stdint.h>
+
+namespace isc {
+namespace dhcp {
+
+/// Indexes for fields in vendor-class (17) DHCPv6 option
+const int VENDOR_CLASS_ENTERPRISE_ID_INDEX = 0;
+const int VENDOR_CLASS_DATA_LEN_INDEX = 1;
+const int VENDOR_CLASS_STRING_INDEX = 2;
+
+/// @brief This class represents vendor-specific information option.
+///
+/// As specified in RFC3925, the option formatting is slightly different
+/// for DHCPv4 than DHCPv6. The DHCPv4 Option includes additional field
+/// holding vendor data length.
+class OptionVendor: public Option {
+public:
+ /// @brief Constructor.
+ ///
+ /// @param u universe (V4 or V6)
+ /// @param vendor_id vendor enterprise-id (unique 32 bit integer)
+ OptionVendor(Option::Universe u, const uint32_t vendor_id);
+
+ /// @brief Constructor.
+ ///
+ /// This constructor creates option from a buffer. This constructor
+ /// may throw exception if \ref unpack function throws during buffer
+ /// parsing.
+ ///
+ /// @param u universe (V4 or V6)
+ /// @param begin iterator to first byte of option data.
+ /// @param end iterator to end of option data (first byte after option end).
+ ///
+ /// @throw isc::OutOfRange if provided buffer is shorter than data size.
+ /// @todo Extend constructor to set encapsulated option space name.
+ OptionVendor(Option::Universe u, OptionBufferConstIter begin,
+ OptionBufferConstIter end);
+
+ /// @brief Writes option in wire-format to buf, returns pointer to first
+ /// unused byte after stored option.
+ ///
+ /// @param [out] buf buffer (option will be stored here)
+ virtual void pack(isc::util::OutputBuffer& buf);
+
+ /// @brief Parses received buffer
+ ///
+ /// Parses received buffer and returns offset to the first unused byte after
+ /// parsed option.
+ ///
+ /// @param begin iterator to first byte of option data
+ /// @param end iterator to end of option data (first byte after option end)
+ ///
+ /// @throw isc::OutOfRange if provided buffer is shorter than data size.
+ virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end);
+
+ /// @brief Sets enterprise identifier
+ ///
+ /// @param vendor_id vendor identifier
+ void setVendorId(const uint32_t vendor_id) { vendor_id_ = vendor_id; }
+
+ /// @brief Returns enterprise identifier
+ ///
+ /// @return enterprise identifier
+ uint32_t getVendorId() const { return (vendor_id_); }
+
+ /// @brief returns complete length of option
+ ///
+ /// Returns length of this option, including option header and suboptions
+ ///
+ /// @return length of this option
+ virtual uint16_t len();
+
+private:
+
+ uint32_t vendor_id_; ///< Enterprise-id
+};
+
+/// Pointer to a vendor option
+typedef boost::shared_ptr<OptionVendor> OptionVendorPtr;
+
+} // isc::dhcp namespace
+} // isc namespace
+
+#endif // OPTION_VENDOR_H
diff --git a/src/lib/dhcp/pkt4.cc b/src/lib/dhcp/pkt4.cc
index 0592807..b641a03 100644
--- a/src/lib/dhcp/pkt4.cc
+++ b/src/lib/dhcp/pkt4.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -33,7 +33,8 @@ namespace dhcp {
const IOAddress DEFAULT_ADDRESS("0.0.0.0");
Pkt4::Pkt4(uint8_t msg_type, uint32_t transid)
- :local_addr_(DEFAULT_ADDRESS),
+ :buffer_out_(DHCPV4_PKT_HDR_LEN),
+ local_addr_(DEFAULT_ADDRESS),
remote_addr_(DEFAULT_ADDRESS),
iface_(""),
ifindex_(0),
@@ -48,8 +49,7 @@ Pkt4::Pkt4(uint8_t msg_type, uint32_t transid)
ciaddr_(DEFAULT_ADDRESS),
yiaddr_(DEFAULT_ADDRESS),
siaddr_(DEFAULT_ADDRESS),
- giaddr_(DEFAULT_ADDRESS),
- bufferOut_(DHCPV4_PKT_HDR_LEN)
+ giaddr_(DEFAULT_ADDRESS)
{
memset(sname_, 0, MAX_SNAME_LEN);
memset(file_, 0, MAX_FILE_LEN);
@@ -58,7 +58,8 @@ Pkt4::Pkt4(uint8_t msg_type, uint32_t transid)
}
Pkt4::Pkt4(const uint8_t* data, size_t len)
- :local_addr_(DEFAULT_ADDRESS),
+ :buffer_out_(0), // not used, this is RX packet
+ local_addr_(DEFAULT_ADDRESS),
remote_addr_(DEFAULT_ADDRESS),
iface_(""),
ifindex_(0),
@@ -72,8 +73,7 @@ Pkt4::Pkt4(const uint8_t* data, size_t len)
ciaddr_(DEFAULT_ADDRESS),
yiaddr_(DEFAULT_ADDRESS),
siaddr_(DEFAULT_ADDRESS),
- giaddr_(DEFAULT_ADDRESS),
- bufferOut_(0) // not used, this is RX packet
+ giaddr_(DEFAULT_ADDRESS)
{
if (len < DHCPV4_PKT_HDR_LEN) {
isc_throw(OutOfRange, "Truncated DHCPv4 packet (len=" << len
@@ -93,7 +93,7 @@ Pkt4::len() {
size_t length = DHCPV4_PKT_HDR_LEN; // DHCPv4 header
// ... and sum of lengths of all options
- for (Option::OptionCollection::const_iterator it = options_.begin();
+ for (OptionCollection::const_iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
@@ -102,113 +102,131 @@ Pkt4::len() {
return (length);
}
-bool
+void
Pkt4::pack() {
if (!hwaddr_) {
isc_throw(InvalidOperation, "Can't build Pkt4 packet. HWAddr not set.");
}
- size_t hw_len = hwaddr_->hwaddr_.size();
-
- bufferOut_.writeUint8(op_);
- bufferOut_.writeUint8(hwaddr_->htype_);
- bufferOut_.writeUint8(hw_len < MAX_CHADDR_LEN ? hw_len : MAX_CHADDR_LEN);
- bufferOut_.writeUint8(hops_);
- bufferOut_.writeUint32(transid_);
- bufferOut_.writeUint16(secs_);
- bufferOut_.writeUint16(flags_);
- bufferOut_.writeUint32(ciaddr_);
- bufferOut_.writeUint32(yiaddr_);
- bufferOut_.writeUint32(siaddr_);
- bufferOut_.writeUint32(giaddr_);
-
-
- if (hw_len <= MAX_CHADDR_LEN) {
- // write up to 16 bytes of the hardware address (CHADDR field is 16
- // bytes long in DHCPv4 message).
- bufferOut_.writeData(&hwaddr_->hwaddr_[0],
- (hw_len < MAX_CHADDR_LEN ? hw_len : MAX_CHADDR_LEN) );
- hw_len = MAX_CHADDR_LEN - hw_len;
- } else {
- hw_len = MAX_CHADDR_LEN;
+ // Clear the output buffer to make sure that consecutive calls to pack()
+ // will not result in concatenation of multiple packet copies.
+ buffer_out_.clear();
+
+ try {
+ size_t hw_len = hwaddr_->hwaddr_.size();
+
+ buffer_out_.writeUint8(op_);
+ buffer_out_.writeUint8(hwaddr_->htype_);
+ buffer_out_.writeUint8(hw_len < MAX_CHADDR_LEN ?
+ hw_len : MAX_CHADDR_LEN);
+ buffer_out_.writeUint8(hops_);
+ buffer_out_.writeUint32(transid_);
+ buffer_out_.writeUint16(secs_);
+ buffer_out_.writeUint16(flags_);
+ buffer_out_.writeUint32(ciaddr_);
+ buffer_out_.writeUint32(yiaddr_);
+ buffer_out_.writeUint32(siaddr_);
+ buffer_out_.writeUint32(giaddr_);
+
+
+ if (hw_len <= MAX_CHADDR_LEN) {
+ // write up to 16 bytes of the hardware address (CHADDR field is 16
+ // bytes long in DHCPv4 message).
+ buffer_out_.writeData(&hwaddr_->hwaddr_[0],
+ (hw_len < MAX_CHADDR_LEN ?
+ hw_len : MAX_CHADDR_LEN) );
+ hw_len = MAX_CHADDR_LEN - hw_len;
+ } else {
+ hw_len = MAX_CHADDR_LEN;
+ }
+
+ // write (len) bytes of padding
+ vector<uint8_t> zeros(hw_len, 0);
+ buffer_out_.writeData(&zeros[0], hw_len);
+ // buffer_out_.writeData(chaddr_, MAX_CHADDR_LEN);
+
+ buffer_out_.writeData(sname_, MAX_SNAME_LEN);
+ buffer_out_.writeData(file_, MAX_FILE_LEN);
+
+ // write DHCP magic cookie
+ buffer_out_.writeUint32(DHCP_OPTIONS_COOKIE);
+
+ LibDHCP::packOptions(buffer_out_, options_);
+
+ // add END option that indicates end of options
+ // (End option is very simple, just a 255 octet)
+ buffer_out_.writeUint8(DHO_END);
+ } catch(const Exception& e) {
+ // An exception is thrown and message will be written to Logger
+ isc_throw(InvalidOperation, e.what());
}
-
- // write (len) bytes of padding
- vector<uint8_t> zeros(hw_len, 0);
- bufferOut_.writeData(&zeros[0], hw_len);
- // bufferOut_.writeData(chaddr_, MAX_CHADDR_LEN);
-
- bufferOut_.writeData(sname_, MAX_SNAME_LEN);
- bufferOut_.writeData(file_, MAX_FILE_LEN);
-
- // write DHCP magic cookie
- bufferOut_.writeUint32(DHCP_OPTIONS_COOKIE);
-
- LibDHCP::packOptions(bufferOut_, options_);
-
- // add END option that indicates end of options
- // (End option is very simple, just a 255 octet)
- bufferOut_.writeUint8(DHO_END);
-
- return (true);
}
void
Pkt4::unpack() {
// input buffer (used during message reception)
- isc::util::InputBuffer bufferIn(&data_[0], data_.size());
+ isc::util::InputBuffer buffer_in(&data_[0], data_.size());
- if (bufferIn.getLength() < DHCPV4_PKT_HDR_LEN) {
+ if (buffer_in.getLength() < DHCPV4_PKT_HDR_LEN) {
isc_throw(OutOfRange, "Received truncated DHCPv4 packet (len="
- << bufferIn.getLength() << " received, at least "
+ << buffer_in.getLength() << " received, at least "
<< DHCPV4_PKT_HDR_LEN << "is expected");
}
- op_ = bufferIn.readUint8();
- uint8_t htype = bufferIn.readUint8();
- uint8_t hlen = bufferIn.readUint8();
- hops_ = bufferIn.readUint8();
- transid_ = bufferIn.readUint32();
- secs_ = bufferIn.readUint16();
- flags_ = bufferIn.readUint16();
- ciaddr_ = IOAddress(bufferIn.readUint32());
- yiaddr_ = IOAddress(bufferIn.readUint32());
- siaddr_ = IOAddress(bufferIn.readUint32());
- giaddr_ = IOAddress(bufferIn.readUint32());
+ op_ = buffer_in.readUint8();
+ uint8_t htype = buffer_in.readUint8();
+ uint8_t hlen = buffer_in.readUint8();
+ hops_ = buffer_in.readUint8();
+ transid_ = buffer_in.readUint32();
+ secs_ = buffer_in.readUint16();
+ flags_ = buffer_in.readUint16();
+ ciaddr_ = IOAddress(buffer_in.readUint32());
+ yiaddr_ = IOAddress(buffer_in.readUint32());
+ siaddr_ = IOAddress(buffer_in.readUint32());
+ giaddr_ = IOAddress(buffer_in.readUint32());
vector<uint8_t> hw_addr(MAX_CHADDR_LEN, 0);
- bufferIn.readVector(hw_addr, MAX_CHADDR_LEN);
- bufferIn.readData(sname_, MAX_SNAME_LEN);
- bufferIn.readData(file_, MAX_FILE_LEN);
+ buffer_in.readVector(hw_addr, MAX_CHADDR_LEN);
+ buffer_in.readData(sname_, MAX_SNAME_LEN);
+ buffer_in.readData(file_, MAX_FILE_LEN);
hw_addr.resize(hlen);
hwaddr_ = HWAddrPtr(new HWAddr(hw_addr, htype));
- if (bufferIn.getLength() == bufferIn.getPosition()) {
+ if (buffer_in.getLength() == buffer_in.getPosition()) {
// this is *NOT* DHCP packet. It does not have any DHCPv4 options. In
// particular, it does not have magic cookie, a 4 byte sequence that
// differentiates between DHCP and BOOTP packets.
isc_throw(InvalidOperation, "Received BOOTP packet. BOOTP is not supported.");
}
- if (bufferIn.getLength() - bufferIn.getPosition() < 4) {
+ if (buffer_in.getLength() - buffer_in.getPosition() < 4) {
// there is not enough data to hold magic DHCP cookie
isc_throw(Unexpected, "Truncated or no DHCP packet.");
}
- uint32_t magic = bufferIn.readUint32();
+ uint32_t magic = buffer_in.readUint32();
if (magic != DHCP_OPTIONS_COOKIE) {
isc_throw(Unexpected, "Invalid or missing DHCP magic cookie");
}
- size_t opts_len = bufferIn.getLength() - bufferIn.getPosition();
- vector<uint8_t> optsBuffer;
+ size_t opts_len = buffer_in.getLength() - buffer_in.getPosition();
+ vector<uint8_t> opts_buffer;
- // First use of readVector.
- bufferIn.readVector(optsBuffer, opts_len);
- LibDHCP::unpackOptions4(optsBuffer, options_);
+ // Use readVector because a function which parses option requires
+ // a vector as an input.
+ buffer_in.readVector(opts_buffer, opts_len);
+ if (callback_.empty()) {
+ LibDHCP::unpackOptions4(opts_buffer, "dhcp4", options_);
+ } else {
+ // The last two arguments are set to NULL because they are
+ // specific to DHCPv6 options parsing. They are unused for
+ // DHCPv4 case. In DHCPv6 case they hold are the relay message
+ // offset and length.
+ callback_(opts_buffer, "dhcp4", options_, NULL, NULL);
+ }
// @todo check will need to be called separately, so hooks can be called
// after the packet is parsed, but before its content is verified
@@ -254,18 +272,18 @@ void Pkt4::setType(uint8_t dhcp_type) {
}
void Pkt4::repack() {
- bufferOut_.writeData(&data_[0], data_.size());
+ buffer_out_.writeData(&data_[0], data_.size());
}
std::string
Pkt4::toText() {
stringstream tmp;
- tmp << "localAddr=" << local_addr_.toText() << ":" << local_port_
- << " remoteAddr=" << remote_addr_.toText()
+ tmp << "localAddr=" << local_addr_ << ":" << local_port_
+ << " remoteAddr=" << remote_addr_
<< ":" << remote_port_ << ", msgtype=" << static_cast<int>(getType())
<< ", transid=0x" << hex << transid_ << dec << endl;
- for (isc::dhcp::Option::OptionCollection::iterator opt=options_.begin();
+ for (isc::dhcp::OptionCollection::iterator opt=options_.begin();
opt != options_.end();
++opt) {
tmp << " " << opt->second->toText() << std::endl;
@@ -276,8 +294,24 @@ Pkt4::toText() {
}
void
-Pkt4::setHWAddr(uint8_t hType, uint8_t hlen,
+Pkt4::setHWAddr(uint8_t htype, uint8_t hlen,
const std::vector<uint8_t>& mac_addr) {
+ setHWAddrMember(htype, hlen, mac_addr, hwaddr_);
+}
+
+void
+Pkt4::setHWAddr(const HWAddrPtr& addr) {
+ if (!addr) {
+ isc_throw(BadValue, "Setting DHCPv4 chaddr field to NULL"
+ << " is forbidden");
+ }
+ hwaddr_ = addr;
+}
+
+void
+Pkt4::setHWAddrMember(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr,
+ HWAddrPtr& hw_addr) {
/// @todo Rewrite this once support for client-identifier option
/// is implemented (ticket 1228?)
if (hlen > MAX_CHADDR_LEN) {
@@ -288,15 +322,37 @@ Pkt4::setHWAddr(uint8_t hType, uint8_t hlen,
isc_throw(OutOfRange, "Invalid HW Address specified");
}
- hwaddr_.reset(new HWAddr(mac_addr, hType));
+ hw_addr.reset(new HWAddr(mac_addr, htype));
}
void
-Pkt4::setHWAddr(const HWAddrPtr& addr) {
+Pkt4::setLocalHWAddr(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr) {
+ setHWAddrMember(htype, hlen, mac_addr, local_hwaddr_);
+}
+
+void
+Pkt4::setLocalHWAddr(const HWAddrPtr& addr) {
if (!addr) {
- isc_throw(BadValue, "Setting hw address to NULL is forbidden");
+ isc_throw(BadValue, "Setting local HW address to NULL is"
+ << " forbidden.");
}
- hwaddr_ = addr;
+ local_hwaddr_ = addr;
+}
+
+void
+Pkt4::setRemoteHWAddr(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr) {
+ setHWAddrMember(htype, hlen, mac_addr, remote_hwaddr_);
+}
+
+void
+Pkt4::setRemoteHWAddr(const HWAddrPtr& addr) {
+ if (!addr) {
+ isc_throw(BadValue, "Setting remote HW address to NULL is"
+ << " forbidden.");
+ }
+ remote_hwaddr_ = addr;
}
void
@@ -385,7 +441,7 @@ Pkt4::addOption(boost::shared_ptr<Option> opt) {
boost::shared_ptr<isc::dhcp::Option>
Pkt4::getOption(uint8_t type) const {
- Option::OptionCollection::const_iterator x = options_.find(type);
+ OptionCollection::const_iterator x = options_.find(type);
if (x != options_.end()) {
return (*x).second;
}
@@ -394,7 +450,7 @@ Pkt4::getOption(uint8_t type) const {
bool
Pkt4::delOption(uint8_t type) {
- isc::dhcp::Option::OptionCollection::iterator x = options_.find(type);
+ isc::dhcp::OptionCollection::iterator x = options_.find(type);
if (x != options_.end()) {
options_.erase(x);
return (true); // delete successful
@@ -407,6 +463,36 @@ Pkt4::updateTimestamp() {
timestamp_ = boost::posix_time::microsec_clock::universal_time();
}
+bool
+Pkt4::isRelayed() const {
+ static const IOAddress zero_addr("0.0.0.0");
+ // For non-relayed message both Giaddr and Hops are zero.
+ if (getGiaddr() == zero_addr && getHops() == 0) {
+ return (false);
+
+ // For relayed message, both Giaddr and Hops are non-zero.
+ } else if (getGiaddr() != zero_addr && getHops() > 0) {
+ return (true);
+ }
+ // In any other case, the packet is considered malformed.
+ isc_throw(isc::BadValue, "invalid combination of giaddr = "
+ << getGiaddr().toText() << " and hops = "
+ << static_cast<int>(getHops()) << ". Valid values"
+ " are: (giaddr = 0 and hops = 0) or (giaddr != 0 and"
+ "hops != 0)");
+}
+
+bool Pkt4::inClass(const std::string& client_class) {
+ return (classes_.find(client_class) != classes_.end());
+}
+
+void
+Pkt4::addClass(const std::string& client_class) {
+ if (classes_.find(client_class) == classes_.end()) {
+ classes_.insert(client_class);
+ }
+}
+
} // end of namespace isc::dhcp
} // end of namespace isc
diff --git a/src/lib/dhcp/pkt4.h b/src/lib/dhcp/pkt4.h
index e7f33c5..485d28a 100644
--- a/src/lib/dhcp/pkt4.h
+++ b/src/lib/dhcp/pkt4.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,6 +16,7 @@
#define PKT4_H
#include <asiolink/io_address.h>
+#include <dhcp/option.h>
#include <util/buffer.h>
#include <dhcp/option.h>
#include <dhcp/hwaddr.h>
@@ -25,6 +26,7 @@
#include <iostream>
#include <vector>
+#include <set>
#include <time.h>
@@ -47,6 +49,13 @@ public:
/// specifies DHCPv4 packet header length (fixed part)
const static size_t DHCPV4_PKT_HDR_LEN = 236;
+ /// Mask for the value of flags field in the DHCPv4 message
+ /// to check whether client requested broadcast response.
+ const static uint16_t FLAG_BROADCAST_MASK = 0x8000;
+
+ /// Container for storing client classes
+ typedef std::set<std::string> Classes;
+
/// Constructor, used in replying to a message.
///
/// @param msg_type type of message (e.g. DHCPDISOVER=1)
@@ -66,10 +75,11 @@ public:
///
/// Prepares on-wire format of message and all its options.
/// Options must be stored in options_ field.
- /// Output buffer will be stored in bufferOut_.
+ /// Output buffer will be stored in buffer_out_.
+ /// The buffer_out_ is cleared before writting to the buffer.
///
- /// @return true if packing procedure was successful
- bool
+ /// @throw InvalidOperation if packing fails
+ void
pack();
/// @brief Parses on-wire form of DHCPv4 packet.
@@ -99,7 +109,7 @@ public:
///
/// This is mostly a diagnostic function. It is being used for sending
/// received packet. Received packet is stored in bufferIn_, but
- /// transmitted data is stored in bufferOut_. If we want to send packet
+ /// transmitted data is stored in buffer_out_. If we want to send packet
/// that we just received, a copy between those two buffers is necessary.
void repack();
@@ -267,10 +277,10 @@ public:
///
/// Note: mac_addr must be a buffer of at least hlen bytes.
///
- /// @param hType hardware type (will be sent in htype field)
+ /// @param htype hardware type (will be sent in htype field)
/// @param hlen hardware length (will be sent in hlen field)
/// @param mac_addr pointer to hardware address
- void setHWAddr(uint8_t hType, uint8_t hlen,
+ void setHWAddr(uint8_t htype, uint8_t hlen,
const std::vector<uint8_t>& mac_addr);
/// @brief Sets hardware address
@@ -303,11 +313,12 @@ public:
/// is only valid till Pkt4 object is valid.
///
/// RX packet or TX packet before pack() will return buffer with
- /// zero length
+ /// zero length. This buffer is returned as non-const, so hooks
+ /// framework (and user's callouts) can modify them if needed
///
/// @return reference to output buffer
- const isc::util::OutputBuffer&
- getBuffer() const { return (bufferOut_); };
+ isc::util::OutputBuffer&
+ getBuffer() { return (buffer_out_); };
/// @brief Add an option.
///
@@ -363,6 +374,72 @@ public:
/// @return interface index
uint32_t getIndex() const { return (ifindex_); };
+ /// @brief Sets remote HW address.
+ ///
+ /// Sets the destination HW address for the outgoing packet
+ /// or source HW address for the incoming packet. When this
+ /// is an outgoing packet this address will be used to construct
+ /// the link layer header.
+ ///
+ /// @note mac_addr must be a buffer of at least hlen bytes.
+ ///
+ /// @param htype hardware type (will be sent in htype field)
+ /// @param hlen hardware length (will be sent in hlen field)
+ /// @param mac_addr pointer to hardware address
+ void setRemoteHWAddr(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr);
+
+ /// @brief Sets remote HW address.
+ ///
+ /// Sets hardware address from an existing HWAddr structure.
+ /// The remote address is a destination address for outgoing
+ /// packet and source address for incoming packet. When this
+ /// is an outgoing packet, this address will be used to
+ /// construct the link layer header.
+ ///
+ /// @param addr structure representing HW address.
+ ///
+ /// @throw BadValue if addr is null
+ void setRemoteHWAddr(const HWAddrPtr& addr);
+
+ /// @brief Returns the remote HW address.
+ ///
+ /// @return remote HW address.
+ HWAddrPtr getRemoteHWAddr() const {
+ return (remote_hwaddr_);
+ }
+
+ /// @brief Sets local HW address.
+ ///
+ /// Sets the source HW address for the outgoing packet or
+ /// destination HW address for the incoming packet.
+ ///
+ /// @note mac_addr must be a buffer of at least hlen bytes.
+ ///
+ /// @param htype hardware type (will be sent in htype field)
+ /// @param hlen hardware length (will be sent in hlen field)
+ /// @param mac_addr pointer to hardware address
+ void setLocalHWAddr(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr);
+
+ /// @brief Sets local HW address.
+ ///
+ /// Sets hardware address from an existing HWAddr structure.
+ /// The local address is a source address for outgoing
+ /// packet and destination address for incoming packet.
+ ///
+ /// @param addr structure representing HW address.
+ ///
+ /// @throw BadValue if addr is null
+ void setLocalHWAddr(const HWAddrPtr& addr);
+
+ /// @brief Returns local HW address.
+ ///
+ /// @return local HW addr.
+ HWAddrPtr getLocalHWAddr() const {
+ return (local_hwaddr_);
+ }
+
/// @brief Sets remote address.
///
/// @param remote specifies remote address
@@ -411,6 +488,32 @@ public:
/// @return remote port
uint16_t getRemotePort() const { return (remote_port_); }
+ /// @brief Checks if a DHCPv4 message has been relayed.
+ ///
+ /// This function returns a boolean value which indicates whether a DHCPv4
+ /// message has been relayed (if true is returned) or not (if false).
+ ///
+ /// This function uses a combination of Giaddr and Hops. It is expected that
+ /// if Giaddr is not 0, the Hops is greater than 0. In this case the message
+ /// is considered relayed. If Giaddr is 0, the Hops value must also be 0. In
+ /// this case the message is considered non-relayed. For any other
+ /// combination of Giaddr and Hops, an exception is thrown to indicate that
+ /// the message is malformed.
+ ///
+ /// @return Boolean value which indicates whether the message is relayed
+ /// (true) or non-relayed (false).
+ /// @throw isc::BadValue if invalid combination of Giaddr and Hops values is
+ /// found.
+ bool isRelayed() const;
+
+ /// @brief Set callback function to be used to parse options.
+ ///
+ /// @param callback An instance of the callback function or NULL to
+ /// uninstall callback.
+ void setCallback(UnpackOptionsCallback callback) {
+ callback_ = callback;
+ }
+
/// @brief Update packet timestamp.
///
/// Updates packet timestamp. This method is invoked
@@ -419,6 +522,86 @@ public:
/// @throw isc::Unexpected if timestamp update failed
void updateTimestamp();
+ /// Output buffer (used during message transmission)
+ ///
+ /// @warning This public member is accessed by derived
+ /// classes directly. One of such derived classes is
+ /// @ref perfdhcp::PerfPkt4. The impact on derived clasess'
+ /// behavior must be taken into consideration before making
+ /// changes to this member such as access scope restriction or
+ /// data format change etc. This field is also public, because
+ /// it may be modified by callouts (which are written in C++ now,
+ /// but we expect to also have them in Python, so any accesibility
+ /// methods would overly complicate things here and degrade
+ /// performance).
+ isc::util::OutputBuffer buffer_out_;
+
+ /// @brief That's the data of input buffer used in RX packet.
+ ///
+ /// @note Note that InputBuffer does not store the data itself, but just
+ /// expects that data will be valid for the whole life of InputBuffer.
+ /// Therefore we need to keep the data around.
+ ///
+ /// @warning This public member is accessed by derived
+ /// classes directly. One of such derived classes is
+ /// @ref perfdhcp::PerfPkt4. The impact on derived clasess'
+ /// behavior must be taken into consideration before making
+ /// changes to this member such as access scope restriction or
+ /// data format change etc. This field is also public, because
+ /// it may be modified by callouts (which are written in C++ now,
+ /// but we expect to also have them in Python, so any accesibility
+ /// methods would overly complicate things here and degrade
+ /// performance).
+ std::vector<uint8_t> data_;
+
+ /// @brief Checks whether a client belongs to a given class
+ ///
+ /// @param client_class name of the class
+ /// @return true if belongs
+ bool inClass(const std::string& client_class);
+
+ /// @brief Adds packet to a specified class
+ ///
+ /// A packet can be added to the same class repeatedly. Any additional
+ /// attempts to add to a class the packet already belongs to, will be
+ /// ignored silently.
+ ///
+ /// @note It is a matter of naming convention. Conceptually, the server
+ /// processes a stream of packets, with some packets belonging to given
+ /// classes. From that perspective, this method adds a packet to specifed
+ /// class. Implementation wise, it looks the opposite - the class name
+ /// is added to the packet. Perhaps the most appropriate name for this
+ /// method would be associateWithClass()? But that seems overly long,
+ /// so I decided to stick with addClass().
+ ///
+ /// @param client_class name of the class to be added
+ void addClass(const std::string& client_class);
+
+ /// @brief Classes this packet belongs to.
+ ///
+ /// This field is public, so the code outside of Pkt4 class can iterate over
+ /// existing classes. Having it public also solves the problem of returned
+ /// reference lifetime. It is preferred to use @ref inClass and @ref addClass
+ /// should be used to operate on this field.
+ Classes classes_;
+
+private:
+
+ /// @brief Generic method that validates and sets HW address.
+ ///
+ /// This is a generic method used by all modifiers of this class
+ /// which set class members representing HW address.
+ ///
+ /// @param htype hardware type.
+ /// @param hlen hardware length.
+ /// @param mac_addr pointer to actual hardware address.
+ /// @param [out] hw_addr pointer to a class member to be modified.
+ ///
+ /// @trow isc::OutOfRange if invalid HW address specified.
+ void setHWAddrMember(const uint8_t htype, const uint8_t hlen,
+ const std::vector<uint8_t>& mac_addr,
+ HWAddrPtr& hw_addr);
+
protected:
/// converts DHCP message type to BOOTP op type
@@ -429,6 +612,12 @@ protected:
uint8_t
DHCPTypeToBootpType(uint8_t dhcpType);
+ /// local HW address (dst if receiving packet, src if sending packet)
+ HWAddrPtr local_hwaddr_;
+
+ // remote HW address (src if receiving packet, dst if sending packet)
+ HWAddrPtr remote_hwaddr_;
+
/// local address (dst if receiving packet, src if sending packet)
isc::asiolink::IOAddress local_addr_;
@@ -498,29 +687,6 @@ protected:
// end of real DHCPv4 fields
- /// output buffer (used during message transmission)
- ///
- /// @warning This protected member is accessed by derived
- /// classes directly. One of such derived classes is
- /// @ref perfdhcp::PerfPkt4. The impact on derived clasess'
- /// behavior must be taken into consideration before making
- /// changes to this member such as access scope restriction or
- /// data format change etc.
- isc::util::OutputBuffer bufferOut_;
-
- /// that's the data of input buffer used in RX packet. Note that
- /// InputBuffer does not store the data itself, but just expects that
- /// data will be valid for the whole life of InputBuffer. Therefore we
- /// need to keep the data around.
- ///
- /// @warning This protected member is accessed by derived
- /// classes directly. One of such derived classes is
- /// @ref perfdhcp::PerfPkt4. The impact on derived clasess'
- /// behavior must be taken into consideration before making
- /// changes to this member such as access scope restriction or
- /// data format change etc.
- std::vector<uint8_t> data_;
-
/// collection of options present in this message
///
/// @warning This protected member is accessed by derived
@@ -529,10 +695,14 @@ protected:
/// behavior must be taken into consideration before making
/// changes to this member such as access scope restriction or
/// data format change etc.
- isc::dhcp::Option::OptionCollection options_;
+ isc::dhcp::OptionCollection options_;
/// packet timestamp
boost::posix_time::ptime timestamp_;
+
+ /// A callback to be called to unpack options from the packet.
+ UnpackOptionsCallback callback_;
+
}; // Pkt4 class
typedef boost::shared_ptr<Pkt4> Pkt4Ptr;
diff --git a/src/lib/dhcp/pkt6.cc b/src/lib/dhcp/pkt6.cc
index c97281e..fdbd18c 100644
--- a/src/lib/dhcp/pkt6.cc
+++ b/src/lib/dhcp/pkt6.cc
@@ -14,6 +14,7 @@
#include <dhcp/dhcp6.h>
#include <dhcp/libdhcp++.h>
+#include <dhcp/option.h>
#include <dhcp/pkt6.h>
#include <exceptions/exceptions.h>
@@ -42,7 +43,7 @@ Pkt6::Pkt6(const uint8_t* buf, uint32_t buf_len, DHCPv6Proto proto /* = UDP */)
remote_addr_("::"),
local_port_(0),
remote_port_(0),
- bufferOut_(0) {
+ buffer_out_(0) {
data_.resize(buf_len);
memcpy(&data_[0], buf, buf_len);
}
@@ -57,7 +58,7 @@ Pkt6::Pkt6(uint8_t msg_type, uint32_t transid, DHCPv6Proto proto /*= UDP*/) :
remote_addr_("::"),
local_port_(0),
remote_port_(0),
- bufferOut_(0) {
+ buffer_out_(0) {
}
uint16_t Pkt6::len() {
@@ -72,13 +73,69 @@ uint16_t Pkt6::len() {
}
}
+OptionPtr Pkt6::getAnyRelayOption(uint16_t opt_type, RelaySearchOrder order) {
+
+ if (relay_info_.empty()) {
+ // There's no relay info, this is a direct message
+ return (OptionPtr());
+ }
+
+ int start = 0; // First relay to check
+ int end = 0; // Last relay to check
+ int direction = 0; // How we going to iterate: forward or backward?
+
+ switch (order) {
+ case RELAY_SEARCH_FROM_CLIENT:
+ // Search backwards
+ start = relay_info_.size() - 1;
+ end = 0;
+ direction = -1;
+ break;
+ case RELAY_SEARCH_FROM_SERVER:
+ // Search forward
+ start = 0;
+ end = relay_info_.size() - 1;
+ direction = 1;
+ break;
+ case RELAY_GET_FIRST:
+ // Look at the innermost relay only
+ start = relay_info_.size() - 1;
+ end = start;
+ direction = 1;
+ break;
+ case RELAY_GET_LAST:
+ // Look at the outermost relay only
+ start = 0;
+ end = 0;
+ direction = 1;
+ }
+
+ // This is a tricky loop. It must go from start to end, but it must work in
+ // both directions (start > end; or start < end). We can't use regular
+ // exit condition, because we don't know whether to use i <= end or i >= end.
+ // That's why we check if in the next iteration we would go past the
+ // list (end + direction). It is similar to STL concept of end pointing
+ // to a place after the last element
+ for (int i = start; i != end + direction; i += direction) {
+ OptionPtr opt = getRelayOption(opt_type, i);
+ if (opt) {
+ return (opt);
+ }
+ }
+
+ // We iterated over specified relays and haven't found what we were
+ // looking for
+ return (OptionPtr());
+}
+
+
OptionPtr Pkt6::getRelayOption(uint16_t opt_type, uint8_t relay_level) {
if (relay_level >= relay_info_.size()) {
isc_throw(OutOfRange, "This message was relayed " << relay_info_.size() << " time(s)."
<< " There is no info about " << relay_level + 1 << " relay.");
}
- for (Option::OptionCollection::iterator it = relay_info_[relay_level].options_.begin();
+ for (OptionCollection::iterator it = relay_info_[relay_level].options_.begin();
it != relay_info_[relay_level].options_.end(); ++it) {
if ((*it).second->getType() == opt_type) {
return (it->second);
@@ -92,7 +149,7 @@ uint16_t Pkt6::getRelayOverhead(const RelayInfo& relay) const {
uint16_t len = DHCPV6_RELAY_HDR_LEN // fixed header
+ Option::OPTION6_HDR_LEN; // header of the relay-msg option
- for (Option::OptionCollection::const_iterator opt = relay.options_.begin();
+ for (OptionCollection::const_iterator opt = relay.options_.begin();
opt != relay.options_.end(); ++opt) {
len += (opt->second)->len();
}
@@ -115,7 +172,7 @@ uint16_t Pkt6::calculateRelaySizes() {
uint16_t Pkt6::directLen() const {
uint16_t length = DHCPV6_PKT_HDR_LEN; // DHCPv6 header
- for (Option::OptionCollection::const_iterator it = options_.begin();
+ for (OptionCollection::const_iterator it = options_.begin();
it != options_.end();
++it) {
length += (*it).second->len();
@@ -125,22 +182,25 @@ uint16_t Pkt6::directLen() const {
}
-bool
+void
Pkt6::pack() {
switch (proto_) {
case UDP:
- return packUDP();
+ packUDP();
+ break;
case TCP:
- return packTCP();
+ packTCP();
+ break;
default:
isc_throw(BadValue, "Invalid protocol specified (non-TCP, non-UDP)");
}
- return (false); // never happens
}
-bool
+void
Pkt6::packUDP() {
try {
+ // Make sure that the buffer is empty before we start writting to it.
+ buffer_out_.clear();
// is this a relayed packet?
if (!relay_info_.empty()) {
@@ -157,11 +217,11 @@ Pkt6::packUDP() {
relay != relay_info_.end(); ++relay) {
// build relay-forw/relay-repl header (see RFC3315, section 7)
- bufferOut_.writeUint8(relay->msg_type_);
- bufferOut_.writeUint8(relay->hop_count_);
- bufferOut_.writeData(&(relay->linkaddr_.toBytes()[0]),
+ buffer_out_.writeUint8(relay->msg_type_);
+ buffer_out_.writeUint8(relay->hop_count_);
+ buffer_out_.writeData(&(relay->linkaddr_.toBytes()[0]),
isc::asiolink::V6ADDRESS_LEN);
- bufferOut_.writeData(&relay->peeraddr_.toBytes()[0],
+ buffer_out_.writeData(&relay->peeraddr_.toBytes()[0],
isc::asiolink::V6ADDRESS_LEN);
// store every option in this relay scope. Usually that will be
@@ -169,43 +229,42 @@ Pkt6::packUDP() {
// present here as well (vendor-opts for Cable modems,
// subscriber-id, remote-id, options echoed back from Echo
// Request Option, etc.)
- for (Option::OptionCollection::const_iterator opt =
+ for (OptionCollection::const_iterator opt =
relay->options_.begin();
opt != relay->options_.end(); ++opt) {
- (opt->second)->pack(bufferOut_);
+ (opt->second)->pack(buffer_out_);
}
// and include header relay-msg option. Its payload will be
// generated in the next iteration (if there are more relays)
// or outside the loop (if there are no more relays and the
// payload is a direct message)
- bufferOut_.writeUint16(D6O_RELAY_MSG);
- bufferOut_.writeUint16(relay->relay_msg_len_);
+ buffer_out_.writeUint16(D6O_RELAY_MSG);
+ buffer_out_.writeUint16(relay->relay_msg_len_);
}
}
// DHCPv6 header: message-type (1 octect) + transaction id (3 octets)
- bufferOut_.writeUint8(msg_type_);
+ buffer_out_.writeUint8(msg_type_);
// store 3-octet transaction-id
- bufferOut_.writeUint8( (transid_ >> 16) & 0xff );
- bufferOut_.writeUint8( (transid_ >> 8) & 0xff );
- bufferOut_.writeUint8( (transid_) & 0xff );
+ buffer_out_.writeUint8( (transid_ >> 16) & 0xff );
+ buffer_out_.writeUint8( (transid_ >> 8) & 0xff );
+ buffer_out_.writeUint8( (transid_) & 0xff );
// the rest are options
- LibDHCP::packOptions(bufferOut_, options_);
+ LibDHCP::packOptions(buffer_out_, options_);
}
catch (const Exception& e) {
- /// @todo: throw exception here once we turn this function to void.
- return (false);
+ // An exception is thrown and message will be written to Logger
+ isc_throw(InvalidOperation, e.what());
}
- return (true);
}
-bool
+void
Pkt6::packTCP() {
/// TODO Implement this function.
- isc_throw(Unexpected, "DHCPv6 over TCP (bulk leasequery and failover)"
+ isc_throw(NotImplemented, "DHCPv6 over TCP (bulk leasequery and failover)"
"not implemented yet.");
}
@@ -268,7 +327,16 @@ Pkt6::unpackMsg(OptionBuffer::const_iterator begin,
try {
OptionBuffer opt_buffer(begin, end);
- LibDHCP::unpackOptions6(opt_buffer, options_);
+ // If custom option parsing function has been set, use this function
+ // to parse options. Otherwise, use standard function from libdhcp.
+ if (callback_.empty()) {
+ LibDHCP::unpackOptions6(opt_buffer, "dhcp6", options_);
+ } else {
+ // The last two arguments hold the DHCPv6 Relay message offset and
+ // length. Setting them to NULL because we are dealing with the
+ // not-relayed message.
+ callback_(opt_buffer, "dhcp6", options_, NULL, NULL);
+ }
} catch (const Exception& e) {
// @todo: throw exception here once we turn this function to void.
return (false);
@@ -305,8 +373,16 @@ Pkt6::unpackRelayMsg() {
try {
// parse the rest as options
OptionBuffer opt_buffer(&data_[offset], &data_[offset+bufsize]);
- LibDHCP::unpackOptions6(opt_buffer, relay.options_, &relay_msg_offset,
- &relay_msg_len);
+
+ // If custom option parsing function has been set, use this function
+ // to parse options. Otherwise, use standard function from libdhcp.
+ if (callback_.empty()) {
+ LibDHCP::unpackOptions6(opt_buffer, "dhcp6", relay.options_,
+ &relay_msg_offset, &relay_msg_len);
+ } else {
+ callback_(opt_buffer, "dhcp6", relay.options_,
+ &relay_msg_offset, &relay_msg_len);
+ }
/// @todo: check that each option appears at most once
//relay.interface_id_ = options->getOption(D6O_INTERFACE_ID);
@@ -377,12 +453,12 @@ Pkt6::unpackTCP() {
std::string
Pkt6::toText() {
stringstream tmp;
- tmp << "localAddr=[" << local_addr_.toText() << "]:" << local_port_
- << " remoteAddr=[" << remote_addr_.toText()
+ tmp << "localAddr=[" << local_addr_ << "]:" << local_port_
+ << " remoteAddr=[" << remote_addr_
<< "]:" << remote_port_ << endl;
tmp << "msgtype=" << static_cast<int>(msg_type_) << ", transid=0x" <<
hex << transid_ << dec << endl;
- for (isc::dhcp::Option::OptionCollection::iterator opt=options_.begin();
+ for (isc::dhcp::OptionCollection::iterator opt=options_.begin();
opt != options_.end();
++opt) {
tmp << opt->second->toText() << std::endl;
@@ -392,18 +468,18 @@ Pkt6::toText() {
OptionPtr
Pkt6::getOption(uint16_t opt_type) {
- isc::dhcp::Option::OptionCollection::const_iterator x = options_.find(opt_type);
+ isc::dhcp::OptionCollection::const_iterator x = options_.find(opt_type);
if (x!=options_.end()) {
return (*x).second;
}
return OptionPtr(); // NULL
}
-isc::dhcp::Option::OptionCollection
+isc::dhcp::OptionCollection
Pkt6::getOptions(uint16_t opt_type) {
- isc::dhcp::Option::OptionCollection found;
+ isc::dhcp::OptionCollection found;
- for (Option::OptionCollection::const_iterator x = options_.begin();
+ for (OptionCollection::const_iterator x = options_.begin();
x != options_.end(); ++x) {
if (x->first == opt_type) {
found.insert(make_pair(opt_type, x->second));
@@ -419,7 +495,7 @@ Pkt6::addOption(const OptionPtr& opt) {
bool
Pkt6::delOption(uint16_t type) {
- isc::dhcp::Option::OptionCollection::iterator x = options_.find(type);
+ isc::dhcp::OptionCollection::iterator x = options_.find(type);
if (x!=options_.end()) {
options_.erase(x);
return (true); // delete successful
@@ -428,7 +504,7 @@ Pkt6::delOption(uint16_t type) {
}
void Pkt6::repack() {
- bufferOut_.writeData(&data_[0], data_.size());
+ buffer_out_.writeData(&data_[0], data_.size());
}
void
@@ -483,5 +559,44 @@ const char* Pkt6::getName() const {
return (getName(getType()));
}
+void Pkt6::copyRelayInfo(const Pkt6Ptr& question) {
+
+ // We use index rather than iterator, because we need that as a parameter
+ // passed to getRelayOption()
+ for (int i = 0; i < question->relay_info_.size(); ++i) {
+ RelayInfo info;
+ info.msg_type_ = DHCPV6_RELAY_REPL;
+ info.hop_count_ = question->relay_info_[i].hop_count_;
+ info.linkaddr_ = question->relay_info_[i].linkaddr_;
+ info.peeraddr_ = question->relay_info_[i].peeraddr_;
+
+ // Is there an interface-id option in this nesting level?
+ // If there is, we need to echo it back
+ OptionPtr opt = question->getRelayOption(D6O_INTERFACE_ID, i);
+ // taken from question->RelayInfo_[i].options_
+ if (opt) {
+ info.options_.insert(make_pair(opt->getType(), opt));
+ }
+
+ /// @todo: Implement support for ERO (Echo Request Option, RFC4994)
+
+ // Add this relay-forw info (client's message) to our relay-repl
+ // message (server's response)
+ relay_info_.push_back(info);
+ }
+}
+
+bool
+Pkt6::inClass(const std::string& client_class) {
+ return (classes_.find(client_class) != classes_.end());
+}
+
+void
+Pkt6::addClass(const std::string& client_class) {
+ if (classes_.find(client_class) == classes_.end()) {
+ classes_.insert(client_class);
+ }
+}
+
} // end of isc::dhcp namespace
} // end of isc namespace
diff --git a/src/lib/dhcp/pkt6.h b/src/lib/dhcp/pkt6.h
index 0bf4192..db80fb9 100644
--- a/src/lib/dhcp/pkt6.h
+++ b/src/lib/dhcp/pkt6.h
@@ -23,6 +23,7 @@
#include <boost/shared_ptr.hpp>
#include <iostream>
+#include <set>
#include <time.h>
@@ -30,6 +31,9 @@ namespace isc {
namespace dhcp {
+class Pkt6;
+typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
+
class Pkt6 {
public:
/// specifies non-relayed DHCPv6 packet header length (over UDP)
@@ -44,6 +48,31 @@ public:
TCP = 1 // there are TCP DHCPv6 packets (bulk leasequery, failover)
};
+ /// Container for storing client classes
+ typedef std::set<std::string> Classes;
+
+ /// @brief defines relay search pattern
+ ///
+ /// Defines order in which options are searched in a message that
+ /// passed through mulitple relays. RELAY_SEACH_FROM_CLIENT will
+ /// start search from the relay that was the closest to the client
+ /// (i.e. innermost in the encapsulated message, which also means
+ /// this was the first relay that forwarded packet received by the
+ /// server and this will be the last relay that will handle the
+ /// response that server sent towards the client.).
+ /// RELAY_SEARCH_FROM_SERVER is the opposite. This will be the
+ /// relay closest to the server (i.e. outermost in the encapsulated
+ /// message, which also means it was the last relay that relayed
+ /// the received message and will be the first one to process
+ /// server's response). RELAY_GET_FIRST will try to get option from
+ /// the first relay only (closest to the client), RELAY_GET_LAST will
+ /// try to get option form the the last relay (closest to the server).
+ enum RelaySearchOrder {
+ RELAY_SEARCH_FROM_CLIENT = 1,
+ RELAY_SEARCH_FROM_SERVER = 2,
+ RELAY_GET_FIRST = 3,
+ RELAY_GET_LAST = 4
+ };
/// @brief structure that describes a single relay information
///
@@ -63,7 +92,7 @@ public:
uint16_t relay_msg_len_;
/// options received from a specified relay, except relay-msg option
- isc::dhcp::Option::OptionCollection options_;
+ isc::dhcp::OptionCollection options_;
};
/// Constructor, used in replying to a message
@@ -90,9 +119,12 @@ public:
/// Options must be stored in options_ field.
/// Output buffer will be stored in data_. Length
/// will be set in data_len_.
+ /// The output buffer is cleared before new data is written to it.
///
- /// @return true if packing procedure was successful
- bool pack();
+ /// @throw BadValue if packet protocol is invalid, InvalidOperation
+ /// if packing fails, or NotImplemented if protocol is TCP (IPv6 over TCP is
+ /// not yet supported).
+ void pack();
/// @brief Dispatch method that handles binary packet parsing.
///
@@ -112,12 +144,7 @@ public:
/// zero length
///
/// @return reference to output buffer
- const isc::util::OutputBuffer& getBuffer() const { return (bufferOut_); };
-
- /// @brief Returns reference to input buffer.
- ///
- /// @return reference to input buffer
- const OptionBuffer& getData() const { return(data_); }
+ const isc::util::OutputBuffer& getBuffer() const { return (buffer_out_); };
/// @brief Returns protocol of this packet (UDP or TCP).
///
@@ -201,6 +228,18 @@ public:
/// @return pointer to the option (or NULL if there is no such option)
OptionPtr getRelayOption(uint16_t option_code, uint8_t nesting_level);
+ /// @brief Return first instance of a specified option
+ ///
+ /// When a client's packet traverses multiple relays, each passing relay may
+ /// insert extra options. This method allows the specific instance of a given
+ /// option to be obtained (e.g. closest to the client, closest to the server,
+ /// etc.) See @ref RelaySearchOrder for a detailed description.
+ ///
+ /// @param option_code searched option
+ /// @param order option search order (see @ref RelaySearchOrder)
+ /// @return option pointer (or NULL if no option matches specified criteria)
+ OptionPtr getAnyRelayOption(uint16_t option_code, RelaySearchOrder order);
+
/// @brief Returns all instances of specified type.
///
/// Returns all instances of options of the specified type. DHCPv6 protocol
@@ -208,7 +247,7 @@ public:
///
/// @param type option type we are looking for
/// @return instance of option collection with requested options
- isc::dhcp::Option::OptionCollection getOptions(uint16_t type);
+ isc::dhcp::OptionCollection getOptions(uint16_t type);
/// Attempts to delete first suboption of requested type
///
@@ -310,15 +349,13 @@ public:
/// collection of options present in this message
///
- /// @todo: Text mentions protected, but this is really public
- ///
- /// @warning This protected member is accessed by derived
+ /// @warning This public member is accessed by derived
/// classes directly. One of such derived classes is
/// @ref perfdhcp::PerfPkt6. The impact on derived clasess'
/// behavior must be taken into consideration before making
/// changes to this member such as access scope restriction or
/// data format change etc.
- isc::dhcp::Option::OptionCollection options_;
+ isc::dhcp::OptionCollection options_;
/// @brief Update packet timestamp.
///
@@ -356,6 +393,22 @@ public:
/// be freed by the caller.
const char* getName() const;
+ /// @brief Set callback function to be used to parse options.
+ ///
+ /// @param callback An instance of the callback function or NULL to
+ /// uninstall callback.
+ void setCallback(UnpackOptionsCallback callback) {
+ callback_ = callback;
+ }
+
+ /// @brief copies relay information from client's packet to server's response
+ ///
+ /// This information is not simply copied over. Some parameter are
+ /// removed, msg_type_is updated (RELAY-FORW => RELAY-REPL), etc.
+ ///
+ /// @param question client's packet
+ void copyRelayInfo(const Pkt6Ptr& question);
+
/// relay information
///
/// this is a public field. Otherwise we hit one of the two problems:
@@ -365,18 +418,59 @@ public:
/// to be impossible). Therefore public field is considered the best
/// (or least bad) solution.
std::vector<RelayInfo> relay_info_;
+
+
+ /// unparsed data (in received packets)
+ ///
+ /// @warning This public member is accessed by derived
+ /// classes directly. One of such derived classes is
+ /// @ref perfdhcp::PerfPkt6. The impact on derived clasess'
+ /// behavior must be taken into consideration before making
+ /// changes to this member such as access scope restriction or
+ /// data format change etc.
+ OptionBuffer data_;
+
+ /// @brief Checks whether a client belongs to a given class
+ ///
+ /// @param client_class name of the class
+ /// @return true if belongs
+ bool inClass(const std::string& client_class);
+
+ /// @brief Adds packet to a specified class
+ ///
+ /// A packet can be added to the same class repeatedly. Any additional
+ /// attempts to add to a class the packet already belongs to, will be
+ /// ignored silently.
+ ///
+ /// @note It is a matter of naming convention. Conceptually, the server
+ /// processes a stream of packets, with some packets belonging to given
+ /// classes. From that perspective, this method adds a packet to specifed
+ /// class. Implementation wise, it looks the opposite - the class name
+ /// is added to the packet. Perhaps the most appropriate name for this
+ /// method would be associateWithClass()? But that seems overly long,
+ /// so I decided to stick with addClass().
+ ///
+ /// @param client_class name of the class to be added
+ void addClass(const std::string& client_class);
+
+ /// @brief Classes this packet belongs to.
+ ///
+ /// This field is public, so code can iterate over existing classes.
+ /// Having it public also solves the problem of returned reference lifetime.
+ Classes classes_;
+
protected:
/// Builds on wire packet for TCP transmission.
///
/// TODO This function is not implemented yet.
///
- /// @return true, if build was successful
- bool packTCP();
+ /// @throw NotImplemented, IPv6 over TCP is not yet supported.
+ void packTCP();
/// Builds on wire packet for UDP transmission.
///
- /// @return true, if build was successful
- bool packUDP();
+ /// @throw InvalidOperation if packing fails
+ void packUDP();
/// @brief Parses on-wire form of TCP DHCPv6 packet.
///
@@ -447,16 +541,6 @@ protected:
/// DHCPv6 transaction-id
uint32_t transid_;
- /// unparsed data (in received packets)
- ///
- /// @warning This protected member is accessed by derived
- /// classes directly. One of such derived classes is
- /// @ref perfdhcp::PerfPkt6. The impact on derived clasess'
- /// behavior must be taken into consideration before making
- /// changes to this member such as access scope restriction or
- /// data format change etc.
- OptionBuffer data_;
-
/// name of the network interface the packet was received/to be sent over
std::string iface_;
@@ -480,7 +564,7 @@ protected:
/// remote TCP or UDP port
uint16_t remote_port_;
- /// output buffer (used during message transmission)
+ /// Output buffer (used during message transmission)
///
/// @warning This protected member is accessed by derived
/// classes directly. One of such derived classes is
@@ -488,13 +572,15 @@ protected:
/// behavior must be taken into consideration before making
/// changes to this member such as access scope restriction or
/// data format change etc.
- isc::util::OutputBuffer bufferOut_;
+ isc::util::OutputBuffer buffer_out_;
/// packet timestamp
boost::posix_time::ptime timestamp_;
-}; // Pkt6 class
-typedef boost::shared_ptr<Pkt6> Pkt6Ptr;
+ /// A callback to be called to unpack options from the packet.
+ UnpackOptionsCallback callback_;
+
+}; // Pkt6 class
} // isc::dhcp namespace
diff --git a/src/lib/dhcp/pkt_filter.cc b/src/lib/dhcp/pkt_filter.cc
new file mode 100644
index 0000000..1eef9c7
--- /dev/null
+++ b/src/lib/dhcp/pkt_filter.cc
@@ -0,0 +1,66 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter.h>
+
+#include <sys/fcntl.h>
+#include <sys/socket.h>
+
+namespace isc {
+namespace dhcp {
+
+int
+PktFilter::openFallbackSocket(const isc::asiolink::IOAddress& addr,
+ const uint16_t port) {
+ // Create socket.
+ int sock = socket(AF_INET, SOCK_DGRAM, 0);
+ if (sock < 0) {
+ isc_throw(SocketConfigError, "failed to create fallback socket for"
+ " address " << addr << ", port " << port
+ << ", reason: " << strerror(errno));
+ }
+ // Bind the socket to a specified address and port.
+ struct sockaddr_in addr4;
+ memset(&addr4, 0, sizeof(addr4));
+ addr4.sin_family = AF_INET;
+ addr4.sin_addr.s_addr = htonl(addr);
+ addr4.sin_port = htons(port);
+
+ if (bind(sock, reinterpret_cast<struct sockaddr*>(&addr4),
+ sizeof(addr4)) < 0) {
+ // Remember to close the socket if we failed to bind it.
+ close(sock);
+ isc_throw(SocketConfigError, "failed to bind fallback socket to"
+ " address " << addr << ", port " << port
+ << ", reason: " << strerror(errno)
+ << " - is another DHCP server running?");
+ }
+
+ // Set socket to non-blocking mode. This is to prevent the read from the
+ // fallback socket to block message processing on the primary socket.
+ if (fcntl(sock, F_SETFL, O_NONBLOCK) != 0) {
+ close(sock);
+ isc_throw(SocketConfigError, "failed to set SO_NONBLOCK option on the"
+ " fallback socket, bound to " << addr << ", port "
+ << port << ", reason: " << strerror(errno));
+ }
+ // Successfully created and bound a fallback socket. Return a descriptor.
+ return (sock);
+}
+
+
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/pkt_filter.h b/src/lib/dhcp/pkt_filter.h
index 946bd14..e35b75d 100644
--- a/src/lib/dhcp/pkt_filter.h
+++ b/src/lib/dhcp/pkt_filter.h
@@ -15,11 +15,21 @@
#ifndef PKT_FILTER_H
#define PKT_FILTER_H
+#include <dhcp/pkt4.h>
#include <asiolink/io_address.h>
+#include <boost/shared_ptr.hpp>
namespace isc {
namespace dhcp {
+/// @brief Exception thrown when invalid packet filter object specified.
+class InvalidPacketFilter : public Exception {
+public:
+ InvalidPacketFilter(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// Forward declaration to the structure describing a socket.
struct SocketInfo;
/// Forward declaration to the class representing interface
@@ -45,20 +55,42 @@ public:
/// @brief Virtual Destructor
virtual ~PktFilter() { }
- /// @brief Open socket.
+ /// @brief Check if packet can be sent to the host without address directly.
+ ///
+ /// Checks if the Packet Filter class has capability to send a packet
+ /// directly to the client having no address assigned. This capability
+ /// is used by DHCPv4 servers which respond to the clients they assign
+ /// addresses to. Not all classes derived from PktFilter support this
+ /// because it requires injection of the destination host HW address to
+ /// the link layer header of the packet.
+ ///
+ /// @return true of the direct response is supported.
+ virtual bool isDirectResponseSupported() const = 0;
+
+ /// @brief Open primary and fallback socket.
+ ///
+ /// A method implementation in the derived class may open one or two
+ /// sockets:
+ /// - a primary socket - used for communication with clients. DHCP messages
+ /// received using this socket are processed and the same socket is used
+ /// to send a response to the client.
+ /// - a fallback socket which is optionally opened if there is a need for
+ /// the presence of the socket which can be bound to a specific IP address
+ /// and UDP port (e.g. raw primary socket can't be). For the details, see
+ /// the documentation of @c isc::dhcp::SocketInfo.
///
- /// @param iface interface descriptor
- /// @param addr address on the interface to be used to send packets.
- /// @param port port number.
- /// @param receive_bcast configure socket to receive broadcast messages
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param receive_bcast Configure socket to receive broadcast messages
/// @param send_bcast configure socket to send broadcast messages.
///
- /// @return created socket's descriptor
- virtual int openSocket(const Iface& iface,
- const isc::asiolink::IOAddress& addr,
- const uint16_t port,
- const bool receive_bcast,
- const bool send_bcast) = 0;
+ /// @return A structure describing a primary and fallback socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool receive_bcast,
+ const bool send_bcast) = 0;
/// @brief Receive packet over specified socket.
///
@@ -71,13 +103,44 @@ public:
/// @brief Send packet over specified socket.
///
+ /// @param iface interface to be used to send packet
/// @param sockfd socket descriptor
/// @param pkt packet to be sent
///
/// @return result of sending the packet. It is 0 if successful.
- virtual int send(uint16_t sockfd, const Pkt4Ptr& pkt) = 0;
+ virtual int send(const Iface& iface, uint16_t sockfd,
+ const Pkt4Ptr& pkt) = 0;
+
+protected:
+
+ /// @brief Default implementation to open a fallback socket.
+ ///
+ /// This method provides a means to open a fallback socket and bind it
+ /// to a given IPv4 address and UDP port. This function may be used by the
+ /// derived classes to create a fallback socket. It can be overriden
+ /// in the derived classes if it happens to be insufficient on some
+ /// environments.
+ ///
+ /// The fallback socket is meant to be opened together with the other socket
+ /// (a.k.a. primary socket) used to receive and handle DHCPv4 traffic. The
+ /// traffic received through the fallback should be dropped. The reasoning
+ /// behind opening the fallback socket is explained in the documentation of
+ /// @c isc::dhcp::SocketInfo structure.
+ ///
+ /// @param addr An IPv4 address to bind the socket to.
+ /// @param port A port number to bind socket to.
+ ///
+ /// @return A fallback socket descriptor. This descriptor should be assigned
+ /// to the @c fallbackfd_ field of the @c isc::dhcp::SocketInfo structure.
+ /// @throw isc::dhcp::SocketConfigError if socket opening, binding or
+ /// configuration fails.
+ virtual int openFallbackSocket(const isc::asiolink::IOAddress& addr,
+ const uint16_t port);
};
+/// Pointer to a PktFilter object.
+typedef boost::shared_ptr<PktFilter> PktFilterPtr;
+
} // namespace isc::dhcp
} // namespace isc
diff --git a/src/lib/dhcp/pkt_filter6.cc b/src/lib/dhcp/pkt_filter6.cc
new file mode 100644
index 0000000..03927fb
--- /dev/null
+++ b/src/lib/dhcp/pkt_filter6.cc
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/pkt_filter6.h>
+
+namespace isc {
+namespace dhcp {
+
+bool
+PktFilter6::joinMulticast(int sock, const std::string& ifname,
+ const std::string & mcast) {
+
+ struct ipv6_mreq mreq;
+ memset(&mreq, 0, sizeof(ipv6_mreq));
+
+ // Convert the multicast address to a binary form.
+ if (inet_pton(AF_INET6, mcast.c_str(), &mreq.ipv6mr_multiaddr) <= 0) {
+ return (false);
+ }
+ // Set the interface being used.
+ mreq.ipv6mr_interface = if_nametoindex(ifname.c_str());
+ // Join the multicast group.
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP,
+ &mreq, sizeof(mreq)) < 0) {
+ return (false);
+ }
+
+ return (true);
+}
+
+
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/pkt_filter6.h b/src/lib/dhcp/pkt_filter6.h
new file mode 100644
index 0000000..dcddf6c
--- /dev/null
+++ b/src/lib/dhcp/pkt_filter6.h
@@ -0,0 +1,150 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PKT_FILTER6_H
+#define PKT_FILTER6_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/pkt6.h>
+
+namespace isc {
+namespace dhcp {
+
+/// Forward declaration to the structure describing a socket.
+struct SocketInfo;
+
+/// Forward declaration to the class representing interface
+class Iface;
+
+/// @brief Abstract packet handling class for DHCPv6.
+///
+/// This class defines methods for performing low level operations on IPv6
+/// socket:
+/// - open socket,
+/// - send DHCPv6 message through the socket,
+/// - receive DHCPv6 through the socket.
+///
+/// Methods exposed by this class are called through the @c IfaceMgr only. They
+/// are not meant to be called directly, except unit testing.
+///
+/// The @c IfaceMgr is responsible for managing the pool of sockets. In
+/// particular, @c IfaceMgr detects interfaces suitable to send/receive DHCPv6
+/// messages. When it intends to open a socket on a particular interface, it
+/// will call the PktFilter6::openSocket. If this call is successful, the
+/// structure describing a new socket is returned.
+///
+/// In order to send or receive a DHCPv6 message through this socket,
+/// the @c IfaceMgr must use PktFilter6::send or PktFilter6::receive
+/// functions of the same class that has been used to open a socket,
+/// i.e. all send/receive operations should be performed using this
+/// particular class.
+///
+/// The major motivation behind creating a separate class, to manage low level
+/// operations using sockets, is to make @c IfaceMgr unit testable. By providing
+/// a stub implementation of this class which mimics the behavior of the real
+/// socket handling class, it is possible to simulate and test various
+/// conditions. For example, the @c IfaceMgr::openSockets function will try to
+/// open sockets on all available interfaces. The test doesn't have any means
+/// to know which interfaces are present. In addition, even if the network
+/// interface detection was implemented on the test side, there is no guarantee
+/// that the particular system has sufficient number of suitable IPv6-enabled
+/// interfaces available for a particular test. Moreover, the test may need
+/// to tweak some of the interface configuration to cover certain test
+/// scenarios. The proposed solution is to not use the actual interfaces
+/// but simply create a pool of fake interfaces which configuration
+/// can be freely modified by a test. This however implies that operations
+/// on sockets must be simulated.
+///
+/// @note This class is named after @c PktFilter abstract class which exposes
+/// similar interface for DHVPv4. However, the PktFilter class is devoted to
+/// solve the problem of sending DHCPv4 messages to the hosts which don't have
+/// an IP address yet (a.k.a. direct DHCPv4 traffic). Where required, the
+/// custom implementations of @c PktFilter are provided to send and receive
+/// messages through raw sockets. In order to filter out the desired traffic
+/// Linux Packet Filtering or Berkeley Packet Filtering is used, hence the
+/// name of the class. In case of DHCPv6 regular IPv6/UDPv6 sockets are used
+/// and derived classes do not use Linux or Berkeley Packet Filtering.
+class PktFilter6 {
+public:
+
+ /// @brief Virtual Destructor.
+ virtual ~PktFilter6() { }
+
+ /// @brief Opens a socket.
+ ///
+ /// This function open an IPv6 socket on an interface and binds it to a
+ /// specified UDP port and IPv6 address.
+ ///
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param join_multicast A boolean parameter which indicates whether
+ /// socket should join All_DHCP_Relay_Agents_and_servers multicast
+ /// group.
+ ///
+ /// @return A structure describing a primary and fallback socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool join_multicast) = 0;
+
+ /// @brief Receives DHCPv6 message on the interface.
+ ///
+ /// This function receives a single DHCPv6 message through using a socket
+ /// open on a specified interface.
+ ///
+ /// @param socket_info A structure holding socket information.
+ ///
+ /// @return A pointer to received message.
+ virtual Pkt6Ptr receive(const SocketInfo& socket_info) = 0;
+
+ /// @brief Sends DHCPv6 message through a specified interface and socket.
+ ///
+ /// This function sends a DHCPv6 message through a specified interface and
+ /// socket. In general, there may be multiple sockets open on a single
+ /// interface as a single interface may have multiple IPv6 addresses.
+ ///
+ /// @param iface Interface to be used to send packet.
+ /// @param sockfd A socket descriptor
+ /// @param pkt A packet to be sent.
+ ///
+ /// @return A result of sending the message. It is 0 if successful.
+ virtual int send(const Iface& iface, uint16_t sockfd,
+ const Pkt6Ptr& pkt) = 0;
+
+ /// @brief Joins IPv6 multicast group on a socket.
+ ///
+ /// Socket must be created and bound to an address. Note that this
+ /// address is different than the multicast address. For example DHCPv6
+ /// server should bind its socket to link-local address (fe80::1234...)
+ /// and later join ff02::1:2 multicast group.
+ ///
+ /// @param sock A socket descriptor (socket must be bound).
+ /// @param ifname An interface name (for link-scoped multicast groups).
+ /// @param mcast A multicast address to join (e.g. "ff02::1:2").
+ ///
+ /// @return true if multicast join was successful
+ static bool joinMulticast(int sock, const std::string& ifname,
+ const std::string & mcast);
+
+};
+
+
+/// Pointer to a PktFilter object.
+typedef boost::shared_ptr<PktFilter6> PktFilter6Ptr;
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // PKT_FILTER6_H
diff --git a/src/lib/dhcp/pkt_filter_inet.cc b/src/lib/dhcp/pkt_filter_inet.cc
index a6360aa..1af3970 100644
--- a/src/lib/dhcp/pkt_filter_inet.cc
+++ b/src/lib/dhcp/pkt_filter_inet.cc
@@ -16,6 +16,8 @@
#include <dhcp/iface_mgr.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt_filter_inet.h>
+#include <errno.h>
+#include <cstring>
using namespace isc::asiolink;
@@ -28,24 +30,12 @@ PktFilterInet::PktFilterInet()
{
}
-// iface is only used when SO_BINDTODEVICE is defined and thus
-// the code section using this variable is compiled.
-#ifdef SO_BINDTODEVICE
-int PktFilterInet::openSocket(const Iface& iface,
- const isc::asiolink::IOAddress& addr,
- const uint16_t port,
- const bool receive_bcast,
- const bool send_bcast) {
-
-#else
-int PktFilterInet::openSocket(const Iface&,
- const isc::asiolink::IOAddress& addr,
- const uint16_t port,
- const bool receive_bcast,
- const bool send_bcast) {
-
-
-#endif
+SocketInfo
+PktFilterInet::openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool receive_bcast,
+ const bool send_bcast) {
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(sockaddr));
@@ -54,7 +44,7 @@ int PktFilterInet::openSocket(const Iface&,
// If we are to receive broadcast messages we have to bind
// to "ANY" address.
- if (receive_bcast) {
+ if (receive_bcast && iface.flag_broadcast_) {
addr4.sin_addr.s_addr = INADDR_ANY;
} else {
addr4.sin_addr.s_addr = htonl(addr);
@@ -66,7 +56,7 @@ int PktFilterInet::openSocket(const Iface&,
}
#ifdef SO_BINDTODEVICE
- if (receive_bcast) {
+ if (receive_bcast && iface.flag_broadcast_) {
// Bind to device so as we receive traffic on a specific interface.
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface.getName().c_str(),
iface.getName().length() + 1) < 0) {
@@ -77,7 +67,7 @@ int PktFilterInet::openSocket(const Iface&,
}
#endif
- if (send_bcast) {
+ if (send_bcast && iface.flag_broadcast_) {
// Enable sending to broadcast address.
int flag = 1;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag)) < 0) {
@@ -89,7 +79,8 @@ int PktFilterInet::openSocket(const Iface&,
if (bind(sock, (struct sockaddr *)&addr4, sizeof(addr4)) < 0) {
close(sock);
- isc_throw(SocketConfigError, "Failed to bind socket " << sock << " to " << addr.toText()
+ isc_throw(SocketConfigError, "Failed to bind socket " << sock
+ << " to " << addr
<< "/port=" << port);
}
@@ -103,7 +94,8 @@ int PktFilterInet::openSocket(const Iface&,
}
#endif
- return (sock);
+ SocketInfo sock_desc(addr, port, sock);
+ return (sock_desc);
}
@@ -198,7 +190,8 @@ PktFilterInet::receive(const Iface& iface, const SocketInfo& socket_info) {
}
int
-PktFilterInet::send(uint16_t sockfd, const Pkt4Ptr& pkt) {
+PktFilterInet::send(const Iface&, uint16_t sockfd,
+ const Pkt4Ptr& pkt) {
memset(&control_buf_[0], 0, control_buf_len_);
// Set the target address we're sending to.
@@ -245,14 +238,15 @@ PktFilterInet::send(uint16_t sockfd, const Pkt4Ptr& pkt) {
struct in_pktinfo* pktinfo =(struct in_pktinfo *)CMSG_DATA(cmsg);
memset(pktinfo, 0, sizeof(struct in_pktinfo));
pktinfo->ipi_ifindex = pkt->getIndex();
- m.msg_controllen = cmsg->cmsg_len;
+ m.msg_controllen = CMSG_SPACE(sizeof(struct in_pktinfo));
#endif
pkt->updateTimestamp();
int result = sendmsg(sockfd, &m, 0);
if (result < 0) {
- isc_throw(SocketWriteError, "pkt4 send failed");
+ isc_throw(SocketWriteError, "pkt4 send failed: sendmsg() returned "
+ " with an error: " << strerror(errno));
}
return (result);
diff --git a/src/lib/dhcp/pkt_filter_inet.h b/src/lib/dhcp/pkt_filter_inet.h
index 4e98612..690622c 100644
--- a/src/lib/dhcp/pkt_filter_inet.h
+++ b/src/lib/dhcp/pkt_filter_inet.h
@@ -16,6 +16,7 @@
#define PKT_FILTER_INET_H
#include <dhcp/pkt_filter.h>
+#include <boost/scoped_array.hpp>
namespace isc {
namespace dhcp {
@@ -32,20 +33,33 @@ public:
/// Allocates control buffer.
PktFilterInet();
- /// @brief Open socket.
+ /// @brief Check if packet can be sent to the host without address directly.
///
- /// @param iface interface descriptor
- /// @param addr address on the interface to be used to send packets.
- /// @param port port number.
- /// @param receive_bcast configure socket to receive broadcast messages
- /// @param send_bcast configure socket to send broadcast messages.
+ /// This Packet Filter sends packets through AF_INET datagram sockets, so
+ /// it can't inject the HW address of the destionation host into the packet.
+ /// Therefore this class does not support direct responses.
///
- /// @return created socket's descriptor
- virtual int openSocket(const Iface& iface,
- const isc::asiolink::IOAddress& addr,
- const uint16_t port,
- const bool receive_bcast,
- const bool send_bcast);
+ /// @return false always.
+ virtual bool isDirectResponseSupported() const {
+ return (false);
+ }
+
+ /// @brief Open primary and fallback socket.
+ ///
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param receive_bcast Configure socket to receive broadcast messages
+ /// @param send_bcast Configure socket to send broadcast messages.
+ ///
+ /// @return A structure describing a primary and fallback socket.
+ /// @throw isc::dhcp::SocketConfigError if error occurs when opening,
+ /// binding or configuring the socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool receive_bcast,
+ const bool send_bcast);
/// @brief Receive packet over specified socket.
///
@@ -53,15 +67,23 @@ public:
/// @param socket_info structure holding socket information
///
/// @return Received packet
+ /// @throw isc::dhcp::SocketReadError if an error occurs during reception
+ /// of the packet.
+ /// @throw An execption thrown by the isc::dhcp::Pkt4 object if DHCPv4
+ /// message parsing fails.
virtual Pkt4Ptr receive(const Iface& iface, const SocketInfo& socket_info);
/// @brief Send packet over specified socket.
///
+ /// @param iface interface to be used to send packet
/// @param sockfd socket descriptor
/// @param pkt packet to be sent
///
/// @return result of sending a packet. It is 0 if successful.
- virtual int send(uint16_t sockfd, const Pkt4Ptr& pkt);
+ /// @throw isc::dhcp::SocketWriteError if an error occures during sending
+ /// a DHCP message through the socket.
+ virtual int send(const Iface& iface, uint16_t sockfd,
+ const Pkt4Ptr& pkt);
private:
/// Length of the control_buf_ array.
diff --git a/src/lib/dhcp/pkt_filter_inet6.cc b/src/lib/dhcp/pkt_filter_inet6.cc
new file mode 100644
index 0000000..1ec75f3
--- /dev/null
+++ b/src/lib/dhcp/pkt_filter_inet6.cc
@@ -0,0 +1,286 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/pkt_filter_inet6.h>
+#include <util/io/pktinfo_utilities.h>
+
+#include <netinet/in.h>
+
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+
+PktFilterInet6::PktFilterInet6()
+: control_buf_len_(CMSG_SPACE(sizeof(struct in6_pktinfo))),
+ control_buf_(new char[control_buf_len_]) {
+}
+
+SocketInfo
+PktFilterInet6::openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool join_multicast) {
+ struct sockaddr_in6 addr6;
+ memset(&addr6, 0, sizeof(addr6));
+ addr6.sin6_family = AF_INET6;
+ addr6.sin6_port = htons(port);
+ if (addr.toText() != "::1") {
+ addr6.sin6_scope_id = if_nametoindex(iface.getName().c_str());
+ }
+
+ memcpy(&addr6.sin6_addr, &addr.toBytes()[0], sizeof(addr6.sin6_addr));
+#ifdef HAVE_SA_LEN
+ addr6.sin6_len = sizeof(addr6);
+#endif
+
+ // @todo use sockcreator once it becomes available
+
+ // make a socket
+ int sock = socket(AF_INET6, SOCK_DGRAM, 0);
+ if (sock < 0) {
+ isc_throw(SocketConfigError, "Failed to create UDP6 socket.");
+ }
+
+ // Set SO_REUSEADDR option.
+ int flag = 1;
+ if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
+ (char *)&flag, sizeof(flag)) < 0) {
+ close(sock);
+ isc_throw(SocketConfigError, "Can't set SO_REUSEADDR option on dhcpv6 socket.");
+ }
+
+ if (bind(sock, (struct sockaddr *)&addr6, sizeof(addr6)) < 0) {
+ close(sock);
+ isc_throw(SocketConfigError, "Failed to bind socket " << sock << " to " << addr.toText()
+ << "/port=" << port);
+ }
+#ifdef IPV6_RECVPKTINFO
+ // RFC3542 - a new way
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO,
+ &flag, sizeof(flag)) != 0) {
+ close(sock);
+ isc_throw(SocketConfigError, "setsockopt: IPV6_RECVPKTINFO failed.");
+ }
+#else
+ // RFC2292 - an old way
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_PKTINFO,
+ &flag, sizeof(flag)) != 0) {
+ close(sock);
+ isc_throw(SocketConfigError, "setsockopt: IPV6_PKTINFO: failed.");
+ }
+#endif
+
+ // Join All_DHCP_Relay_Agents_and_Servers multicast group if
+ // requested.
+ if (join_multicast &&
+ !joinMulticast(sock, iface.getName(),
+ std::string(ALL_DHCP_RELAY_AGENTS_AND_SERVERS))) {
+ close(sock);
+ isc_throw(SocketConfigError, "Failed to join "
+ << ALL_DHCP_RELAY_AGENTS_AND_SERVERS
+ << " multicast group.");
+ }
+
+ return (SocketInfo(addr, port, sock));
+}
+
+Pkt6Ptr
+PktFilterInet6::receive(const SocketInfo& socket_info) {
+ // Now we have a socket, let's get some data from it!
+ uint8_t buf[IfaceMgr::RCVBUFSIZE];
+ memset(&control_buf_[0], 0, control_buf_len_);
+ struct sockaddr_in6 from;
+ memset(&from, 0, sizeof(from));
+
+ // Initialize our message header structure.
+ struct msghdr m;
+ memset(&m, 0, sizeof(m));
+
+ // Point so we can get the from address.
+ m.msg_name = &from;
+ m.msg_namelen = sizeof(from);
+
+ // Set the data buffer we're receiving. (Using this wacky
+ // "scatter-gather" stuff... but we that doesn't really make
+ // sense for us, so we use a single vector entry.)
+ struct iovec v;
+ memset(&v, 0, sizeof(v));
+ v.iov_base = static_cast<void*>(buf);
+ v.iov_len = IfaceMgr::RCVBUFSIZE;
+ m.msg_iov = &v;
+ m.msg_iovlen = 1;
+
+ // Getting the interface is a bit more involved.
+ //
+ // We set up some space for a "control message". We have
+ // previously asked the kernel to give us packet
+ // information (when we initialized the interface), so we
+ // should get the destination address from that.
+ m.msg_control = &control_buf_[0];
+ m.msg_controllen = control_buf_len_;
+
+ int result = recvmsg(socket_info.sockfd_, &m, 0);
+
+ struct in6_addr to_addr;
+ memset(&to_addr, 0, sizeof(to_addr));
+
+ int ifindex = -1;
+ if (result >= 0) {
+ struct in6_pktinfo* pktinfo = NULL;
+
+
+ // If we did read successfully, then we need to loop
+ // through the control messages we received and
+ // find the one with our destination address.
+ //
+ // We also keep a flag to see if we found it. If we
+ // didn't, then we consider this to be an error.
+ bool found_pktinfo = false;
+ struct cmsghdr* cmsg = CMSG_FIRSTHDR(&m);
+ while (cmsg != NULL) {
+ if ((cmsg->cmsg_level == IPPROTO_IPV6) &&
+ (cmsg->cmsg_type == IPV6_PKTINFO)) {
+ pktinfo = util::io::internal::convertPktInfo6(CMSG_DATA(cmsg));
+ to_addr = pktinfo->ipi6_addr;
+ ifindex = pktinfo->ipi6_ifindex;
+ found_pktinfo = true;
+ break;
+ }
+ cmsg = CMSG_NXTHDR(&m, cmsg);
+ }
+ if (!found_pktinfo) {
+ isc_throw(SocketReadError, "unable to find pktinfo");
+ }
+ } else {
+ isc_throw(SocketReadError, "failed to receive data");
+ }
+
+ // Let's create a packet.
+ Pkt6Ptr pkt;
+ try {
+ pkt = Pkt6Ptr(new Pkt6(buf, result));
+ } catch (const std::exception& ex) {
+ isc_throw(SocketReadError, "failed to create new packet");
+ }
+
+ pkt->updateTimestamp();
+
+ pkt->setLocalAddr(IOAddress::fromBytes(AF_INET6,
+ reinterpret_cast<const uint8_t*>(&to_addr)));
+ pkt->setRemoteAddr(IOAddress::fromBytes(AF_INET6,
+ reinterpret_cast<const uint8_t*>(&from.sin6_addr)));
+ pkt->setRemotePort(ntohs(from.sin6_port));
+ pkt->setIndex(ifindex);
+
+ Iface* received = IfaceMgr::instance().getIface(pkt->getIndex());
+ if (received) {
+ pkt->setIface(received->getName());
+ } else {
+ isc_throw(SocketReadError, "received packet over unknown interface"
+ << "(ifindex=" << pkt->getIndex() << ")");
+ }
+
+ return (pkt);
+
+}
+
+int
+PktFilterInet6::send(const Iface&, uint16_t sockfd, const Pkt6Ptr& pkt) {
+
+ memset(&control_buf_[0], 0, control_buf_len_);
+
+ // Set the target address we're sending to.
+ sockaddr_in6 to;
+ memset(&to, 0, sizeof(to));
+ to.sin6_family = AF_INET6;
+ to.sin6_port = htons(pkt->getRemotePort());
+ memcpy(&to.sin6_addr,
+ &pkt->getRemoteAddr().toBytes()[0],
+ 16);
+ to.sin6_scope_id = pkt->getIndex();
+
+ // Initialize our message header structure.
+ struct msghdr m;
+ memset(&m, 0, sizeof(m));
+ m.msg_name = &to;
+ m.msg_namelen = sizeof(to);
+
+ // Set the data buffer we're sending. (Using this wacky
+ // "scatter-gather" stuff... we only have a single chunk
+ // of data to send, so we declare a single vector entry.)
+
+ // As v structure is a C-style is used for both sending and
+ // receiving data, it is shared between sending and receiving
+ // (sendmsg and recvmsg). It is also defined in system headers,
+ // so we have no control over its definition. To set iov_base
+ // (defined as void*) we must use const cast from void *.
+ // Otherwise C++ compiler would complain that we are trying
+ // to assign const void* to void*.
+ struct iovec v;
+ memset(&v, 0, sizeof(v));
+ v.iov_base = const_cast<void *>(pkt->getBuffer().getData());
+ v.iov_len = pkt->getBuffer().getLength();
+ m.msg_iov = &v;
+ m.msg_iovlen = 1;
+
+ // Setting the interface is a bit more involved.
+ //
+ // We have to create a "control message", and set that to
+ // define the IPv6 packet information. We could set the
+ // source address if we wanted, but we can safely let the
+ // kernel decide what that should be.
+ m.msg_control = &control_buf_[0];
+ m.msg_controllen = control_buf_len_;
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&m);
+
+ // FIXME: Code below assumes that cmsg is not NULL, but
+ // CMSG_FIRSTHDR() is coded to return NULL as a possibility. The
+ // following assertion should never fail, but if it did and you came
+ // here, fix the code. :)
+ assert(cmsg != NULL);
+
+ cmsg->cmsg_level = IPPROTO_IPV6;
+ cmsg->cmsg_type = IPV6_PKTINFO;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
+ struct in6_pktinfo *pktinfo =
+ util::io::internal::convertPktInfo6(CMSG_DATA(cmsg));
+ memset(pktinfo, 0, sizeof(struct in6_pktinfo));
+ pktinfo->ipi6_ifindex = pkt->getIndex();
+ // According to RFC3542, section 20.2, the msg_controllen field
+ // may be set using CMSG_SPACE (which includes padding) or
+ // using CMSG_LEN. Both forms appear to work fine on Linux, FreeBSD,
+ // NetBSD, but OpenBSD appears to have a bug, discussed here:
+ // http://www.archivum.info/mailing.openbsd.bugs/2009-02/00017/
+ // kernel-6080-msg_controllen-of-IPV6_PKTINFO.html
+ // which causes sendmsg to return EINVAL if the CMSG_LEN is
+ // used to set the msg_controllen value.
+ m.msg_controllen = CMSG_SPACE(sizeof(struct in6_pktinfo));
+
+ pkt->updateTimestamp();
+
+ int result = sendmsg(sockfd, &m, 0);
+ if (result < 0) {
+ isc_throw(SocketWriteError, "pkt6 send failed: sendmsg() returned"
+ " with an error: " << strerror(errno));
+ }
+
+ return (result);
+}
+
+
+}
+}
diff --git a/src/lib/dhcp/pkt_filter_inet6.h b/src/lib/dhcp/pkt_filter_inet6.h
new file mode 100644
index 0000000..c475cf5
--- /dev/null
+++ b/src/lib/dhcp/pkt_filter_inet6.h
@@ -0,0 +1,98 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PKT_FILTER_INET6_H
+#define PKT_FILTER_INET6_H
+
+#include <dhcp/pkt_filter6.h>
+#include <boost/scoped_array.hpp>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief A DHCPv6 packet handling class using datagram sockets.
+///
+/// This class opens a datagram IPv6/UDPv6 socket. It also implements functions
+/// to send and receive DHCPv6 messages through this socket. It is a default
+/// class to be used by @c IfaceMgr to access IPv6 sockets.
+class PktFilterInet6 : public PktFilter6 {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// Initializes a control buffer used in the message transmission.
+ PktFilterInet6();
+
+ /// @brief Opens a socket.
+ ///
+ /// This function open an IPv6 socket on an interface and binds it to a
+ /// specified UDP port and IP address.
+ ///
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param join_multicast A boolean parameter which indicates whether
+ /// socket should join All_DHCP_Relay_Agents_and_servers multicast
+ /// group.
+ ///
+ /// @return A structure describing a primary and fallback socket.
+ /// @throw isc::dhcp::SocketConfigError if error occured when opening
+ /// or configuring a socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool join_multicast);
+
+ /// @brief Receives DHCPv6 message on the interface.
+ ///
+ /// This function receives a single DHCPv6 message through a socket
+ /// open on a specified interface. This function will block if there is
+ /// no message waiting on the specified socket. Therefore the @c IfaceMgr
+ /// must first check that there is any message on the socket (using
+ /// select function) prior to calling this function.
+ ///
+ /// @param socket_info A structure holding socket information.
+ ///
+ /// @return A pointer to received message.
+ /// @throw isc::dhcp::SocketReadError if error occurred during packet
+ /// reception.
+ virtual Pkt6Ptr receive(const SocketInfo& socket_info);
+
+ /// @brief Sends DHCPv6 message through a specified interface and socket.
+ ///
+ /// Thie function sends a DHCPv6 message through a specified interface and
+ /// socket. In general, there may be multiple sockets open on a single
+ /// interface as a single interface may have multiple IPv6 addresses.
+ ///
+ /// @param iface Interface to be used to send packet.
+ /// @param sockfd A socket descriptor
+ /// @param pkt A packet to be sent.
+ ///
+ /// @return A result of sending the message. It is 0 if successful.
+ /// @throw isc::dhcp::SocketWriteError if error occured when sending a
+ /// packet.
+ virtual int send(const Iface& iface, uint16_t sockfd,
+ const Pkt6Ptr& pkt);
+
+private:
+ /// Length of the control_buf_ array.
+ size_t control_buf_len_;
+ /// Control buffer, used in transmission and reception.
+ boost::scoped_array<char> control_buf_;
+};
+
+} // namespace isc::dhcp
+} // namespace isc
+
+#endif // PKT_FILTER_INET6_H
diff --git a/src/lib/dhcp/pkt_filter_lpf.cc b/src/lib/dhcp/pkt_filter_lpf.cc
index ef75426..06f1e19 100644
--- a/src/lib/dhcp/pkt_filter_lpf.cc
+++ b/src/lib/dhcp/pkt_filter_lpf.cc
@@ -13,31 +13,273 @@
// PERFORMANCE OF THIS SOFTWARE.
#include <config.h>
+#include <dhcp/dhcp4.h>
#include <dhcp/iface_mgr.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt_filter_lpf.h>
+#include <dhcp/protocol_util.h>
+#include <exceptions/exceptions.h>
+#include <linux/filter.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <net/ethernet.h>
+
+namespace {
+
+using namespace isc::dhcp;
+
+/// The following structure defines a Berkely Packet Filter program to perform
+/// packet filtering. The program operates on Ethernet packets. To help with
+/// interpretation of the program, for the types of Ethernet packets we are
+/// interested in, the header layout is:
+///
+/// 6 bytes Destination Ethernet Address
+/// 6 bytes Source Ethernet Address
+/// 2 bytes Ethernet packet type
+///
+/// 20 bytes Fixed part of IP header
+/// variable Variable part of IP header
+///
+/// 2 bytes UDP Source port
+/// 2 bytes UDP destination port
+/// 4 bytes Rest of UDP header
+///
+/// @todo We may want to extend the filter to receive packets sent
+/// to the particular IP address assigned to the interface or
+/// broadcast address.
+struct sock_filter dhcp_sock_filter [] = {
+ // Make sure this is an IP packet: check the half-word (two bytes)
+ // at offset 12 in the packet (the Ethernet packet type). If it
+ // is, advance to the next instruction. If not, advance 8
+ // instructions (which takes execution to the last instruction in
+ // the sequence: "drop it").
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, ETHERNET_PACKET_TYPE_OFFSET),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_IP, 0, 8),
+
+ // Make sure it's a UDP packet. The IP protocol is at offset
+ // 9 in the IP header so, adding the Ethernet packet header size
+ // of 14 bytes gives an absolute byte offset in the packet of 23.
+ BPF_STMT(BPF_LD + BPF_B + BPF_ABS, ETHERNET_HEADER_LEN + IP_PROTO_TYPE_OFFSET),
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 0, 6),
+
+ // Make sure this isn't a fragment by checking that the fragment
+ // offset field in the IP header is zero. This field is the
+ // least-significant 13 bits in the bytes at offsets 6 and 7 in
+ // the IP header, so the half-word at offset 20 (6 + size of
+ // Ethernet header) is loaded and an appropriate mask applied.
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, ETHERNET_HEADER_LEN + IP_FLAGS_OFFSET),
+ BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x1fff, 4, 0),
+
+ // Get the IP header length. This is achieved by the following
+ // (special) instruction that, given the offset of the start
+ // of the IP header (offset 14) loads the IP header length.
+ BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, ETHERNET_HEADER_LEN),
+
+ // Make sure it's to the right port. The following instruction
+ // adds the previously extracted IP header length to the given
+ // offset to locate the correct byte. The given offset of 16
+ // comprises the length of the Ethernet header (14) plus the offset
+ // of the UDP destination port (2) within the UDP header.
+ BPF_STMT(BPF_LD + BPF_H + BPF_IND, ETHERNET_HEADER_LEN + UDP_DEST_PORT),
+ // The following instruction tests against the default DHCP server port,
+ // but the action port is actually set in PktFilterLPF::openSocket().
+ // N.B. The code in that method assumes that this instruction is at
+ // offset 8 in the program. If this is changed, openSocket() must be
+ // updated.
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DHCP4_SERVER_PORT, 0, 1),
+
+ // If we passed all the tests, ask for the whole packet.
+ BPF_STMT(BPF_RET + BPF_K, (u_int)-1),
+
+ // Otherwise, drop it.
+ BPF_STMT(BPF_RET + BPF_K, 0),
+};
+
+}
+
+using namespace isc::util;
namespace isc {
namespace dhcp {
-int
-PktFilterLPF::openSocket(const Iface&, const isc::asiolink::IOAddress&,
- const uint16_t, const bool,
+SocketInfo
+PktFilterLPF::openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool,
const bool) {
- isc_throw(isc::NotImplemented,
- "Linux Packet Filtering is not implemented yet");
+
+ // Open fallback socket first. If it fails, it will give us an indication
+ // that there is another service (perhaps DHCP server) running.
+ // The function will throw an exception and effectivelly cease opening
+ // raw socket below.
+ int fallback = openFallbackSocket(addr, port);
+
+ // The fallback is open, so we are good to open primary socket.
+ int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
+ if (sock < 0) {
+ close(fallback);
+ isc_throw(SocketConfigError, "Failed to create raw LPF socket");
+ }
+
+ // Create socket filter program. This program will only allow incoming UDP
+ // traffic which arrives on the specific (DHCP) port). It will also filter
+ // out all fragmented packets.
+ struct sock_fprog filter_program;
+ memset(&filter_program, 0, sizeof(filter_program));
+
+ filter_program.filter = dhcp_sock_filter;
+ filter_program.len = sizeof(dhcp_sock_filter) / sizeof(struct sock_filter);
+ // Override the default port value.
+ dhcp_sock_filter[8].k = port;
+ // Apply the filter.
+ if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &filter_program,
+ sizeof(filter_program)) < 0) {
+ close(sock);
+ close(fallback);
+ isc_throw(SocketConfigError, "Failed to install packet filtering program"
+ << " on the socket " << sock);
+ }
+
+ struct sockaddr_ll sa;
+ memset(&sa, 0, sizeof(sockaddr_ll));
+ sa.sll_family = AF_PACKET;
+ sa.sll_ifindex = iface.getIndex();
+
+ // For raw sockets we construct IP headers on our own, so we don't bind
+ // socket to IP address but to the interface. We will later use the
+ // Linux Packet Filtering to filter out these packets that we are
+ // interested in.
+ if (bind(sock, reinterpret_cast<const struct sockaddr*>(&sa),
+ sizeof(sa)) < 0) {
+ close(sock);
+ close(fallback);
+ isc_throw(SocketConfigError, "Failed to bind LPF socket '" << sock
+ << "' to interface '" << iface.getName() << "'");
+ }
+
+ return (SocketInfo(addr, port, sock, fallback));
+
}
Pkt4Ptr
-PktFilterLPF::receive(const Iface&, const SocketInfo&) {
- isc_throw(isc::NotImplemented,
- "Linux Packet Filtering is not implemented yet");
+PktFilterLPF::receive(const Iface& iface, const SocketInfo& socket_info) {
+ uint8_t raw_buf[IfaceMgr::RCVBUFSIZE];
+ // First let's get some data from the fallback socket. The data will be
+ // discarded but we don't want the socket buffer to bloat. We get the
+ // packets from the socket in loop but most of the time the loop will
+ // end after receiving one packet. The call to recv returns immediately
+ // when there is no data left on the socket because the socket is
+ // non-blocking.
+ // @todo In the normal conditions, both the primary socket and the fallback
+ // socket are in sync as they are set to receive packets on the same
+ // address and port. The reception of packets on the fallback socket
+ // shouldn't cause significant lags in packet reception. If we find in the
+ // future that it does, the sort of threshold could be set for the maximum
+ // bytes received on the fallback socket in a single round. Further
+ // optimizations would include an asynchronous read from the fallback socket
+ // when the DHCP server is idle.
+ int datalen;
+ do {
+ datalen = recv(socket_info.fallbackfd_, raw_buf, sizeof(raw_buf), 0);
+ } while (datalen > 0);
+
+ // Now that we finished getting data from the fallback socket, we
+ // have to get the data from the raw socket too.
+ int data_len = read(socket_info.sockfd_, raw_buf, sizeof(raw_buf));
+ // If negative value is returned by read(), it indicates that an
+ // error occured. If returned value is 0, no data was read from the
+ // socket. In both cases something has gone wrong, because we expect
+ // that a chunk of data is there. We signal the lack of data by
+ // returing an empty packet.
+ if (data_len <= 0) {
+ return Pkt4Ptr();
+ }
+
+ InputBuffer buf(raw_buf, data_len);
+
+ // @todo: This is awkward way to solve the chicken and egg problem
+ // whereby we don't know the offset where DHCP data start in the
+ // received buffer when we create the packet object. In general case,
+ // the IP header has variable length. The information about its length
+ // is stored in one of its fields. Therefore, we have to decode the
+ // packet to get the offset of the DHCP data. The dummy object is
+ // created so as we can pass it to the functions which decode IP stack
+ // and find actual offset of the DHCP data.
+ // Once we find the offset we can create another Pkt4 object from
+ // the reminder of the input buffer and set the IP addresses and
+ // ports from the dummy packet. We should consider doing it
+ // in some more elegant way.
+ Pkt4Ptr dummy_pkt = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 0));
+
+ // Decode ethernet, ip and udp headers.
+ decodeEthernetHeader(buf, dummy_pkt);
+ decodeIpUdpHeader(buf, dummy_pkt);
+
+ // Read the DHCP data.
+ std::vector<uint8_t> dhcp_buf;
+ buf.readVector(dhcp_buf, buf.getLength() - buf.getPosition());
+
+ // Decode DHCP data into the Pkt4 object.
+ Pkt4Ptr pkt = Pkt4Ptr(new Pkt4(&dhcp_buf[0], dhcp_buf.size()));
+
+ // Set the appropriate packet members using data collected from
+ // the decoded headers.
+ pkt->setIndex(iface.getIndex());
+ pkt->setIface(iface.getName());
+ pkt->setLocalAddr(dummy_pkt->getLocalAddr());
+ pkt->setRemoteAddr(dummy_pkt->getRemoteAddr());
+ pkt->setLocalPort(dummy_pkt->getLocalPort());
+ pkt->setRemotePort(dummy_pkt->getRemotePort());
+ pkt->setLocalHWAddr(dummy_pkt->getLocalHWAddr());
+ pkt->setRemoteHWAddr(dummy_pkt->getRemoteHWAddr());
+
+ return (pkt);
}
int
-PktFilterLPF::send(uint16_t, const Pkt4Ptr&) {
- isc_throw(isc::NotImplemented,
- "Linux Packet Filtering is not implemented yet");
+PktFilterLPF::send(const Iface& iface, uint16_t sockfd, const Pkt4Ptr& pkt) {
+
+ OutputBuffer buf(14);
+
+ // Some interfaces may have no HW address - e.g. loopback interface.
+ // For these interfaces the HW address length is 0. If this is the case,
+ // then we will rely on the functions which construct the IP/UDP headers
+ // to provide a default HW addres. Otherwise, create the HW address
+ // object using the HW address of the interface.
+ if (iface.getMacLen() > 0) {
+ HWAddrPtr hwaddr(new HWAddr(iface.getMac(), iface.getMacLen(),
+ iface.getHWType()));
+ pkt->setLocalHWAddr(hwaddr);
+ }
+
+
+ // Ethernet frame header.
+ // Note that we don't validate whether HW addresses in 'pkt'
+ // are valid because they are checked by the function called.
+ writeEthernetHeader(pkt, buf);
+
+ // IP and UDP header
+ writeIpUdpHeader(pkt, buf);
+
+ // DHCPv4 message
+ buf.writeData(pkt->getBuffer().getData(), pkt->getBuffer().getLength());
+
+ sockaddr_ll sa;
+ sa.sll_family = AF_PACKET;
+ sa.sll_ifindex = iface.getIndex();
+ sa.sll_protocol = htons(ETH_P_IP);
+ sa.sll_halen = 6;
+
+ int result = sendto(sockfd, buf.getData(), buf.getLength(), 0,
+ reinterpret_cast<const struct sockaddr*>(&sa),
+ sizeof(sockaddr_ll));
+ if (result < 0) {
+ isc_throw(SocketWriteError, "failed to send DHCPv4 packet, errno="
+ << errno << " (check errno.h)");
+ }
+
+ return (0);
+
}
diff --git a/src/lib/dhcp/pkt_filter_lpf.h b/src/lib/dhcp/pkt_filter_lpf.h
index 67b190f..1815dda 100644
--- a/src/lib/dhcp/pkt_filter_lpf.h
+++ b/src/lib/dhcp/pkt_filter_lpf.h
@@ -17,34 +17,43 @@
#include <dhcp/pkt_filter.h>
+#include <util/buffer.h>
+
namespace isc {
namespace dhcp {
/// @brief Packet handling class using Linux Packet Filtering
///
-/// This class provides methods to send and recive packet using raw sockets
-/// and Linux Packet Filtering.
-///
-/// @warning This class is not implemented yet. Therefore all functions
-/// currently throw isc::NotImplemented exception.
+/// This class provides methods to send and recive DHCPv4 messages using raw
+/// sockets and Linux Packet Filtering. It is used by @c isc::dhcp::IfaceMgr
+/// to send DHCPv4 messages to the hosts which don't have an IPv4 address
+/// assigned yet.
class PktFilterLPF : public PktFilter {
public:
- /// @brief Open socket.
+ /// @brief Check if packet can be sent to the host without address directly.
///
- /// @param iface interface descriptor
- /// @param addr address on the interface to be used to send packets.
- /// @param port port number.
- /// @param receive_bcast configure socket to receive broadcast messages
- /// @param send_bcast configure socket to send broadcast messages.
+ /// This class supports direct responses to the host without address.
///
- /// @throw isc::NotImplemented always
- /// @return created socket's descriptor
- virtual int openSocket(const Iface& iface,
- const isc::asiolink::IOAddress& addr,
- const uint16_t port,
- const bool receive_bcast,
- const bool send_bcast);
+ /// @return true always.
+ virtual bool isDirectResponseSupported() const {
+ return (true);
+ }
+
+ /// @brief Open primary and fallback socket.
+ ///
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param receive_bcast Configure socket to receive broadcast messages
+ /// @param send_bcast Configure socket to send broadcast messages.
+ ///
+ /// @return A structure describing a primary and fallback socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool receive_bcast,
+ const bool send_bcast);
/// @brief Receive packet over specified socket.
///
@@ -57,12 +66,14 @@ public:
/// @brief Send packet over specified socket.
///
+ /// @param iface interface to be used to send packet
/// @param sockfd socket descriptor
/// @param pkt packet to be sent
///
/// @throw isc::NotImplemented always
/// @return result of sending a packet. It is 0 if successful.
- virtual int send(uint16_t sockfd, const Pkt4Ptr& pkt);
+ virtual int send(const Iface& iface, uint16_t sockfd,
+ const Pkt4Ptr& pkt);
};
diff --git a/src/lib/dhcp/protocol_util.cc b/src/lib/dhcp/protocol_util.cc
new file mode 100644
index 0000000..d93f8c4
--- /dev/null
+++ b/src/lib/dhcp/protocol_util.cc
@@ -0,0 +1,243 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/protocol_util.h>
+#include <boost/static_assert.hpp>
+// in_systm.h is required on some some BSD systems
+// complaining that n_time is undefined but used
+// in ip.h.
+#include <netinet/in_systm.h>
+#include <netinet/ip.h>
+
+using namespace isc::asiolink;
+using namespace isc::util;
+
+namespace isc {
+namespace dhcp {
+
+void
+decodeEthernetHeader(InputBuffer& buf, Pkt4Ptr& pkt) {
+ // The size of the buffer to be parsed must not be lower
+ // then the size of the Ethernet frame header.
+ if (buf.getLength() - buf.getPosition() < ETHERNET_HEADER_LEN) {
+ isc_throw(InvalidPacketHeader, "size of ethernet header in received "
+ << "packet is invalid, expected at least "
+ << ETHERNET_HEADER_LEN << " bytes, received "
+ << buf.getLength() - buf.getPosition() << " bytes");
+ }
+ // Packet object must not be NULL. We want to output some values
+ // to this object.
+ if (!pkt) {
+ isc_throw(BadValue, "NULL packet object provided when parsing ethernet"
+ " frame header");
+ }
+
+ // The size of the single address is always lower then the size of
+ // the header that holds this address. Otherwise, it is a programming
+ // error that we want to detect in the compilation time.
+ BOOST_STATIC_ASSERT(ETHERNET_HEADER_LEN > HWAddr::ETHERNET_HWADDR_LEN);
+
+ // Remember initial position.
+ size_t start_pos = buf.getPosition();
+
+ // Read the destination HW address.
+ std::vector<uint8_t> dest_addr;
+ buf.readVector(dest_addr, HWAddr::ETHERNET_HWADDR_LEN);
+ pkt->setLocalHWAddr(HWTYPE_ETHERNET, HWAddr::ETHERNET_HWADDR_LEN, dest_addr);
+ // Read the source HW address.
+ std::vector<uint8_t> src_addr;
+ buf.readVector(src_addr, HWAddr::ETHERNET_HWADDR_LEN);
+ pkt->setRemoteHWAddr(HWTYPE_ETHERNET, HWAddr::ETHERNET_HWADDR_LEN, src_addr);
+ // Move the buffer read pointer to the end of the Ethernet frame header.
+ buf.setPosition(start_pos + ETHERNET_HEADER_LEN);
+}
+
+void
+decodeIpUdpHeader(InputBuffer& buf, Pkt4Ptr& pkt) {
+ // The size of the buffer must be at least equal to the minimal size of
+ // the IPv4 packet header plus UDP header length.
+ if (buf.getLength() - buf.getPosition() < MIN_IP_HEADER_LEN + UDP_HEADER_LEN) {
+ isc_throw(InvalidPacketHeader, "the total size of the IP and UDP headers in "
+ << "received packet is invalid, expected at least "
+ << MIN_IP_HEADER_LEN + UDP_HEADER_LEN
+ << " bytes, received " << buf.getLength() - buf.getPosition()
+ << " bytes");
+ }
+
+ // Packet object must not be NULL.
+ if (!pkt) {
+ isc_throw(BadValue, "NULL packet object provided when parsing IP and UDP"
+ " packet headers");
+ }
+
+ BOOST_STATIC_ASSERT(IP_SRC_ADDR_OFFSET < MIN_IP_HEADER_LEN);
+
+ // Remember initial position of the read pointer.
+ size_t start_pos = buf.getPosition();
+
+ // Read IP header length (mask most significant bits as they indicate IP version).
+ uint8_t ip_len = buf.readUint8() & 0xF;
+ // IP length is the number of 4 byte chunks that construct IPv4 header.
+ // It must not be lower than 5 because first 20 bytes are fixed.
+ if (ip_len < 5) {
+ isc_throw(InvalidPacketHeader, "Value of the length of the IP header must not be"
+ << " lower than 5 words. The length of the received header is "
+ << ip_len << ".");
+ }
+
+ // Seek to the position of source IP address.
+ buf.setPosition(start_pos + IP_SRC_ADDR_OFFSET);
+ // Read source address.
+ pkt->setRemoteAddr(IOAddress(buf.readUint32()));
+ // Read destination address.
+ pkt->setLocalAddr(IOAddress(buf.readUint32()));
+
+ // Skip IP header options (if any) to start of the
+ // UDP header.
+ buf.setPosition(start_pos + ip_len * 4);
+
+ // Read source port from UDP header.
+ pkt->setRemotePort(buf.readUint16());
+ // Read destination port from UDP header.
+ pkt->setLocalPort(buf.readUint16());
+
+ // Set the pointer position to the first byte o the
+ // UDP payload (DHCP packet).
+ buf.setPosition(start_pos + ip_len * 4 + UDP_HEADER_LEN);
+}
+
+void
+writeEthernetHeader(const Pkt4Ptr& pkt, OutputBuffer& out_buf) {
+ // Set destination HW address.
+ HWAddrPtr remote_addr = pkt->getRemoteHWAddr();
+ if (remote_addr) {
+ if (remote_addr->hwaddr_.size() == HWAddr::ETHERNET_HWADDR_LEN) {
+ out_buf.writeData(&remote_addr->hwaddr_[0],
+ HWAddr::ETHERNET_HWADDR_LEN);
+ } else {
+ isc_throw(BadValue, "invalid size of the remote HW address "
+ << remote_addr->hwaddr_.size() << " when constructing"
+ << " an ethernet frame header; expected size is"
+ << " " << HWAddr::ETHERNET_HWADDR_LEN);
+ }
+ } else {
+ // HW address has not been specified. This is possible when receiving
+ // packet through a logical interface (e.g. lo). In such cases, we
+ // don't want to fail but rather provide a default HW address, which
+ // consists of zeros.
+ out_buf.writeData(&std::vector<uint8_t>(HWAddr::ETHERNET_HWADDR_LEN)[0],
+ HWAddr::ETHERNET_HWADDR_LEN);
+ }
+
+ // Set source HW address.
+ HWAddrPtr local_addr = pkt->getLocalHWAddr();
+ if (local_addr) {
+ if (local_addr->hwaddr_.size() == HWAddr::ETHERNET_HWADDR_LEN) {
+ out_buf.writeData(&local_addr->hwaddr_[0],
+ HWAddr::ETHERNET_HWADDR_LEN);
+ } else {
+ isc_throw(BadValue, "invalid size of the local HW address "
+ << local_addr->hwaddr_.size() << " when constructing"
+ << " an ethernet frame header; expected size is"
+ << " " << HWAddr::ETHERNET_HWADDR_LEN);
+ }
+ } else {
+ // Provide default HW address.
+ out_buf.writeData(&std::vector<uint8_t>(HWAddr::ETHERNET_HWADDR_LEN)[0],
+ HWAddr::ETHERNET_HWADDR_LEN);
+ }
+
+ // Type IP.
+ out_buf.writeUint16(ETHERNET_TYPE_IP);
+}
+
+void
+writeIpUdpHeader(const Pkt4Ptr& pkt, util::OutputBuffer& out_buf) {
+
+ out_buf.writeUint8(0x45); // IP version 4, IP header length 5
+ out_buf.writeUint8(IPTOS_LOWDELAY); // DSCP and ECN
+ out_buf.writeUint16(28 + pkt->getBuffer().getLength()); // Total length.
+ out_buf.writeUint16(0); // Identification
+ out_buf.writeUint16(0x4000); // Disable fragmentation.
+ out_buf.writeUint8(128); // TTL
+ out_buf.writeUint8(IPPROTO_UDP); // Protocol UDP.
+ out_buf.writeUint16(0); // Temporarily set checksum to 0.
+ out_buf.writeUint32(pkt->getLocalAddr()); // Source address.
+ out_buf.writeUint32(pkt->getRemoteAddr()); // Destination address.
+
+ // Calculate pseudo header checksum. It will be necessary to compute
+ // UDP checksum.
+ // Get the UDP length. This includes udp header's and data length.
+ uint32_t udp_len = 8 + pkt->getBuffer().getLength();
+ // The magic number "8" indicates the offset where the source address
+ // is stored in the buffer. This offset is counted here from the
+ // current tail of the buffer. Starting from this offset we calculate
+ // the checksum using 8 following bytes of data. This will include
+ // 4 bytes of source address and 4 bytes of destination address.
+ // The IPPROTO_UDP and udp_len are also added up to the checksum.
+ uint16_t pseudo_hdr_checksum =
+ calcChecksum(static_cast<const uint8_t*>(out_buf.getData()) + out_buf.getLength() - 8,
+ 8, IPPROTO_UDP + udp_len);
+
+ // Calculate IP header checksum.
+ uint16_t ip_checksum = ~calcChecksum(static_cast<const uint8_t*>(out_buf.getData())
+ + out_buf.getLength() - 20, 20);
+ // Write checksum in the IP header. The offset of the checksum is 10 bytes
+ // back from the tail of the current buffer.
+ out_buf.writeUint16At(ip_checksum, out_buf.getLength() - 10);
+
+ // Start UDP header.
+ out_buf.writeUint16(pkt->getLocalPort()); // Source port.
+ out_buf.writeUint16(pkt->getRemotePort()); // Destination port.
+ out_buf.writeUint16(udp_len); // Length of the header and data.
+
+ // Checksum is calculated from the contents of UDP header, data and pseudo ip header.
+ // The magic number "6" indicates that the UDP header starts at offset 6 from the
+ // tail of the current buffer. These 6 bytes contain source and destination port
+ // as well as the length of the header.
+ uint16_t udp_checksum =
+ ~calcChecksum(static_cast<const uint8_t*>(out_buf.getData()) + out_buf.getLength() - 6, 6,
+ calcChecksum(static_cast<const uint8_t*>(pkt->getBuffer().getData()),
+ pkt->getBuffer().getLength(),
+ pseudo_hdr_checksum));
+ // Write UDP checksum.
+ out_buf.writeUint16(udp_checksum);
+}
+
+uint16_t
+calcChecksum(const uint8_t* buf, const uint32_t buf_size, uint32_t sum) {
+ uint32_t i;
+ for (i = 0; i < (buf_size & ~1U); i += 2) {
+ uint16_t chunk = buf[i] << 8 | buf[i + 1];
+ sum += chunk;
+ if (sum > 0xFFFF) {
+ sum -= 0xFFFF;
+ }
+ }
+ // If one byte has left, we also need to add it to the checksum.
+ if (i < buf_size) {
+ sum += buf[i] << 8;
+ if (sum > 0xFFFF) {
+ sum -= 0xFFFF;
+ }
+ }
+
+ return (sum);
+
+}
+
+}
+}
diff --git a/src/lib/dhcp/protocol_util.h b/src/lib/dhcp/protocol_util.h
new file mode 100644
index 0000000..b3f8085
--- /dev/null
+++ b/src/lib/dhcp/protocol_util.h
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PROTOCOL_UTIL_H
+#define PROTOCOL_UTIL_H
+
+#include <dhcp/pkt4.h>
+#include <util/buffer.h>
+
+#include <stdint.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Exception thrown when error occured during parsing packet's headers.
+///
+/// This exception is thrown when parsing link, Internet or Transport layer
+/// header has failed.
+class InvalidPacketHeader : public Exception {
+public:
+ InvalidPacketHeader(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// Size of the Ethernet frame header.
+static const size_t ETHERNET_HEADER_LEN = 14;
+/// Offset of the 2-byte word in the Ethernet packet which
+/// holds the type of the protocol it encapsulates.
+static const size_t ETHERNET_PACKET_TYPE_OFFSET = 12;
+/// This value is held in the Ethertype field of Ethernet frame
+/// and indicates that an IP packet is encapsulated with this
+/// frame. In the standard headers, there is an ETHERTYPE_IP,
+/// constant which serves the same purpose. However, it is more
+/// convenient to have our constant because we avoid
+/// inclusion of additional headers, which have different names
+/// and locations on different OSes.
+static const uint16_t ETHERNET_TYPE_IP = 0x0800;
+
+/// Minimal IPv4 header length.
+static const size_t MIN_IP_HEADER_LEN = 20;
+/// Offset in the IP header where the flags field starts.
+static const size_t IP_FLAGS_OFFSET = 6;
+/// Offset of the byte in IP header which holds the type
+/// of the protocol it encapsulates.
+static const size_t IP_PROTO_TYPE_OFFSET = 9;
+/// Offset of source address in the IPv4 header.
+static const size_t IP_SRC_ADDR_OFFSET = 12;
+
+/// UDP header length.
+static const size_t UDP_HEADER_LEN = 8;
+/// Offset within UDP header where destination port is held.
+static const size_t UDP_DEST_PORT = 2;
+
+/// @brief Decode the Ethernet header.
+///
+/// This function reads Ethernet frame header from the provided
+/// buffer at the current read position. The source HW address
+/// is read from the header and assigned as client address in
+/// the pkt object. The buffer read pointer is set to the end
+/// of the Ethernet frame header if read was successful.
+///
+/// @warning This function does not check that the provided 'pkt'
+/// pointer is valid. Caller must make sure that pointer is
+/// allocated.
+///
+/// @param buf input buffer holding header to be parsed.
+/// @param [out] pkt packet object receiving HW source address read from header.
+///
+/// @throw InvalidPacketHeader if packet header is truncated
+/// @throw BadValue if pkt object is NULL.
+void decodeEthernetHeader(util::InputBuffer& buf, Pkt4Ptr& pkt);
+
+/// @brief Decode IP and UDP header.
+///
+/// This function reads IP and UDP headers from the provided buffer
+/// at the current read position. The source and destination IP
+/// addresses and ports and read from these headers and stored in
+/// the appropriate members of the pkt object.
+///
+/// @warning This function does not check that the provided 'pkt'
+/// pointer is valid. Caller must make sure that pointer is
+/// allocated.
+///
+/// @param buf input buffer holding headers to be parsed.
+/// @param [out] pkt packet object where IP addresses and ports
+/// are stored.
+///
+/// @throw InvalidPacketHeader if packet header is truncated
+/// @throw BadValue if pkt object is NULL.
+void decodeIpUdpHeader(util::InputBuffer& buf, Pkt4Ptr& pkt);
+
+/// @brief Writes ethernet frame header into a buffer.
+///
+/// @warning This function does not check that the provided 'pkt'
+/// pointer is valid. Caller must make sure that pointer is
+/// allocated.
+///
+/// @param pkt packet object holding source and destination HW address.
+/// @param [out] out_buf buffer where a header is written.
+void writeEthernetHeader(const Pkt4Ptr& pkt,
+ util::OutputBuffer& out_buf);
+
+/// @brief Writes both IP and UDP header into output buffer
+///
+/// This utility function assembles IP and UDP packet headers for the
+/// provided DHCPv4 message. The source and destination addreses and
+/// ports stored in the pkt object are copied as source and destination
+/// addresses and ports into IP/UDP headers.
+///
+/// @warning This function does not check that the provided 'pkt'
+/// pointer is valid. Caller must make sure that pointer is
+/// allocated.
+///
+/// @param pkt DHCPv4 packet to be sent in IP packet
+/// @param [out] out_buf buffer where an IP header is written
+void writeIpUdpHeader(const Pkt4Ptr& pkt, util::OutputBuffer& out_buf);
+
+/// @brief Calculates checksum for provided buffer
+///
+/// This function returns the sum of 16-bit values from the provided
+/// buffer. If the third parameter is specified, it indicates the
+/// initial checksum value. This parameter can be a result of
+/// calcChecksum function's invocation on different data buffer.
+/// The IP or UDP checksum value is a complement of the result returned
+/// by this function. However, this function does not compute complement
+/// of the summed values. It must be calculated outside of this function
+/// before writing the value to the packet buffer.
+///
+/// The IP header checksum calculation algorithm has been defined in
+/// <a href="https://tools.ietf.org/html/rfc791#page-14">RFC 791</a>
+///
+/// @param buf buffer for which the checksum is calculated.
+/// @param buf_size size of the buffer for which checksum is calculated.
+/// @param sum initial checksum value, other values will be added to it.
+///
+/// @return calculated checksum.
+uint16_t calcChecksum(const uint8_t* buf, const uint32_t buf_size,
+ uint32_t sum = 0);
+
+}
+}
+#endif // PROTOCOL_UTIL_H
diff --git a/src/lib/dhcp/std_option_defs.h b/src/lib/dhcp/std_option_defs.h
index 839a5d9..6611f19 100644
--- a/src/lib/dhcp/std_option_defs.h
+++ b/src/lib/dhcp/std_option_defs.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,6 +16,8 @@
#define STD_OPTION_DEFS_H
#include <dhcp/option_data_types.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
namespace {
@@ -62,12 +64,13 @@ struct OptionDefParams {
// RFC 1035, section 3.1. The latter could be handled
// by OPT_FQDN_TYPE but we can't use it here because
// clients may request ASCII encoding.
-RECORD_DECL(FQDN_RECORDS, OPT_UINT8_TYPE, OPT_UINT8_TYPE, OPT_STRING_TYPE);
+RECORD_DECL(FQDN_RECORDS, OPT_UINT8_TYPE, OPT_UINT8_TYPE, OPT_UINT8_TYPE,
+ OPT_FQDN_TYPE);
/// @brief Definitions of standard DHCPv4 options.
const OptionDefParams OPTION_DEF_PARAMS4[] = {
{ "subnet-mask", DHO_SUBNET_MASK, OPT_IPV4_ADDRESS_TYPE, false, NO_RECORD_DEF, "" },
- { "time-offset", DHO_TIME_OFFSET, OPT_UINT32_TYPE, false, NO_RECORD_DEF, "" },
+ { "time-offset", DHO_TIME_OFFSET, OPT_INT32_TYPE, false, NO_RECORD_DEF, "" },
{ "routers", DHO_ROUTERS, OPT_IPV4_ADDRESS_TYPE, true, NO_RECORD_DEF, "" },
{ "time-servers", DHO_TIME_SERVERS, OPT_IPV4_ADDRESS_TYPE, true, NO_RECORD_DEF, "" },
{ "name-servers", DHO_NAME_SERVERS, OPT_IPV4_ADDRESS_TYPE,
@@ -84,7 +87,7 @@ const OptionDefParams OPTION_DEF_PARAMS4[] = {
{ "host-name", DHO_HOST_NAME, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
{ "boot-size", DHO_BOOT_SIZE, OPT_UINT16_TYPE, false, NO_RECORD_DEF, "" },
{ "merit-dump", DHO_MERIT_DUMP, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
- { "domain-name", DHO_DOMAIN_NAME, OPT_FQDN_TYPE, false, NO_RECORD_DEF, "" },
+ { "domain-name", DHO_DOMAIN_NAME, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
{ "swap-server", DHO_SWAP_SERVER, OPT_IPV4_ADDRESS_TYPE, false, NO_RECORD_DEF, "" },
{ "root-path", DHO_ROOT_PATH, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
{ "extensions-path", DHO_EXTENSIONS_PATH, OPT_STRING_TYPE,
@@ -157,11 +160,15 @@ const OptionDefParams OPTION_DEF_PARAMS4[] = {
{ "dhcp-rebinding-time", DHO_DHCP_REBINDING_TIME,
OPT_UINT32_TYPE, false, NO_RECORD_DEF, "" },
{ "vendor-class-identifier", DHO_VENDOR_CLASS_IDENTIFIER,
- OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
+ OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
{ "dhcp-client-identifier", DHO_DHCP_CLIENT_IDENTIFIER,
OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
{ "nwip-domain-name", DHO_NWIP_DOMAIN_NAME, OPT_STRING_TYPE, false, NO_RECORD_DEF, "" },
{ "nwip-suboptions", DHO_NWIP_SUBOPTIONS, OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
+ { "tftp-server-name", DHO_TFTP_SERVER_NAME, OPT_STRING_TYPE, false,
+ NO_RECORD_DEF, "" },
+ { "boot-file-name", DHO_BOOT_FILE_NAME, OPT_STRING_TYPE, false,
+ NO_RECORD_DEF, "" },
{ "user-class", DHO_USER_CLASS, OPT_BINARY_TYPE, false, NO_RECORD_DEF, "" },
{ "fqdn", DHO_FQDN, OPT_RECORD_TYPE, false, RECORD_DEF(FQDN_RECORDS), "" },
{ "dhcp-agent-options", DHO_DHCP_AGENT_OPTIONS,
@@ -213,7 +220,7 @@ RECORD_DECL(IA_NA_RECORDS, OPT_UINT32_TYPE, OPT_UINT32_TYPE, OPT_UINT32_TYPE);
RECORD_DECL(IA_PD_RECORDS, OPT_UINT32_TYPE, OPT_UINT32_TYPE, OPT_UINT32_TYPE);
// ia-prefix
RECORD_DECL(IA_PREFIX_RECORDS, OPT_UINT32_TYPE, OPT_UINT32_TYPE,
- OPT_UINT8_TYPE, OPT_BINARY_TYPE);
+ OPT_UINT8_TYPE, OPT_IPV6_ADDRESS_TYPE);
// lq-query
RECORD_DECL(LQ_QUERY_RECORDS, OPT_UINT8_TYPE, OPT_IPV6_ADDRESS_TYPE);
// lq-relay-data
@@ -223,7 +230,8 @@ RECORD_DECL(REMOTE_ID_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
// status-code
RECORD_DECL(STATUS_CODE_RECORDS, OPT_UINT16_TYPE, OPT_STRING_TYPE);
// vendor-class
-RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_BINARY_TYPE);
+RECORD_DECL(VENDOR_CLASS_RECORDS, OPT_UINT32_TYPE, OPT_UINT16_TYPE,
+ OPT_STRING_TYPE);
/// Standard DHCPv6 option definitions.
///
diff --git a/src/lib/dhcp/tests/Makefile.am b/src/lib/dhcp/tests/Makefile.am
index c868553..6b3683d 100644
--- a/src/lib/dhcp/tests/Makefile.am
+++ b/src/lib/dhcp/tests/Makefile.am
@@ -31,9 +31,12 @@ libdhcp___unittests_SOURCES += hwaddr_unittest.cc
libdhcp___unittests_SOURCES += iface_mgr_unittest.cc
libdhcp___unittests_SOURCES += libdhcp++_unittest.cc
libdhcp___unittests_SOURCES += option4_addrlst_unittest.cc
+libdhcp___unittests_SOURCES += option4_client_fqdn_unittest.cc
libdhcp___unittests_SOURCES += option6_addrlst_unittest.cc
+libdhcp___unittests_SOURCES += option6_client_fqdn_unittest.cc
libdhcp___unittests_SOURCES += option6_ia_unittest.cc
libdhcp___unittests_SOURCES += option6_iaaddr_unittest.cc
+libdhcp___unittests_SOURCES += option6_iaprefix_unittest.cc
libdhcp___unittests_SOURCES += option_int_unittest.cc
libdhcp___unittests_SOURCES += option_int_array_unittest.cc
libdhcp___unittests_SOURCES += option_data_types_unittest.cc
@@ -41,8 +44,21 @@ libdhcp___unittests_SOURCES += option_definition_unittest.cc
libdhcp___unittests_SOURCES += option_custom_unittest.cc
libdhcp___unittests_SOURCES += option_unittest.cc
libdhcp___unittests_SOURCES += option_space_unittest.cc
+libdhcp___unittests_SOURCES += option_string_unittest.cc
+libdhcp___unittests_SOURCES += option_vendor_unittest.cc
libdhcp___unittests_SOURCES += pkt4_unittest.cc
libdhcp___unittests_SOURCES += pkt6_unittest.cc
+libdhcp___unittests_SOURCES += pkt_filter_unittest.cc
+libdhcp___unittests_SOURCES += pkt_filter_inet_unittest.cc
+libdhcp___unittests_SOURCES += pkt_filter_inet6_unittest.cc
+libdhcp___unittests_SOURCES += pkt_filter_test_utils.h pkt_filter_test_utils.cc
+libdhcp___unittests_SOURCES += pkt_filter6_test_utils.h pkt_filter6_test_utils.cc
+
+if OS_LINUX
+libdhcp___unittests_SOURCES += pkt_filter_lpf_unittest.cc
+endif
+
+libdhcp___unittests_SOURCES += protocol_util_unittest.cc
libdhcp___unittests_SOURCES += duid_unittest.cc
libdhcp___unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
diff --git a/src/lib/dhcp/tests/iface_mgr_unittest.cc b/src/lib/dhcp/tests/iface_mgr_unittest.cc
index ede7abf..dc5d85b 100644
--- a/src/lib/dhcp/tests/iface_mgr_unittest.cc
+++ b/src/lib/dhcp/tests/iface_mgr_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -19,7 +19,9 @@
#include <dhcp/iface_mgr.h>
#include <dhcp/pkt6.h>
#include <dhcp/pkt_filter.h>
+#include <dhcp/tests/pkt_filter6_test_utils.h>
+#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
@@ -34,6 +36,7 @@ using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
+using namespace isc::dhcp::test;
using boost::scoped_ptr;
namespace {
@@ -74,14 +77,40 @@ public:
: open_socket_called_(false) {
}
- /// Pretends to open socket. Only records a call to this function.
- virtual int openSocket(const Iface&,
- const isc::asiolink::IOAddress&,
- const uint16_t,
- const bool,
- const bool) {
+ virtual bool isDirectResponseSupported() const {
+ return (false);
+ }
+
+ /// @brief Pretend to open a socket.
+ ///
+ /// This function doesn't open a real socket. It always returns the
+ /// same fake socket descriptor. It also records the fact that it has
+ /// been called in the public open_socket_called_ member.
+ /// As in the case of opening a real socket, this function will check
+ /// if there is another fake socket "bound" to the same address and port.
+ /// If there is, it will throw an exception. This allows to simulate the
+ /// conditions when one of the sockets can't be open because there is
+ /// a socket already open and test how IfaceMgr will handle it.
+ ///
+ /// @param iface An interface on which the socket is to be opened.
+ /// @param addr An address to which the socket is to be bound.
+ /// @param port A port to which the socket is to be bound.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool,
+ const bool) {
+ // Check if there is any other socket bound to the specified address
+ // and port on this interface.
+ const Iface::SocketCollection& sockets = iface.getSockets();
+ for (Iface::SocketCollection::const_iterator socket = sockets.begin();
+ socket != sockets.end(); ++socket) {
+ if ((socket->addr_ == addr) && (socket->port_ == port)) {
+ isc_throw(SocketConfigError, "test socket bind error");
+ }
+ }
open_socket_called_ = true;
- return (1024);
+ return (SocketInfo(addr, port, 255));
}
/// Does nothing
@@ -91,7 +120,7 @@ public:
}
/// Does nothing
- virtual int send(uint16_t, const Pkt4Ptr&) {
+ virtual int send(const Iface&, uint16_t, const Pkt4Ptr&) {
return (0);
}
@@ -103,42 +132,267 @@ public:
class NakedIfaceMgr: public IfaceMgr {
// "Naked" Interface Manager, exposes internal fields
public:
- NakedIfaceMgr() { }
- IfaceCollection & getIfacesLst() { return ifaces_; }
+
+ /// @brief Constructor.
+ NakedIfaceMgr() {
+ loDetect();
+ }
+
+ /// @brief detects name of the loopback interface
+ ///
+ /// This method detects name of the loopback interface.
+ static void loDetect() {
+ // Poor man's interface detection. It will go away as soon as proper
+ // interface detection is implemented
+ if (if_nametoindex("lo") > 0) {
+ snprintf(LOOPBACK, BUF_SIZE - 1, "lo");
+ } else if (if_nametoindex("lo0") > 0) {
+ snprintf(LOOPBACK, BUF_SIZE - 1, "lo0");
+ } else {
+ cout << "Failed to detect loopback interface. Neither "
+ << "lo nor lo0 worked. I give up." << endl;
+ FAIL();
+ }
+ }
+
+ /// @brief Returns the collection of existing interfaces.
+ IfaceCollection& getIfacesLst() { return (ifaces_); }
+
+ /// @brief This function creates fictitious interfaces with fictious
+ /// addresses.
+ ///
+ /// These interfaces can be used in tests that don't actually try
+ /// to open the sockets on these interfaces. Some tests use mock
+ /// objects to mimic sockets being open. These interfaces are
+ /// suitable for such tests.
+ void createIfaces() {
+
+ ifaces_.clear();
+
+ // local loopback
+ Iface lo = createIface("lo", 0);
+ lo.addAddress(IOAddress("127.0.0.1"));
+ lo.addAddress(IOAddress("::1"));
+ ifaces_.push_back(lo);
+ // eth0
+ Iface eth0 = createIface("eth0", 1);
+ eth0.addAddress(IOAddress("10.0.0.1"));
+ eth0.addAddress(IOAddress("fe80::3a60:77ff:fed5:cdef"));
+ eth0.addAddress(IOAddress("2001:db8:1::1"));
+ ifaces_.push_back(eth0);
+ // eth1
+ Iface eth1 = createIface("eth1", 2);
+ eth1.addAddress(IOAddress("192.0.2.3"));
+ eth1.addAddress(IOAddress("fe80::3a60:77ff:fed5:abcd"));
+ ifaces_.push_back(eth1);
+ }
+
+ /// @brief Create an object representing interface.
+ ///
+ /// Apart from creating an interface, this function also sets the
+ /// interface flags:
+ /// - loopback flag if interface name is "lo"
+ /// - up always true
+ /// - running always true
+ /// - inactive always to false
+ /// - multicast always to true
+ /// - broadcast always to false
+ ///
+ /// If one needs to modify the default flag settings, the setIfaceFlags
+ /// function should be used.
+ ///
+ /// @param name A name of the interface to be created.
+ /// @param ifindex An index of the interface to be created.
+ ///
+ /// @return An object representing interface.
+ static Iface createIface(const std::string& name, const int ifindex) {
+ Iface iface(name, ifindex);
+ if (name == "lo") {
+ iface.flag_loopback_ = true;
+ }
+ iface.flag_multicast_ = true;
+ // On BSD systems, the SO_BINDTODEVICE option is not supported.
+ // Therefore the IfaceMgr will throw an exception on attempt to
+ // open sockets on more than one broadcast-capable interface at
+ // the same time. In order to prevent this error, we mark all
+ // interfaces broadcast-incapable for unit testing.
+ iface.flag_broadcast_ = false;
+ iface.flag_up_ = true;
+ iface.flag_running_ = true;
+ iface.inactive4_ = false;
+ iface.inactive6_ = false;
+ return (iface);
+ }
+
+ /// @brief Checks if the specified interface has a socket bound to a
+ /// specified adddress.
+ ///
+ /// @param iface_name A name of the interface.
+ /// @param addr An address to be checked for binding.
+ ///
+ /// @return true if there is a socket bound to the specified address.
+ bool isBound(const std::string& iface_name, const std::string& addr) {
+ Iface* iface = getIface(iface_name);
+ if (iface == NULL) {
+ ADD_FAILURE() << "the interface " << iface_name << " doesn't exist";
+ return (false);
+ }
+ const Iface::SocketCollection& sockets = iface->getSockets();
+ for (Iface::SocketCollection::const_iterator sock = sockets.begin();
+ sock != sockets.end(); ++sock) {
+ if (sock->addr_ == IOAddress(addr)) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+ /// @brief Modify flags on the interface.
+ ///
+ /// @param name A name of the interface.
+ /// @param loopback A new value of the loopback flag.
+ /// @param up A new value of the up flag.
+ /// @param running A new value of the running flag.
+ /// @param inactive A new value of the inactive flag.
+ void setIfaceFlags(const std::string& name, const bool loopback,
+ const bool up, const bool running,
+ const bool inactive4,
+ const bool inactive6) {
+ for (IfaceMgr::IfaceCollection::iterator iface = ifaces_.begin();
+ iface != ifaces_.end(); ++iface) {
+ if (iface->getName() == name) {
+ iface->flag_loopback_ = loopback;
+ iface->flag_up_ = up;
+ iface->flag_running_ = running;
+ iface->inactive4_ = inactive4;
+ iface->inactive6_ = inactive6;
+ }
+ }
+ }
+
};
-// Dummy class for now, but this will be expanded when needed
+/// @brief A test fixture class for IfaceMgr.
+///
+/// @todo Sockets being opened by IfaceMgr tests should be managed by
+/// the test fixture. In particular, the class should close sockets after
+/// each test. Current approach where test cases are responsible for
+/// closing sockets is resource leak prone, especially in case of the
+/// test failure path.
class IfaceMgrTest : public ::testing::Test {
public:
- // These are empty for now, but let's keep them around
- IfaceMgrTest() {
+ /// @brief Constructor.
+ IfaceMgrTest()
+ : errors_count_(0) {
}
~IfaceMgrTest() {
}
+ /// @brief Tests the number of IPv6 sockets on interface
+ ///
+ /// This function checks the expected number of open IPv6 sockets on the
+ /// specified interface. On non-Linux systems, sockets are bound to a
+ /// link-local address and the number of unicast addresses specified.
+ /// On Linux systems, there is one more socket bound to a ff02::1:2
+ /// multicast address.
+ ///
+ /// @param iface An interface on which sockets are open.
+ /// @param unicast_num A number of unicast addresses bound.
+ /// @param link_local_num A number of link local addresses bound.
+ void checkSocketsCount6(const Iface& iface, const int unicast_num,
+ const int link_local_num = 1) {
+ // On local-loopback interface, there should be no sockets.
+ if (iface.flag_loopback_) {
+ ASSERT_TRUE(iface.getSockets().empty())
+ << "expected empty socket set on loopback interface "
+ << iface.getName();
+ return;
+ }
+#if defined OS_LINUX
+ // On Linux, for each link-local address there may be an
+ // additional socket opened and bound to ff02::1:2. This socket
+ // is only opened if the interface is multicast-capable.
+ ASSERT_EQ(unicast_num + (iface.flag_multicast_ ? link_local_num : 0)
+ + link_local_num, iface.getSockets().size())
+ << "invalid number of sockets on interface "
+ << iface.getName();
+#else
+ // On non-Linux, there is no additional socket.
+ ASSERT_EQ(unicast_num + link_local_num, iface.getSockets().size())
+ << "invalid number of sockets on interface "
+ << iface.getName();
+
+#endif
+ }
+
+ // Get ther number of IPv4 or IPv6 sockets on the loopback interface
+ int getOpenSocketsCount(const Iface& iface, uint16_t family) const {
+ // Get all sockets.
+ Iface::SocketCollection sockets = iface.getSockets();
+
+ // Loop through sockets and try to find the ones which match the
+ // specified type.
+ int sockets_count = 0;
+ for (Iface::SocketCollection::const_iterator sock = sockets.begin();
+ sock != sockets.end(); ++sock) {
+ // Match found, increase the counter.
+ if (sock->family_ == family) {
+ ++sockets_count;
+ }
+ }
+ return (sockets_count);
+ }
+
+ /// @brief returns socket bound to a specific address (or NULL)
+ ///
+ /// A helper function, used to pick a socketinfo that is bound to a given
+ /// address.
+ ///
+ /// @param sockets sockets collection
+ /// @param addr address the socket is bound to
+ ///
+ /// @return socket info structure (or NULL)
+ const isc::dhcp::SocketInfo*
+ getSocketByAddr(const isc::dhcp::Iface::SocketCollection& sockets,
+ const IOAddress& addr) {
+ for (isc::dhcp::Iface::SocketCollection::const_iterator s =
+ sockets.begin(); s != sockets.end(); ++s) {
+ if (s->addr_ == addr) {
+ return (&(*s));
+ }
+ }
+ return (NULL);
+ }
+
+ /// @brief Implements an IfaceMgr error handler.
+ ///
+ /// This function can be installed as an error handler for the
+ /// IfaceMgr::openSockets4 function. The error handler is invoked
+ /// when an attempt to open a particular socket fails for any reason.
+ /// Typically, the error handler will log a warning. When the error
+ /// handler returns, the openSockets4 function should continue opening
+ /// sockets on other interfaces.
+ ///
+ /// @param errmsg An error string indicating the reason for failure.
+ void ifaceMgrErrorHandler(const std::string&) {
+ // Increase the counter of invocations to this function. By checking
+ // this number, a test amy check if the expected number of errors
+ // has occurred.
+ ++errors_count_;
+ }
+
+ /// Holds the invocation counter for ifaceMgrErrorHandler.
+ int errors_count_;
+
};
// We need some known interface to work reliably. Loopback interface is named
// lo on Linux and lo0 on BSD boxes. We need to find out which is available.
// This is not a real test, but rather a workaround that will go away when
-// interface detection is implemented.
-
-// NOTE: At this stage of development, write access to current directory
-// during running tests is required.
+// interface detection is implemented on all OSes.
TEST_F(IfaceMgrTest, loDetect) {
-
- // Poor man's interface detection. It will go away as soon as proper
- // interface detection is implemented
- if (if_nametoindex("lo") > 0) {
- snprintf(LOOPBACK, BUF_SIZE - 1, "lo");
- } else if (if_nametoindex("lo0") > 0) {
- snprintf(LOOPBACK, BUF_SIZE - 1, "lo0");
- } else {
- cout << "Failed to detect loopback interface. Neither "
- << "lo nor lo0 worked. I give up." << endl;
- FAIL();
- }
+ NakedIfaceMgr::loDetect();
}
// Uncomment this test to create packet writer. It will
@@ -174,10 +428,10 @@ TEST_F(IfaceMgrTest, dhcp6Sniffer) {
cout << " pkt = new Pkt6(" << pkt->data_len_ << ");" << endl;
cout << " pkt->remote_port_ = " << pkt-> remote_port_ << ";" << endl;
cout << " pkt->remote_addr_ = IOAddress(\""
- << pkt->remote_addr_.toText() << "\");" << endl;
+ << pkt->remote_addr_ << "\");" << endl;
cout << " pkt->local_port_ = " << pkt-> local_port_ << ";" << endl;
cout << " pkt->local_addr_ = IOAddress(\""
- << pkt->local_addr_.toText() << "\");" << endl;
+ << pkt->local_addr_ << "\");" << endl;
cout << " pkt->ifindex_ = " << pkt->ifindex_ << ";" << endl;
cout << " pkt->iface_ = \"" << pkt->iface_ << "\";" << endl;
@@ -208,6 +462,66 @@ TEST_F(IfaceMgrTest, basic) {
ASSERT_TRUE(&ifacemgr != 0);
}
+
+// This test verifies that sockets can be closed selectively, i.e. all
+// IPv4 sockets can be closed first and all IPv6 sockets remain open.
+TEST_F(IfaceMgrTest, closeSockets) {
+ // Will be using local loopback addresses for this test.
+ IOAddress loaddr("127.0.0.1");
+ IOAddress loaddr6("::1");
+
+ // Create instance of IfaceMgr.
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr);
+
+ // Out constructor does not detect interfaces by itself. We need
+ // to create one and add.
+ int ifindex = if_nametoindex(LOOPBACK);
+ ASSERT_GT(ifindex, 0);
+ Iface lo_iface(LOOPBACK, ifindex);
+ iface_mgr->getIfacesLst().push_back(lo_iface);
+
+ // Create set of V4 and V6 sockets on the loopback interface.
+ // They must differ by a port they are bound to.
+ for (int i = 0; i < 6; ++i) {
+ // Every other socket will be IPv4.
+ if (i % 2) {
+ ASSERT_NO_THROW(
+ iface_mgr->openSocket(LOOPBACK, loaddr, 10000 + i)
+ );
+ } else {
+ ASSERT_NO_THROW(
+ iface_mgr->openSocket(LOOPBACK, loaddr6, 10000 + i)
+ );
+ }
+ }
+
+ // At the end we should have 3 IPv4 and 3 IPv6 sockets open.
+ Iface* iface = iface_mgr->getIface(LOOPBACK);
+ ASSERT_TRUE(iface != NULL);
+
+ int v4_sockets_count = getOpenSocketsCount(*iface, AF_INET);
+ ASSERT_EQ(3, v4_sockets_count);
+ int v6_sockets_count = getOpenSocketsCount(*iface, AF_INET6);
+ ASSERT_EQ(3, v6_sockets_count);
+
+ // Let's try to close only IPv4 sockets.
+ ASSERT_NO_THROW(iface_mgr->closeSockets(AF_INET));
+ v4_sockets_count = getOpenSocketsCount(*iface, AF_INET);
+ EXPECT_EQ(0, v4_sockets_count);
+ // The IPv6 sockets should remain open.
+ v6_sockets_count = getOpenSocketsCount(*iface, AF_INET6);
+ EXPECT_EQ(3, v6_sockets_count);
+
+ // Let's try to close IPv6 sockets.
+ ASSERT_NO_THROW(iface_mgr->closeSockets(AF_INET6));
+ v4_sockets_count = getOpenSocketsCount(*iface, AF_INET);
+ EXPECT_EQ(0, v4_sockets_count);
+ // They should have been closed now.
+ v6_sockets_count = getOpenSocketsCount(*iface, AF_INET6);
+ EXPECT_EQ(0, v6_sockets_count);
+}
+
TEST_F(IfaceMgrTest, ifaceClass) {
// Basic tests for Iface inner class
@@ -264,6 +578,25 @@ TEST_F(IfaceMgrTest, getIface) {
EXPECT_EQ(static_cast<void*>(NULL), ifacemgr->getIface("wifi15") );
}
+TEST_F(IfaceMgrTest, clearIfaces) {
+ NakedIfaceMgr ifacemgr;
+ // Create a set of fake interfaces. At the same time, remove the actual
+ // interfaces that have been detected by the IfaceMgr.
+ ifacemgr.createIfaces();
+
+ ASSERT_GT(ifacemgr.countIfaces(), 0);
+
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ ASSERT_NO_THROW(ifacemgr.openSockets4());
+
+ ifacemgr.clearIfaces();
+
+ EXPECT_EQ(0, ifacemgr.countIfaces());
+}
+
TEST_F(IfaceMgrTest, receiveTimeout6) {
using namespace boost::posix_time;
std::cout << "Testing DHCPv6 packet reception timeouts."
@@ -277,8 +610,8 @@ TEST_F(IfaceMgrTest, receiveTimeout6) {
ASSERT_NO_THROW(
socket1 = ifacemgr->openSocket(LOOPBACK, loAddr, 10547)
);
- // Socket is open if its descriptor is greater than zero.
- ASSERT_GT(socket1, 0);
+ // Socket is open if result is non-negative.
+ ASSERT_GE(socket1, 0);
// Remember when we call receive6().
ptime start_time = microsec_clock::universal_time();
@@ -329,8 +662,8 @@ TEST_F(IfaceMgrTest, receiveTimeout4) {
ASSERT_NO_THROW(
socket1 = ifacemgr->openSocket(LOOPBACK, loAddr, 10067)
);
- // Socket is open if its descriptor is greater than zero.
- ASSERT_GT(socket1, 0);
+ // Socket is open if returned value is non-negative.
+ ASSERT_GE(socket1, 0);
Pkt4Ptr pkt;
// Remember when we call receive4().
@@ -379,7 +712,7 @@ TEST_F(IfaceMgrTest, multipleSockets) {
ASSERT_NO_THROW(
socket1 = ifacemgr->openSocketFromIface(LOOPBACK, PORT1, AF_INET);
);
- ASSERT_GT(socket1, 0);
+ ASSERT_GE(socket1, 0);
init_sockets.push_back(socket1);
// Create socket #2
@@ -388,7 +721,7 @@ TEST_F(IfaceMgrTest, multipleSockets) {
ASSERT_NO_THROW(
socket2 = ifacemgr->openSocketFromRemoteAddress(loAddr, PORT2);
);
- ASSERT_GT(socket2, 0);
+ ASSERT_GE(socket2, 0);
init_sockets.push_back(socket2);
// Get loopback interface. If we don't find one we are unable to run
@@ -469,13 +802,13 @@ TEST_F(IfaceMgrTest, sockets6) {
// Bind multicast socket to port 10547
int socket1 = ifacemgr->openSocket(LOOPBACK, loAddr, 10547);
- EXPECT_GT(socket1, 0); // socket > 0
+ EXPECT_GE(socket1, 0); // socket >= 0
EXPECT_EQ(socket1, ifacemgr->getSocket(pkt6));
// Bind unicast socket to port 10548
int socket2 = ifacemgr->openSocket(LOOPBACK, loAddr, 10548);
- EXPECT_GT(socket2, 0);
+ EXPECT_GE(socket2, 0);
// Removed code for binding socket twice to the same address/port
// as it caused problems on some platforms (e.g. Mac OS X)
@@ -509,8 +842,8 @@ TEST_F(IfaceMgrTest, socketsFromIface) {
EXPECT_NO_THROW(
socket1 = ifacemgr->openSocketFromIface(LOOPBACK, PORT1, AF_INET6);
);
- // Socket descriptor must be positive integer
- EXPECT_GT(socket1, 0);
+ // Socket descriptor must be non-negative integer
+ EXPECT_GE(socket1, 0);
close(socket1);
// Open v4 socket on loopback interface and bind to different port
@@ -518,8 +851,8 @@ TEST_F(IfaceMgrTest, socketsFromIface) {
EXPECT_NO_THROW(
socket2 = ifacemgr->openSocketFromIface(LOOPBACK, PORT2, AF_INET);
);
- // socket descriptor must be positive integer
- EXPECT_GT(socket2, 0);
+ // socket descriptor must be non-negative integer
+ EXPECT_GE(socket2, 0);
close(socket2);
// Close sockets here because the following tests will want to
@@ -546,8 +879,8 @@ TEST_F(IfaceMgrTest, socketsFromAddress) {
EXPECT_NO_THROW(
socket1 = ifacemgr->openSocketFromAddress(loAddr6, PORT1);
);
- // socket descriptor must be positive integer
- EXPECT_GT(socket1, 0);
+ // socket descriptor must be non-negative integer
+ EXPECT_GE(socket1, 0);
// Open v4 socket on loopback interface and bind to different port
int socket2 = 0;
@@ -556,7 +889,7 @@ TEST_F(IfaceMgrTest, socketsFromAddress) {
socket2 = ifacemgr->openSocketFromAddress(loAddr, PORT2);
);
// socket descriptor must be positive integer
- EXPECT_GT(socket2, 0);
+ EXPECT_GE(socket2, 0);
// Close sockets here because the following tests will want to
// open sockets on the same ports.
@@ -583,7 +916,7 @@ TEST_F(IfaceMgrTest, socketsFromRemoteAddress) {
EXPECT_NO_THROW(
socket1 = ifacemgr->openSocketFromRemoteAddress(loAddr6, PORT1);
);
- EXPECT_GT(socket1, 0);
+ EXPECT_GE(socket1, 0);
// Open v4 socket to connect to remote address.
int socket2 = 0;
@@ -591,25 +924,17 @@ TEST_F(IfaceMgrTest, socketsFromRemoteAddress) {
EXPECT_NO_THROW(
socket2 = ifacemgr->openSocketFromRemoteAddress(loAddr, PORT2);
);
- EXPECT_GT(socket2, 0);
+ EXPECT_GE(socket2, 0);
// Close sockets here because the following tests will want to
// open sockets on the same ports.
ifacemgr->closeSockets();
- // The following test is currently disabled for OSes other than
- // Linux because interface detection is not implemented on them.
- // @todo enable this test for all OSes once interface detection
- // is implemented.
-#if defined(OS_LINUX)
- // Open v4 socket to connect to broadcast address.
- int socket3 = 0;
- IOAddress bcastAddr("255.255.255.255");
- EXPECT_NO_THROW(
- socket3 = ifacemgr->openSocketFromRemoteAddress(bcastAddr, PORT2);
- );
- EXPECT_GT(socket3, 0);
-#endif
+ // There used to be a check here that verified the ability to open
+ // suitable socket for sending broadcast request. However,
+ // there is no guarantee for such test to work on all systems
+ // because some systems may have no broadcast capable interfaces at all.
+ // Thus, this check has been removed.
// Do not call closeSockets() because it is called by IfaceMgr's
// virtual destructor.
@@ -628,12 +953,12 @@ TEST_F(IfaceMgrTest, DISABLED_sockets6Mcast) {
// bind multicast socket to port 10547
int socket1 = ifacemgr->openSocket(LOOPBACK, mcastAddr, 10547);
- EXPECT_GT(socket1, 0); // socket > 0
+ EXPECT_GE(socket1, 0); // socket > 0
// expect success. This address/port is already bound, but
// we are using SO_REUSEADDR, so we can bind it twice
int socket2 = ifacemgr->openSocket(LOOPBACK, mcastAddr, 10547);
- EXPECT_GT(socket2, 0);
+ EXPECT_GE(socket2, 0);
// there's no good way to test negative case here.
// we would need non-multicast interface. We will be able
@@ -659,8 +984,8 @@ TEST_F(IfaceMgrTest, sendReceive6) {
socket2 = ifacemgr->openSocket(LOOPBACK, loAddr, 10546);
);
- EXPECT_GT(socket1, 0);
- EXPECT_GT(socket2, 0);
+ EXPECT_GE(socket1, 0);
+ EXPECT_GE(socket2, 0);
// prepare dummy payload
@@ -686,23 +1011,17 @@ TEST_F(IfaceMgrTest, sendReceive6) {
ASSERT_TRUE(rcvPkt); // received our own packet
// let's check that we received what was sent
- ASSERT_EQ(sendPkt->getData().size(), rcvPkt->getData().size());
- EXPECT_EQ(0, memcmp(&sendPkt->getData()[0], &rcvPkt->getData()[0],
- rcvPkt->getData().size()));
+ ASSERT_EQ(sendPkt->data_.size(), rcvPkt->data_.size());
+ EXPECT_EQ(0, memcmp(&sendPkt->data_[0], &rcvPkt->data_[0],
+ rcvPkt->data_.size()));
- EXPECT_EQ(sendPkt->getRemoteAddr().toText(), rcvPkt->getRemoteAddr().toText());
+ EXPECT_EQ(sendPkt->getRemoteAddr(), rcvPkt->getRemoteAddr());
// since we opened 2 sockets on the same interface and none of them is multicast,
// none is preferred over the other for sending data, so we really should not
// assume the one or the other will always be chosen for sending data. Therefore
// we should accept both values as source ports.
EXPECT_TRUE((rcvPkt->getRemotePort() == 10546) || (rcvPkt->getRemotePort() == 10547));
-
- // try to send/receive data over the closed socket. Closed socket's descriptor is
- // still being hold by IfaceMgr which will try to use it to receive data.
- close(socket1);
- EXPECT_THROW(ifacemgr->receive6(10), SocketReadError);
- EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
}
TEST_F(IfaceMgrTest, sendReceive4) {
@@ -714,14 +1033,12 @@ TEST_F(IfaceMgrTest, sendReceive4) {
// let's assume that every supported OS have lo interface
IOAddress loAddr("127.0.0.1");
- int socket1 = 0, socket2 = 0;
+ int socket1 = 0;
EXPECT_NO_THROW(
socket1 = ifacemgr->openSocket(LOOPBACK, loAddr, DHCP4_SERVER_PORT + 10000);
- socket2 = ifacemgr->openSocket(LOOPBACK, loAddr, DHCP4_SERVER_PORT + 10000 + 1);
);
EXPECT_GE(socket1, 0);
- EXPECT_GE(socket2, 0);
boost::shared_ptr<Pkt4> sendPkt(new Pkt4(DHCPDISCOVER, 1234) );
@@ -754,7 +1071,7 @@ TEST_F(IfaceMgrTest, sendReceive4) {
boost::shared_ptr<Pkt4> rcvPkt;
- EXPECT_EQ(true, ifacemgr->send(sendPkt));
+ EXPECT_NO_THROW(ifacemgr->send(sendPkt));
ASSERT_NO_THROW(rcvPkt = ifacemgr->receive4(10));
ASSERT_TRUE(rcvPkt); // received our own packet
@@ -789,10 +1106,30 @@ TEST_F(IfaceMgrTest, sendReceive4) {
// assume the one or the other will always be chosen for sending data. We should
// skip checking source port of sent address.
- // try to receive data over the closed socket. Closed socket's descriptor is
- // still being hold by IfaceMgr which will try to use it to receive data.
+ // Close the socket. Further we will test if errors are reported
+ // properly on attempt to use closed soscket.
close(socket1);
+
+// Warning: kernel bug on FreeBSD. The following code checks that attempt to
+// read through invalid descriptor will result in exception. The reason why
+// this failure is expected is that select() function should result in EBADF
+// error when invalid descriptor is passed to it. In particular, closed socket
+// descriptor is invalid. On the following OS:
+//
+// 8.1-RELEASE FreeBSD 8.1-RELEASE #0: Mon Jul 19 02:55:53 UTC 2010
+//
+// calling select() using invalid descriptor results in timeout and eventually
+// value of 0 is returned. This has been identified and reported as a bug in
+// FreeBSD: http://www.freebsd.org/cgi/query-pr.cgi?pr=155606
+//
+// @todo: This part of the test is currently disabled on all BSD systems as it was
+// the quick fix. We need a more elegant (config-based) solution to disable
+// this check on affected systems only. The ticket has been submited for this
+// work: http://bind10.isc.org/ticket/2971
+#ifndef OS_BSD
EXPECT_THROW(ifacemgr->receive4(10), SocketReadError);
+#endif
+
EXPECT_THROW(ifacemgr->send(sendPkt), SocketWriteError);
}
@@ -824,10 +1161,173 @@ TEST_F(IfaceMgrTest, setPacketFilter) {
// Check that openSocket function was called.
EXPECT_TRUE(custom_packet_filter->open_socket_called_);
- // This function always returns fake socket descriptor equal to 1024.
- EXPECT_EQ(1024, socket1);
+ // This function always returns fake socket descriptor equal to 255.
+ EXPECT_EQ(255, socket1);
+
+ // Replacing current packet filter object while there are IPv4
+ // sockets open is not allowed!
+ EXPECT_THROW(iface_mgr->setPacketFilter(custom_packet_filter),
+ PacketFilterChangeDenied);
+
+ // So, let's close the open IPv4 sockets and retry. Now it should succeed.
+ iface_mgr->closeSockets(AF_INET);
+ EXPECT_NO_THROW(iface_mgr->setPacketFilter(custom_packet_filter));
+}
+
+// This test checks that the default packet filter for DHCPv6 can be replaced
+// with the custom one.
+TEST_F(IfaceMgrTest, setPacketFilter6) {
+ // Create an instance of IfaceMgr.
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr);
+
+ // Try to set NULL packet filter object and make sure it is rejected.
+ boost::shared_ptr<PktFilter6Stub> custom_packet_filter;
+ EXPECT_THROW(iface_mgr->setPacketFilter(custom_packet_filter),
+ isc::dhcp::InvalidPacketFilter);
+
+ // Create valid object and check if it can be set.
+ custom_packet_filter.reset(new PktFilter6Stub());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(iface_mgr->setPacketFilter(custom_packet_filter));
+
+ // Try to open socket using IfaceMgr. It should call the openSocket()
+ // function on the packet filter object we have set.
+ IOAddress loAddr("::1");
+ int socket1 = 0;
+ EXPECT_NO_THROW(
+ socket1 = iface_mgr->openSocket(LOOPBACK, loAddr,
+ DHCP6_SERVER_PORT + 10000);
+ );
+ // Check that openSocket function has been actually called on the packet
+ // filter object.
+ EXPECT_EQ(1, custom_packet_filter->open_socket_count_);
+ // Also check that the returned socket descriptor has an expected value.
+ EXPECT_EQ(0, socket1);
+
+ // Replacing current packet filter object, while there are sockets open,
+ // is not allowed!
+ EXPECT_THROW(iface_mgr->setPacketFilter(custom_packet_filter),
+ PacketFilterChangeDenied);
+
+ // So, let's close the IPv6 sockets and retry. Now it should succeed.
+ iface_mgr->closeSockets(AF_INET6);
+ EXPECT_NO_THROW(iface_mgr->setPacketFilter(custom_packet_filter));
+
+}
+
+
+#if defined OS_LINUX
+
+// This Linux specific test checks whether it is possible to use
+// IfaceMgr to figure out which Pakcket Filter object should be
+// used when direct responses to hosts, having no address assigned
+// are desired or not desired.
+TEST_F(IfaceMgrTest, setMatchingPacketFilter) {
+
+ // Create an instance of IfaceMgr.
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr);
+
+ // Let IfaceMgr figure out which Packet Filter to use when
+ // direct response capability is not desired. It should pick
+ // PktFilterInet.
+ EXPECT_NO_THROW(iface_mgr->setMatchingPacketFilter(false));
+ // The PktFilterInet is supposed to report lack of direct
+ // response capability.
+ EXPECT_FALSE(iface_mgr->isDirectResponseSupported());
+
+ // There is working implementation of direct responses on Linux
+ // in PktFilterLPF. It uses Linux Packet Filtering as underlying
+ // mechanism. When direct responses are desired the object of
+ // this class should be set.
+ EXPECT_NO_THROW(iface_mgr->setMatchingPacketFilter(true));
+ // This object should report that direct responses are supported.
+ EXPECT_TRUE(iface_mgr->isDirectResponseSupported());
+}
+
+// This test checks that it is not possible to open two sockets: IP/UDP
+// and raw (LPF) socket and bind to the same address and port. The
+// raw socket should be opened together with the fallback IP/UDP socket.
+// The fallback socket should fail to open when there is another IP/UDP
+// socket bound to the same address and port. Failing to open the fallback
+// socket should preclude the raw socket from being open.
+TEST_F(IfaceMgrTest, checkPacketFilterLPFSocket) {
+ IOAddress loAddr("127.0.0.1");
+ int socket1 = -1, socket2 = -1;
+ // Create two instances of IfaceMgr.
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr1(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr1);
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr2(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr2);
+
+ // Let IfaceMgr figure out which Packet Filter to use when
+ // direct response capability is not desired. It should pick
+ // PktFilterInet.
+ EXPECT_NO_THROW(iface_mgr1->setMatchingPacketFilter(false));
+ // Let's open a loopback socket with handy unpriviliged port number
+ socket1 = iface_mgr1->openSocket(LOOPBACK, loAddr,
+ DHCP4_SERVER_PORT + 10000);
+
+ EXPECT_GE(socket1, 0);
+
+ // Then the second use PkFilterLPF mode
+ EXPECT_NO_THROW(iface_mgr2->setMatchingPacketFilter(true));
+
+ // The socket is open and bound. Another attempt to open socket and
+ // bind to the same address and port should result in an exception.
+ EXPECT_THROW(
+ socket2 = iface_mgr2->openSocket(LOOPBACK, loAddr,
+ DHCP4_SERVER_PORT + 10000),
+ isc::dhcp::SocketConfigError
+ );
+ // Surprisingly we managed to open another socket. We have to close it
+ // to prevent resource leak.
+ if (socket2 >= 0) {
+ close(socket2);
+ ADD_FAILURE() << "Two sockets opened and bound to the same IP"
+ " address and UDP port";
+ }
+
+ if (socket1 >= 0) {
+ close(socket1);
+ }
+}
+
+#else
+
+// This non-Linux specific test checks whether it is possible to use
+// IfaceMgr to figure out which Pakcket Filter object should be
+// used when direct responses to hosts, having no address assigned
+// are desired or not desired. Since direct responses aren't supported
+// on systems other than Linux the function under test should always
+// set object of PktFilterInet type as current Packet Filter. This
+// object does not support direct responses. Once implementation is
+// added on non-Linux systems the OS specific version of the test
+// will be removed.
+TEST_F(IfaceMgrTest, setMatchingPacketFilter) {
+
+ // Create an instance of IfaceMgr.
+ boost::scoped_ptr<NakedIfaceMgr> iface_mgr(new NakedIfaceMgr());
+ ASSERT_TRUE(iface_mgr);
+
+ // Let IfaceMgr figure out which Packet Filter to use when
+ // direct response capability is not desired. It should pick
+ // PktFilterInet.
+ EXPECT_NO_THROW(iface_mgr->setMatchingPacketFilter(false));
+ // The PktFilterInet is supposed to report lack of direct
+ // response capability.
+ EXPECT_FALSE(iface_mgr->isDirectResponseSupported());
+
+ // On non-Linux systems, we are missing the direct traffic
+ // implementation. Therefore, we expect that PktFilterInet
+ // object will be set.
+ EXPECT_NO_THROW(iface_mgr->setMatchingPacketFilter(true));
+ // This object should report lack of direct response capability.
+ EXPECT_FALSE(iface_mgr->isDirectResponseSupported());
}
+#endif
TEST_F(IfaceMgrTest, socket4) {
@@ -842,17 +1342,608 @@ TEST_F(IfaceMgrTest, socket4) {
socket1 = ifacemgr->openSocket(LOOPBACK, loAddr, DHCP4_SERVER_PORT + 10000);
);
- EXPECT_GT(socket1, 0);
+ EXPECT_GE(socket1, 0);
Pkt4 pkt(DHCPDISCOVER, 1234);
pkt.setIface(LOOPBACK);
// Expect that we get the socket that we just opened.
- EXPECT_EQ(socket1, ifacemgr->getSocket(pkt));
+ EXPECT_EQ(socket1, ifacemgr->getSocket(pkt).sockfd_);
close(socket1);
}
+// This test verifies that IPv4 sockets are open on all interfaces (except
+// loopback), when interfaces are up, running and active (not disabled from
+// the DHCP configuration).
+TEST_F(IfaceMgrTest, openSockets4) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ // Use the custom packet filter object. This object mimics the socket
+ // opening operation - the real socket is not open.
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Simulate opening sockets using the dummy packet filter.
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, NULL));
+
+ // Expect that the sockets are open on both eth0 and eth1.
+ EXPECT_EQ(1, ifacemgr.getIface("eth0")->getSockets().size());
+ EXPECT_EQ(1, ifacemgr.getIface("eth1")->getSockets().size());
+ // Socket shouldn't have been opened on loopback.
+ EXPECT_TRUE(ifacemgr.getIface("lo")->getSockets().empty());
+}
+
+// This test verifies that the socket is not open on the interface which is
+// down, but sockets are open on all other non-loopback interfaces.
+TEST_F(IfaceMgrTest, openSockets4IfaceDown) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Boolean parameters specify that eth0 is:
+ // - not a loopback
+ // - is "down" (not up)
+ // - is not running
+ // - is active (is not inactive)
+ ifacemgr.setIfaceFlags("eth0", false, false, true, false, false);
+ ASSERT_FALSE(ifacemgr.getIface("eth0")->flag_up_);
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, NULL));
+
+ // There should be no socket on eth0 open, because interface was down.
+ EXPECT_TRUE(ifacemgr.getIface("eth0")->getSockets().empty());
+ // Expecting that the socket is open on eth1 because it was up, running
+ // and active.
+ EXPECT_EQ(1, ifacemgr.getIface("eth1")->getSockets().size());
+ // Never open socket on loopback interface.
+ EXPECT_TRUE(ifacemgr.getIface("lo")->getSockets().empty());
+}
+
+// This test verifies that the socket is not open on the interface which is
+// disabled from the DHCP configuration, but sockets are open on all other
+// non-loopback interfaces.
+TEST_F(IfaceMgrTest, openSockets4IfaceInactive) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Boolean parameters specify that eth1 is:
+ // - not a loopback
+ // - is up
+ // - is running
+ // - is inactive
+ ifacemgr.setIfaceFlags("eth1", false, true, true, true, false);
+ ASSERT_TRUE(ifacemgr.getIface("eth1")->inactive4_);
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, NULL));
+
+ // The socket on eth0 should be open because interface is up, running and
+ // active (not disabled through DHCP configuration, for example).
+ EXPECT_EQ(1, ifacemgr.getIface("eth0")->getSockets().size());
+ // There should be no socket open on eth1 because it was marked inactive.
+ EXPECT_TRUE(ifacemgr.getIface("eth1")->getSockets().empty());
+ // Sockets are not open on loopback interfaces too.
+ EXPECT_TRUE(ifacemgr.getIface("lo")->getSockets().empty());
+}
+
+// Test that exception is thrown when trying to bind a new socket to the port
+// and address which is already in use by another socket.
+TEST_F(IfaceMgrTest, openSockets4NoErrorHandler) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Open socket on eth1. The openSockets4 should detect that this
+ // socket has been already open and an attempt to open another socket
+ // and bind to this address and port should fail.
+ ASSERT_NO_THROW(ifacemgr.openSocket("eth1", IOAddress("192.0.2.3"),
+ DHCP4_SERVER_PORT));
+
+ // The function throws an exception when it tries to open a socket
+ // and bind it to the address in use.
+ EXPECT_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, NULL),
+ isc::dhcp::SocketConfigError);
+
+}
+
+// Test that the external error handler is called when trying to bind a new
+// socket to the address and port being in use. The sockets on the other
+// interfaces should open just fine.
+TEST_F(IfaceMgrTest, openSocket4ErrorHandler) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Open socket on eth0.
+ ASSERT_NO_THROW(ifacemgr.openSocket("eth0", IOAddress("10.0.0.1"),
+ DHCP4_SERVER_PORT));
+
+ // Install an error handler before trying to open sockets. This handler
+ // should be called when the IfaceMgr fails to open socket on eth0.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&IfaceMgrTest::ifaceMgrErrorHandler, this, _1);
+ // The openSockets4 should detect that there is another socket already
+ // open and bound to the same address and port. An attempt to open
+ // another socket and bind to this address and port should fail.
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, error_handler));
+ // We expect that an error occured when we tried to open a socket on
+ // eth0, but the socket on eth1 should open just fine.
+ EXPECT_EQ(1, errors_count_);
+
+ // Reset errors count.
+ errors_count_ = 0;
+
+ // Now that we have two sockets open, we can try this again but this time
+ // we should get two errors: one when opening a socket on eth0, another one
+ // when opening a socket on eth1.
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, error_handler));
+ EXPECT_EQ(2, errors_count_);
+
+}
+
+// This test verifies that the function correctly checks that the v4 socket is
+// open and bound to a specific address.
+TEST_F(IfaceMgrTest, hasOpenSocketForAddress4) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ // Use the custom packet filter object. This object mimics the socket
+ // opening operation - the real socket is not open.
+ boost::shared_ptr<TestPktFilter> custom_packet_filter(new TestPktFilter());
+ ASSERT_TRUE(custom_packet_filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(custom_packet_filter));
+
+ // Simulate opening sockets using the dummy packet filter.
+ ASSERT_NO_THROW(ifacemgr.openSockets4(DHCP4_SERVER_PORT, true, NULL));
+
+ // Expect that the sockets are open on both eth0 and eth1.
+ ASSERT_EQ(1, ifacemgr.getIface("eth0")->getSockets().size());
+ ASSERT_EQ(1, ifacemgr.getIface("eth1")->getSockets().size());
+ // Socket shouldn't have been opened on loopback.
+ ASSERT_TRUE(ifacemgr.getIface("lo")->getSockets().empty());
+
+ // Check that there are sockets bound to addresses that we have
+ // set for interfaces.
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(IOAddress("192.0.2.3")));
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(IOAddress("10.0.0.1")));
+ // Check that there is no socket for the address which is not
+ // configured on any interface.
+ EXPECT_FALSE(ifacemgr.hasOpenSocket(IOAddress("10.1.1.1")));
+
+ // Check that v4 sockets are open, but no v6 socket is open.
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(AF_INET));
+ EXPECT_FALSE(ifacemgr.hasOpenSocket(AF_INET6));
+
+}
+
+// This test checks that the sockets are open and bound to link local addresses
+// only, if unicast addresses are not specified.
+TEST_F(IfaceMgrTest, openSockets6LinkLocal) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that the number of sockets is correct on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0);
+
+ // Sockets on eth0 should be bound to link-local and should not be bound
+ // to global unicast address, even though this address is configured on
+ // the eth0.
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ // Socket on eth1 should be bound to link local only.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+ EXPECT_TRUE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+}
+
+// This test checks that socket is not open on the interface which doesn't
+// have a link-local address.
+TEST_F(IfaceMgrTest, openSockets6NoLinkLocal) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Remove a link local address from eth0. If there is no link-local
+ // address, the socket should not open.
+ ASSERT_TRUE(ifacemgr.getIface("eth0")->
+ delAddress(IOAddress("fe80::3a60:77ff:fed5:cdef")));
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that the number of sockets is correct on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ // The thrid parameter specifies that the number of link-local
+ // addresses for eth0 is equal to 0.
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 0, 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0, 1);
+
+ // There should be no sockets open on eth0 because it neither has
+ // link-local nor global unicast addresses.
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ // Socket on eth1 should be bound to link local only.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_FALSE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+
+}
+
+// This test checks that socket is open on the non-muticast-capable
+// interface. This socket simply doesn't join multicast group.
+TEST_F(IfaceMgrTest, openSockets6NotMulticast) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Make eth0 multicast-incapable.
+ ifacemgr.getIface("eth0")->flag_multicast_ = false;
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that the number of sockets is correct on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0);
+
+ // Sockets on eth0 should be bound to link-local and should not be bound
+ // to global unicast address, even though this address is configured on
+ // the eth0.
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ // The eth0 is not a multicast-capable interface, so the socket should
+ // not be bound to the multicast address.
+ EXPECT_FALSE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+ // Socket on eth1 should be bound to link local only.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+ // on eth1.
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+}
+
+// This test checks that the sockets are opened and bound to link local
+// and unicast addresses which have been explicitly specified.
+TEST_F(IfaceMgrTest, openSockets6Unicast) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Configure the eth0 to open socket on the unicast address, apart
+ // from link-local address.
+ ifacemgr.getIface("eth0")->addUnicast(IOAddress("2001:db8:1::1"));
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that we have correct number of sockets on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 1); // one unicast address.
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0);
+
+ // eth0 should have two sockets, one bound to link-local, another one
+ // bound to unicast address.
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ // eth1 should have one socket, bound to link-local address.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+ EXPECT_TRUE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+
+}
+
+// This test checks that the socket is open and bound to a global unicast
+// address if the link-local address does not exist for the particular
+// interface.
+TEST_F(IfaceMgrTest, openSockets6UnicastOnly) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Configure the eth0 to open socket on the unicast address, apart
+ // from link-local address.
+ ifacemgr.getIface("eth0")->addUnicast(IOAddress("2001:db8:1::1"));
+ // Explicitly remove the link-local address from eth0.
+ ASSERT_TRUE(ifacemgr.getIface("eth0")->
+ delAddress(IOAddress("fe80::3a60:77ff:fed5:cdef")));
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that we have correct number of sockets on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 1, 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0);
+
+ // The link-local address is not present on eth0. Therefore, no socket
+ // must be bound to this address, nor to multicast address.
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+ // There should be one socket bound to unicast address.
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ // eth1 should have one socket, bound to link-local address.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+
+}
+
+// This test checks that no sockets are open for the interface which is down.
+TEST_F(IfaceMgrTest, openSockets6IfaceDown) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Configure the eth0 to open socket on the unicast address, apart
+ // from link-local address.
+ ifacemgr.getIface("eth0")->addUnicast(IOAddress("2001:db8:1::1"));
+
+ // Boolean parameters specify that eth0 is:
+ // - not a loopback
+ // - is "down" (not up)
+ // - is not running
+ // - is active for both v4 and v6
+ ifacemgr.setIfaceFlags("eth0", false, false, false, false, false);
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that we have correct number of sockets on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ // There should be no sockets on eth0 because interface is down.
+ ASSERT_TRUE(ifacemgr.getIface("eth0")->getSockets().empty());
+ checkSocketsCount6(*ifacemgr.getIface("eth1"), 0);
+
+ // eth0 should have no sockets because the interface is down.
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+ EXPECT_FALSE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+ // eth1 should have one socket, bound to link-local address.
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+
+}
+
+// This test checks that no sockets are open for the interface which is
+// inactive.
+TEST_F(IfaceMgrTest, openSockets6IfaceInactive) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Configure the eth0 to open socket on the unicast address, apart
+ // from link-local address.
+ ifacemgr.getIface("eth0")->addUnicast(IOAddress("2001:db8:1::1"));
+
+ // Boolean parameters specify that eth1 is:
+ // - not a loopback
+ // - is up
+ // - is running
+ // - is active for v4
+ // - is inactive for v6
+ ifacemgr.setIfaceFlags("eth1", false, true, true, false, true);
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Check that we have correct number of sockets on each interface.
+ checkSocketsCount6(*ifacemgr.getIface("lo"), 0);
+ checkSocketsCount6(*ifacemgr.getIface("eth0"), 1); // one unicast address
+ // There should be no sockets on eth1 because interface is inactive
+ ASSERT_TRUE(ifacemgr.getIface("eth1")->getSockets().empty());
+
+ // eth0 should have one socket bound to a link-local address, another one
+ // bound to unicast address.
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_TRUE(ifacemgr.isBound("eth0", "2001:db8:1::1"));
+
+ // eth1 shouldn't have a socket bound to link local address because
+ // interface is inactive.
+ EXPECT_FALSE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+ EXPECT_FALSE(ifacemgr.isBound("eth1", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+
+ // If we are on Linux, there is one more socket bound to ff02::1:2
+#if defined OS_LINUX
+ EXPECT_TRUE(ifacemgr.isBound("eth0", ALL_DHCP_RELAY_AGENTS_AND_SERVERS));
+#endif
+
+}
+
+// Test that the openSockets6 function does not throw if there are no interfaces
+// detected. This function is expected to return false.
+TEST_F(IfaceMgrTest, openSockets6NoIfaces) {
+ NakedIfaceMgr ifacemgr;
+ // Remove existing interfaces.
+ ifacemgr.getIfacesLst().clear();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // This value indicates if at least one socket opens. There are no
+ // interfaces, so it should be set to false.
+ bool socket_open = false;
+ ASSERT_NO_THROW(socket_open = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_FALSE(socket_open);
+}
+
+// Test that the external error handler is called when trying to bind a new
+// socket to the address and port being in use. The sockets on the other
+// interfaces should open just fine.
+TEST_F(IfaceMgrTest, openSocket6ErrorHandler) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Open socket on eth0.
+ ASSERT_NO_THROW(ifacemgr.openSocket("eth0",
+ IOAddress("fe80::3a60:77ff:fed5:cdef"),
+ DHCP6_SERVER_PORT));
+
+ // Install an error handler before trying to open sockets. This handler
+ // should be called when the IfaceMgr fails to open socket on eth0.
+ isc::dhcp::IfaceMgrErrorMsgCallback error_handler =
+ boost::bind(&IfaceMgrTest::ifaceMgrErrorHandler, this, _1);
+ // The openSockets6 should detect that a socket has been already
+ // opened on eth0 and an attempt to open another socket and bind to
+ // the same address and port should fail.
+ ASSERT_NO_THROW(ifacemgr.openSockets6(DHCP6_SERVER_PORT, error_handler));
+ // We expect that an error occured when we tried to open a socket on
+ // eth0, but the socket on eth1 should open just fine.
+ EXPECT_EQ(1, errors_count_);
+
+ // Reset errors count.
+ errors_count_ = 0;
+
+ // Now that we have two sockets open, we can try this again but this time
+ // we should get two errors: one when opening a socket on eth0, another one
+ // when opening a socket on eth1.
+ ASSERT_NO_THROW(ifacemgr.openSockets6(DHCP6_SERVER_PORT, error_handler));
+ EXPECT_EQ(2, errors_count_);
+
+}
+
+// This test verifies that the function correctly checks that the v6 socket is
+// open and bound to a specific address.
+TEST_F(IfaceMgrTest, hasOpenSocketForAddress6) {
+ NakedIfaceMgr ifacemgr;
+
+ // Remove all real interfaces and create a set of dummy interfaces.
+ ifacemgr.createIfaces();
+
+ boost::shared_ptr<PktFilter6Stub> filter(new PktFilter6Stub());
+ ASSERT_TRUE(filter);
+ ASSERT_NO_THROW(ifacemgr.setPacketFilter(filter));
+
+ // Simulate opening sockets using the dummy packet filter.
+ bool success = false;
+ ASSERT_NO_THROW(success = ifacemgr.openSockets6(DHCP6_SERVER_PORT));
+ EXPECT_TRUE(success);
+
+ // Make sure that the sockets are bound as expected.
+ ASSERT_TRUE(ifacemgr.isBound("eth0", "fe80::3a60:77ff:fed5:cdef"));
+ EXPECT_TRUE(ifacemgr.isBound("eth1", "fe80::3a60:77ff:fed5:abcd"));
+
+ // There should be v6 sockets only, no v4 sockets.
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(AF_INET6));
+ EXPECT_FALSE(ifacemgr.hasOpenSocket(AF_INET));
+
+ // Check that there are sockets bound to the addresses we have configured
+ // for interfaces.
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(IOAddress("fe80::3a60:77ff:fed5:cdef")));
+ EXPECT_TRUE(ifacemgr.hasOpenSocket(IOAddress("fe80::3a60:77ff:fed5:abcd")));
+ // Check that there is no socket bound to the address which hasn't been
+ // configured on any interface.
+ EXPECT_FALSE(ifacemgr.hasOpenSocket(IOAddress("fe80::3a60:77ff:feed:1")));
+
+}
+
// Test the Iface structure itself
TEST_F(IfaceMgrTest, iface) {
boost::scoped_ptr<Iface> iface;
@@ -926,18 +2017,28 @@ TEST_F(IfaceMgrTest, iface_methods) {
TEST_F(IfaceMgrTest, socketInfo) {
// Check that socketinfo for IPv4 socket is functional
- SocketInfo sock1(7, IOAddress("192.0.2.56"), DHCP4_SERVER_PORT + 7);
+ SocketInfo sock1(IOAddress("192.0.2.56"), DHCP4_SERVER_PORT + 7, 7);
EXPECT_EQ(7, sock1.sockfd_);
+ EXPECT_EQ(-1, sock1.fallbackfd_);
EXPECT_EQ("192.0.2.56", sock1.addr_.toText());
EXPECT_EQ(AF_INET, sock1.family_);
EXPECT_EQ(DHCP4_SERVER_PORT + 7, sock1.port_);
+ // Check that non-default value of the fallback socket descriptor is set
+ SocketInfo sock2(IOAddress("192.0.2.53"), DHCP4_SERVER_PORT + 8, 8, 10);
+ EXPECT_EQ(8, sock2.sockfd_);
+ EXPECT_EQ(10, sock2.fallbackfd_);
+ EXPECT_EQ("192.0.2.53", sock2.addr_.toText());
+ EXPECT_EQ(AF_INET, sock2.family_);
+ EXPECT_EQ(DHCP4_SERVER_PORT + 8, sock2.port_);
+
// Check that socketinfo for IPv6 socket is functional
- SocketInfo sock2(9, IOAddress("2001:db8:1::56"), DHCP4_SERVER_PORT + 9);
- EXPECT_EQ(9, sock2.sockfd_);
- EXPECT_EQ("2001:db8:1::56", sock2.addr_.toText());
- EXPECT_EQ(AF_INET6, sock2.family_);
- EXPECT_EQ(DHCP4_SERVER_PORT + 9, sock2.port_);
+ SocketInfo sock3(IOAddress("2001:db8:1::56"), DHCP4_SERVER_PORT + 9, 9);
+ EXPECT_EQ(9, sock3.sockfd_);
+ EXPECT_EQ(-1, sock3.fallbackfd_);
+ EXPECT_EQ("2001:db8:1::56", sock3.addr_.toText());
+ EXPECT_EQ(AF_INET6, sock3.family_);
+ EXPECT_EQ(DHCP4_SERVER_PORT + 9, sock3.port_);
// Now let's test if IfaceMgr handles socket info properly
scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
@@ -945,6 +2046,7 @@ TEST_F(IfaceMgrTest, socketInfo) {
ASSERT_TRUE(loopback);
loopback->addSocket(sock1);
loopback->addSocket(sock2);
+ loopback->addSocket(sock3);
Pkt6 pkt6(DHCPV6_REPLY, 123456);
@@ -995,10 +2097,11 @@ TEST_F(IfaceMgrTest, socketInfo) {
// Socket info is set, packet has well defined interface. It should work.
pkt4.setIface(LOOPBACK);
- EXPECT_EQ(7, ifacemgr->getSocket(pkt4));
+ EXPECT_EQ(7, ifacemgr->getSocket(pkt4).sockfd_);
EXPECT_NO_THROW(
ifacemgr->getIface(LOOPBACK)->delSocket(7);
+ ifacemgr->getIface(LOOPBACK)->delSocket(8);
);
// It should throw again, there's no usable socket anymore.
@@ -1194,7 +2297,7 @@ void parse_ifconfig(const std::string& textFile, IfaceMgr::IfaceCollection& ifac
// TODO: temporarily disabled, see ticket #1529
TEST_F(IfaceMgrTest, DISABLED_detectIfaces_linux) {
- NakedIfaceMgr* ifacemgr = new NakedIfaceMgr();
+ scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
IfaceMgr::IfaceCollection& detectedIfaces = ifacemgr->getIfacesLst();
const std::string textFile = "ifconfig.txt";
@@ -1233,7 +2336,7 @@ TEST_F(IfaceMgrTest, DISABLED_detectIfaces_linux) {
const Iface::AddressCollection& addrs = i->getAddresses();
for (Iface::AddressCollection::const_iterator a= addrs.begin();
a != addrs.end(); ++a) {
- cout << a->toText() << " ";
+ cout << *a << " ";
}
cout << endl;
}
@@ -1291,7 +2394,7 @@ TEST_F(IfaceMgrTest, DISABLED_detectIfaces_linux) {
<< " address on " << detected->getFullName() << " interface." << endl;
FAIL();
}
- cout << "Address " << addr->toText() << " on interface " << detected->getFullName()
+ cout << "Address " << *addr << " on interface " << detected->getFullName()
<< " matched with 'ifconfig -a' output." << endl;
}
}
@@ -1299,10 +2402,192 @@ TEST_F(IfaceMgrTest, DISABLED_detectIfaces_linux) {
FAIL();
}
}
+}
+#endif
- delete ifacemgr;
+#if defined(OS_BSD)
+#include <net/if_dl.h>
+#endif
+
+#include <sys/socket.h>
+#include <net/if.h>
+#include <ifaddrs.h>
+
+/// @brief Checks the index of this interface
+/// @param iface Kea interface structure to be checked
+/// @param ifptr original structure returned by getifaddrs
+/// @return true if index is returned properly
+bool
+checkIfIndex(const Iface & iface,
+ struct ifaddrs *& ifptr) {
+ return (iface.getIndex() == if_nametoindex(ifptr->ifa_name));
+}
+
+/// @brief Checks if the interface has proper flags set
+/// @param iface Kea interface structure to be checked
+/// @param ifptr original structure returned by getifaddrs
+/// @return true if flags are set properly
+bool
+checkIfFlags(const Iface & iface,
+ struct ifaddrs *& ifptr) {
+ bool flag_loopback_ = ifptr->ifa_flags & IFF_LOOPBACK;
+ bool flag_up_ = ifptr->ifa_flags & IFF_UP;
+ bool flag_running_ = ifptr->ifa_flags & IFF_RUNNING;
+ bool flag_multicast_ = ifptr->ifa_flags & IFF_MULTICAST;
+
+ return ((iface.flag_loopback_ == flag_loopback_) &&
+ (iface.flag_up_ == flag_up_) &&
+ (iface.flag_running_ == flag_running_) &&
+ (iface.flag_multicast_ == flag_multicast_));
}
+
+/// @brief Checks if MAC Address/IP Addresses are properly well formed
+/// @param iface Kea interface structure to be checked
+/// @param ifptr original structure returned by getifaddrs
+/// @return true if addresses are returned properly
+bool
+checkIfAddrs(const Iface & iface, struct ifaddrs *& ifptr) {
+ const unsigned char * p = 0;
+#if defined(OS_LINUX)
+ // Workaround for Linux ...
+ if(ifptr->ifa_data != 0) {
+ // We avoid localhost as it has no MAC Address
+ if (!strncmp(iface.getName().c_str(), "lo", 2)) {
+ return (true);
+ }
+
+ struct ifreq ifr;
+ memset(& ifr.ifr_name, 0, sizeof ifr.ifr_name);
+ strncpy(ifr.ifr_name, iface.getName().c_str(), sizeof ifr.ifr_name);
+
+ int s = -1; // Socket descriptor
+
+ if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
+ isc_throw(Unexpected, "Cannot create AF_INET socket");
+ }
+
+ if (ioctl(s, SIOCGIFHWADDR, & ifr) < 0) {
+ close(s);
+ isc_throw(Unexpected, "Cannot set SIOCGIFHWADDR flag");
+ }
+
+ const uint8_t * p =
+ reinterpret_cast<uint8_t *>(ifr.ifr_ifru.ifru_hwaddr.sa_data);
+
+ close(s);
+
+ /// @todo: Check MAC address length. For some interfaces it is
+ /// different than 6. Some have 0, while some exotic ones (like
+ /// infiniband) have 20.
+ return (!memcmp(p, iface.getMac(), iface.getMacLen()));
+ }
+#endif
+
+ if(!ifptr->ifa_addr) {
+ return (false);
+ }
+
+ switch(ifptr->ifa_addr->sa_family) {
+#if defined(OS_BSD)
+ case AF_LINK: {
+ // We avoid localhost as it has no MAC Address
+ if (!strncmp(iface.getName().c_str(), "lo", 2)) {
+ return (true);
+ }
+
+ struct sockaddr_dl * hwdata =
+ reinterpret_cast<struct sockaddr_dl *>(ifptr->ifa_addr);
+ p = reinterpret_cast<uint8_t *>(LLADDR(hwdata));
+
+ // Extract MAC address length
+ if (hwdata->sdl_alen != iface.getMacLen()) {
+ return (false);
+ }
+
+ return (!memcmp(p, iface.getMac(), hwdata->sdl_alen));
+ }
#endif
+ case AF_INET: {
+ struct sockaddr_in * v4data =
+ reinterpret_cast<struct sockaddr_in *>(ifptr->ifa_addr);
+ p = reinterpret_cast<uint8_t *>(& v4data->sin_addr);
+
+ IOAddress addrv4 = IOAddress::fromBytes(AF_INET, p);
+
+ for (Iface::AddressCollection::const_iterator a =
+ iface.getAddresses().begin();
+ a != iface.getAddresses().end(); ++ a) {
+ if(a->isV4() && (*a) == addrv4) {
+ return (true);
+ }
+ }
+
+ return (false);
+ }
+ case AF_INET6: {
+ struct sockaddr_in6 * v6data =
+ reinterpret_cast<struct sockaddr_in6 *>(ifptr->ifa_addr);
+ p = reinterpret_cast<uint8_t *>(& v6data->sin6_addr);
+
+ IOAddress addrv6 = IOAddress::fromBytes(AF_INET6, p);
+
+ for(Iface::AddressCollection::const_iterator a =
+ iface.getAddresses().begin();
+ a != iface.getAddresses().end(); ++ a) {
+ if(a->isV6() && (*a) == addrv6) {
+ return (true);
+ }
+ }
+
+ return (false);
+ }
+ default:
+ return (true);
+ }
+}
+
+TEST_F(IfaceMgrTest, detectIfaces) {
+
+ NakedIfaceMgr* ifacemgr = new NakedIfaceMgr();
+ IfaceMgr::IfaceCollection& detectedIfaces = ifacemgr->getIfacesLst();
+
+ // We are using struct ifaddrs as it is the only good portable one
+ // ifreq and ioctls are far from portabe. For BSD ifreq::ifa_flags field
+ // is only a short which, nowadays, can be negative
+ struct ifaddrs * iflist = 0, * ifptr = 0;
+
+ if(getifaddrs(& iflist) != 0) {
+ isc_throw(Unexpected, "Cannot detect interfaces");
+ }
+
+ for (ifptr = iflist; ifptr != 0; ifptr = ifptr->ifa_next) {
+ for (IfaceMgr::IfaceCollection::const_iterator i = detectedIfaces.begin();
+ i != detectedIfaces.end(); ++ i) {
+ if (!strncmp(ifptr->ifa_name, (*i).getName().c_str(),
+ (*i).getName().size())) {
+
+ // Typically unit-tests try to be silent. But interface detection
+ // is tricky enough to warrant additional prints.
+ std::cout << "Checking interface " << i->getName() << std::endl;
+
+ // Check if interface index is reported properly
+ EXPECT_TRUE(checkIfIndex(*i, ifptr));
+
+ // Check if flags are reported properly
+ EXPECT_TRUE(checkIfFlags(*i, ifptr));
+
+ // Check if addresses are reported properly
+ EXPECT_TRUE(checkIfAddrs(*i, ifptr));
+
+ }
+ }
+ }
+
+ freeifaddrs(iflist);
+ iflist = 0;
+
+ delete ifacemgr;
+}
volatile bool callback_ok;
@@ -1350,4 +2635,116 @@ TEST_F(IfaceMgrTest, controlSession) {
close(pipefd[0]);
}
+// Test checks if the unicast sockets can be opened.
+// This test is now disabled, because there is no reliable way to test it. We
+// can't even use loopback, beacuse openSockets() skips loopback interface
+// (as it should be, because DHCP server is not supposed to listen on loopback).
+TEST_F(IfaceMgrTest, DISABLED_openUnicastSockets) {
+ /// @todo Need to implement a test that is able to check whether we can open
+ /// unicast sockets. There are 2 problems with it:
+ /// 1. We need to have a non-link-local address on an interface that is
+ /// up, running, IPv6 and multicast capable
+ /// 2. We need that information on every OS that we run tests on. So far
+ /// we are only supporting interface detection in Linux.
+ ///
+ /// To achieve this, we will probably need a pre-test setup, similar to what
+ /// BIND9 is doing (i.e. configuring well known addresses on loopback).
+
+ scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
+
+ // Get the interface (todo: which interface)
+ Iface* iface = ifacemgr->getIface("eth0");
+ ASSERT_TRUE(iface);
+ iface->inactive6_ = false;
+
+ // Tell the interface that it should bind to this global interface
+ EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
+
+ // Tell IfaceMgr to open sockets. This should trigger at least 2 sockets
+ // to open on eth0: link-local and global. On some systems (Linux), an
+ // additional socket for multicast may be opened.
+ EXPECT_TRUE(ifacemgr->openSockets6(PORT1));
+
+ const Iface::SocketCollection& sockets = iface->getSockets();
+ ASSERT_GE(2, sockets.size());
+
+ // Global unicast should be first
+ EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("2001:db8::1")));
+ EXPECT_TRUE(getSocketByAddr(sockets, IOAddress("figure-out-link-local-addr")));
+}
+
+// Checks if there is a protection against unicast duplicates.
+TEST_F(IfaceMgrTest, unicastDuplicates) {
+ NakedIfaceMgr ifacemgr;
+
+ Iface* iface = ifacemgr.getIface(LOOPBACK);
+ if (iface == NULL) {
+ cout << "Local loopback interface not found. Skipping test. " << endl;
+ return;
+ }
+
+ // Tell the interface that it should bind to this global interface
+ EXPECT_NO_THROW(iface->addUnicast(IOAddress("2001:db8::1")));
+
+ // Tell the interface that it should bind to this global interface
+ EXPECT_THROW(iface->addUnicast(IOAddress("2001:db8::1")), BadValue);
+}
+
+// This test requires addresses 2001:db8:15c::1/128 and fe80::1/64 to be
+// configured on loopback interface
+//
+// Useful commands:
+// ip a a 2001:db8:15c::1/128 dev lo
+// ip a a fe80::1/64 dev lo
+//
+// If you do not issue those commands before running this test, it will fail.
+TEST_F(IfaceMgrTest, DISABLED_getSocket) {
+ // Testing socket operation in a portable way is tricky
+ // without interface detection implemented.
+
+ scoped_ptr<NakedIfaceMgr> ifacemgr(new NakedIfaceMgr());
+
+ IOAddress lo_addr("::1");
+ IOAddress link_local("fe80::1");
+ IOAddress global("2001:db8:15c::1");
+
+ IOAddress dst_link_local("fe80::dead:beef");
+ IOAddress dst_global("2001:db8:15c::dead:beef");
+
+ // Bind loopback address
+ int socket1 = ifacemgr->openSocket(LOOPBACK, lo_addr, 10547);
+ EXPECT_GE(socket1, 0); // socket >= 0
+
+ // Bind link-local address
+ int socket2 = ifacemgr->openSocket(LOOPBACK, link_local, 10547);
+ EXPECT_GE(socket2, 0);
+
+ int socket3 = ifacemgr->openSocket(LOOPBACK, global, 10547);
+ EXPECT_GE(socket3, 0);
+
+ // Let's make sure those sockets are unique
+ EXPECT_NE(socket1, socket2);
+ EXPECT_NE(socket2, socket3);
+ EXPECT_NE(socket3, socket1);
+
+ // Create a packet
+ Pkt6 pkt6(DHCPV6_SOLICIT, 123);
+ pkt6.setIface(LOOPBACK);
+
+ // Check that packets sent to link-local will get socket bound to link local
+ pkt6.setLocalAddr(global);
+ pkt6.setRemoteAddr(dst_global);
+ EXPECT_EQ(socket3, ifacemgr->getSocket(pkt6));
+
+ // Check that packets sent to link-local will get socket bound to link local
+ pkt6.setLocalAddr(link_local);
+ pkt6.setRemoteAddr(dst_link_local);
+ EXPECT_EQ(socket2, ifacemgr->getSocket(pkt6));
+
+ // Close sockets here because the following tests will want to
+ // open sockets on the same ports.
+ ifacemgr->closeSockets();
+}
+
+
}
diff --git a/src/lib/dhcp/tests/libdhcp++_unittest.cc b/src/lib/dhcp/tests/libdhcp++_unittest.cc
index d3f5717..4d645a4 100644
--- a/src/lib/dhcp/tests/libdhcp++_unittest.cc
+++ b/src/lib/dhcp/tests/libdhcp++_unittest.cc
@@ -18,13 +18,21 @@
#include <dhcp/dhcp6.h>
#include <dhcp/libdhcp++.h>
#include <dhcp/option4_addrlst.h>
+#include <dhcp/option4_client_fqdn.h>
#include <dhcp/option6_addrlst.h>
+#include <dhcp/option6_client_fqdn.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
#include <dhcp/option_custom.h>
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
+#include <dhcp/option_string.h>
+#include <dhcp/option_vendor.h>
#include <util/buffer.h>
+#include <util/encode/hex.h>
+
+#include <boost/pointer_cast.hpp>
#include <gtest/gtest.h>
@@ -39,6 +47,12 @@ using namespace isc::dhcp;
using namespace isc::util;
namespace {
+
+// DHCPv6 suboptions of Vendor Options Option.
+/// @todo move to src/lib/dhcp/docsis3_option_defs.h once #3194 is merged.
+const uint16_t OPTION_CMTS_CAPS = 1025;
+const uint16_t OPTION_CM_MAC = 1026;
+
class LibDhcpTest : public ::testing::Test {
public:
LibDhcpTest() { }
@@ -102,6 +116,33 @@ public:
testStdOptionDefs(Option::V6, code, begin, end, expected_type,
encapsulates);
}
+
+ /// @brief Create a sample DHCPv4 option 43 with suboptions.
+ static OptionBuffer createVendorOption() {
+ const uint8_t opt_data[] = {
+ 0x2B, 0x0D, // Vendor-Specific Information (CableLabs)
+ // Suboptions start here...
+ 0x02, 0x05, // Device Type Option (length = 5)
+ 'D', 'u', 'm', 'm', 'y',
+ 0x04, 0x04, // Serial Number Option (length = 4)
+ 0x42, 0x52, 0x32, 0x32 // Serial number
+ };
+ return (OptionBuffer(opt_data, opt_data + sizeof(opt_data)));
+ }
+
+ /// @brief Create a sample DHCPv4 option 82 with suboptions.
+ static OptionBuffer createAgentInformationOption() {
+ const uint8_t opt_data[] = {
+ 0x52, 0x0E, // Agent Information Option (length = 14)
+ // Suboptions start here...
+ 0x01, 0x04, // Agent Circuit ID (length = 4)
+ 0x20, 0x00, 0x00, 0x02, // ID
+ 0x02, 0x06, // Agent Remote ID
+ 0x20, 0xE5, 0x2A, 0xB8, 0x15, 0x14 // ID
+ };
+ return (OptionBuffer(opt_data, opt_data + sizeof(opt_data)));
+ }
+
private:
/// @brief Test DHCPv4 or DHCPv6 option definition.
@@ -174,7 +215,19 @@ const uint8_t v6packed[] = {
0, 2, 0, 3, 105, 106, 107, // SERVER_ID (7 bytes)
0, 14, 0, 0, // RAPID_COMMIT (0 bytes)
0, 6, 0, 4, 108, 109, 110, 111, // ORO (8 bytes)
- 0, 8, 0, 2, 112, 113 // ELAPSED_TIME (6 bytes)
+ 0, 8, 0, 2, 112, 113, // ELAPSED_TIME (6 bytes)
+ // Vendor Specific Information Option starts here
+ 0x00, 0x11, // VSI Option Code
+ 0x00, 0x16, // VSI Option Length
+ 0x00, 0x00, 0x11, 0x8B, // Enterprise ID
+ 0x04, 0x01, // CMTS Capabilities Option
+ 0x00, 0x04, // Length
+ 0x01, 0x02,
+ 0x03, 0x00, // DOCSIS Version Number
+ 0x04, 0x02, // CM MAC Address Suboption
+ 0x00, 0x06, // Length
+ 0x74, 0x56, 0x12, 0x29, 0x97, 0xD0, // Actual MAC Address
+
};
TEST_F(LibDhcpTest, optionFactory) {
@@ -251,7 +304,7 @@ TEST_F(LibDhcpTest, optionFactory) {
TEST_F(LibDhcpTest, packOptions6) {
OptionBuffer buf(512);
- isc::dhcp::Option::OptionCollection opts; // list of options
+ isc::dhcp::OptionCollection opts; // list of options
// generate content for options
for (int i = 0; i < 64; i++) {
@@ -264,11 +317,23 @@ TEST_F(LibDhcpTest, packOptions6) {
OptionPtr opt4(new Option(Option::V6, 6, buf.begin() + 8, buf.begin() + 12));
OptionPtr opt5(new Option(Option::V6, 8, buf.begin() + 12, buf.begin() + 14));
- opts.insert(pair<int, OptionPtr >(opt1->getType(), opt1));
- opts.insert(pair<int, OptionPtr >(opt1->getType(), opt2));
- opts.insert(pair<int, OptionPtr >(opt1->getType(), opt3));
- opts.insert(pair<int, OptionPtr >(opt1->getType(), opt4));
- opts.insert(pair<int, OptionPtr >(opt1->getType(), opt5));
+ OptionPtr cm_mac(new Option(Option::V6, OPTION_CM_MAC,
+ OptionBuffer(v6packed + 54, v6packed + 60)));
+
+ OptionPtr cmts_caps(new Option(Option::V6, OPTION_CMTS_CAPS,
+ OptionBuffer(v6packed + 46, v6packed + 50)));
+
+ boost::shared_ptr<OptionInt<uint32_t> >
+ vsi(new OptionInt<uint32_t>(Option::V6, D6O_VENDOR_OPTS, 4491));
+ vsi->addOption(cm_mac);
+ vsi->addOption(cmts_caps);
+
+ opts.insert(make_pair(opt1->getType(), opt1));
+ opts.insert(make_pair(opt1->getType(), opt2));
+ opts.insert(make_pair(opt1->getType(), opt3));
+ opts.insert(make_pair(opt1->getType(), opt4));
+ opts.insert(make_pair(opt1->getType(), opt5));
+ opts.insert(make_pair(opt1->getType(), vsi));
OutputBuffer assembled(512);
@@ -283,19 +348,19 @@ TEST_F(LibDhcpTest, unpackOptions6) {
// Option is used as a simple option implementation
// More advanced uses are validated in tests dedicated for
// specific derived classes.
- isc::dhcp::Option::OptionCollection options; // list of options
+ isc::dhcp::OptionCollection options; // list of options
OptionBuffer buf(512);
memcpy(&buf[0], v6packed, sizeof(v6packed));
EXPECT_NO_THROW ({
LibDHCP::unpackOptions6(OptionBuffer(buf.begin(), buf.begin() + sizeof(v6packed)),
- options);
+ "dhcp6", options);
});
- EXPECT_EQ(options.size(), 5); // there should be 5 options
+ EXPECT_EQ(options.size(), 6); // there should be 5 options
- isc::dhcp::Option::OptionCollection::const_iterator x = options.find(1);
+ isc::dhcp::OptionCollection::const_iterator x = options.find(1);
ASSERT_FALSE(x == options.end()); // option 1 should exist
EXPECT_EQ(1, x->second->getType()); // this should be option 1
ASSERT_EQ(9, x->second->len()); // it should be of length 9
@@ -354,6 +419,27 @@ TEST_F(LibDhcpTest, unpackOptions6) {
// Returned value should be equivalent to two byte values: 112, 113
EXPECT_EQ(0x7071, opt_elapsed_time->getValue());
+ // Check if Vendor Specific Information Option along with suboptions
+ // have been parsed correctly.
+ x = options.find(D6O_VENDOR_OPTS);
+ EXPECT_FALSE(x == options.end());
+ EXPECT_EQ(D6O_VENDOR_OPTS, x->second->getType());
+ EXPECT_EQ(26, x->second->len());
+
+ // CM MAC Address Option
+ OptionPtr cm_mac = x->second->getOption(OPTION_CM_MAC);
+ ASSERT_TRUE(cm_mac);
+ EXPECT_EQ(OPTION_CM_MAC, cm_mac->getType());
+ ASSERT_EQ(10, cm_mac->len());
+ EXPECT_EQ(0, memcmp(&cm_mac->getData()[0], v6packed + 54, 6));
+
+ // CMTS Capabilities
+ OptionPtr cmts_caps = x->second->getOption(OPTION_CMTS_CAPS);
+ ASSERT_TRUE(cmts_caps);
+ EXPECT_EQ(OPTION_CMTS_CAPS, cmts_caps->getType());
+ ASSERT_EQ(8, cmts_caps->len());
+ EXPECT_EQ(0, memcmp(&cmts_caps->getData()[0], v6packed + 46, 4));
+
x = options.find(0);
EXPECT_TRUE(x == options.end()); // option 0 not found
@@ -372,12 +458,17 @@ TEST_F(LibDhcpTest, unpackOptions6) {
/// is no restriction on the data length being carried by them.
/// For simplicity, we assign data of the length 3 for each
/// of them.
-static uint8_t v4Opts[] = {
+static uint8_t v4_opts[] = {
12, 3, 0, 1, 2, // Hostname
60, 3, 10, 11, 12, // Class Id
14, 3, 20, 21, 22, // Merit Dump File
254, 3, 30, 31, 32, // Reserved
- 128, 3, 40, 41, 42 // Vendor specific
+ 128, 3, 40, 41, 42, // Vendor specific
+ 0x52, 0x19, // RAI
+ 0x01, 0x04, 0x20, 0x00, 0x00, 0x02, // Agent Circuit ID
+ 0x02, 0x06, 0x20, 0xE5, 0x2A, 0xB8, 0x15, 0x14, // Agent Remote ID
+ 0x09, 0x09, 0x00, 0x00, 0x11, 0x8B, 0x04, // Vendor Specific Information
+ 0x01, 0x02, 0x03, 0x00 // Vendor Specific Information continued
};
TEST_F(LibDhcpTest, packOptions4) {
@@ -396,66 +487,146 @@ TEST_F(LibDhcpTest, packOptions4) {
OptionPtr opt4(new Option(Option::V4,254, payload[3]));
OptionPtr opt5(new Option(Option::V4,128, payload[4]));
- isc::dhcp::Option::OptionCollection opts; // list of options
+ // Add RAI option, which comprises 3 sub-options.
+
+ // Get the option definition for RAI option. This option is represented
+ // by OptionCustom which requires a definition to be passed to
+ // the constructor.
+ OptionDefinitionPtr rai_def = LibDHCP::getOptionDef(Option::V4,
+ DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_TRUE(rai_def);
+ // Create RAI option.
+ OptionCustomPtr rai(new OptionCustom(*rai_def, Option::V4));
+
+ // The sub-options are created using the bits of v4_opts buffer because
+ // we want to use this buffer as a reference to verify that produced
+ // option in on-wire format is correct.
+
+ // Create Ciruit ID sub-option and add to RAI.
+ OptionPtr circuit_id(new Option(Option::V4, RAI_OPTION_AGENT_CIRCUIT_ID,
+ OptionBuffer(v4_opts + 29,
+ v4_opts + 33)));
+ rai->addOption(circuit_id);
+
+ // Create Remote ID option and add to RAI.
+ OptionPtr remote_id(new Option(Option::V4, RAI_OPTION_REMOTE_ID,
+ OptionBuffer(v4_opts + 35, v4_opts + 41)));
+ rai->addOption(remote_id);
+
+ // Create Vendor Specific Information and add to RAI.
+ OptionPtr vsi(new Option(Option::V4, RAI_OPTION_VSI,
+ OptionBuffer(v4_opts + 43, v4_opts + 52)));
+ rai->addOption(vsi);
+
+ isc::dhcp::OptionCollection opts; // list of options
+ // Note that we insert each option under the same option code into
+ // the map. This gurantees that options are packed in the same order
+ // they were added. Otherwise, options would get sorted by code and
+ // the resulting buffer wouldn't match with the reference buffer.
opts.insert(make_pair(opt1->getType(), opt1));
opts.insert(make_pair(opt1->getType(), opt2));
opts.insert(make_pair(opt1->getType(), opt3));
opts.insert(make_pair(opt1->getType(), opt4));
opts.insert(make_pair(opt1->getType(), opt5));
-
- vector<uint8_t> expVect(v4Opts, v4Opts + sizeof(v4Opts));
+ opts.insert(make_pair(opt1->getType(), rai));
OutputBuffer buf(100);
EXPECT_NO_THROW(LibDHCP::packOptions(buf, opts));
- ASSERT_EQ(buf.getLength(), sizeof(v4Opts));
- EXPECT_EQ(0, memcmp(v4Opts, buf.getData(), sizeof(v4Opts)));
-
+ ASSERT_EQ(buf.getLength(), sizeof(v4_opts));
+ EXPECT_EQ(0, memcmp(v4_opts, buf.getData(), sizeof(v4_opts)));
}
TEST_F(LibDhcpTest, unpackOptions4) {
- vector<uint8_t> v4packed(v4Opts, v4Opts + sizeof(v4Opts));
- isc::dhcp::Option::OptionCollection options; // list of options
+ vector<uint8_t> v4packed(v4_opts, v4_opts + sizeof(v4_opts));
+ isc::dhcp::OptionCollection options; // list of options
ASSERT_NO_THROW(
- LibDHCP::unpackOptions4(v4packed, options);
+ LibDHCP::unpackOptions4(v4packed, "dhcp4", options);
);
- isc::dhcp::Option::OptionCollection::const_iterator x = options.find(12);
+ isc::dhcp::OptionCollection::const_iterator x = options.find(12);
ASSERT_FALSE(x == options.end()); // option 1 should exist
- EXPECT_EQ(12, x->second->getType()); // this should be option 12
- ASSERT_EQ(3, x->second->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->second->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4Opts+2, 3)); // data len=3
+ // Option 12 holds a string so let's cast it to an appropriate type.
+ OptionStringPtr option12 = boost::static_pointer_cast<OptionString>(x->second);
+ ASSERT_TRUE(option12);
+ EXPECT_EQ(12, option12->getType()); // this should be option 12
+ ASSERT_EQ(3, option12->getValue().length()); // it should be of length 3
+ EXPECT_EQ(5, option12->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&option12->getValue()[0], v4_opts + 2, 3)); // data len=3
x = options.find(60);
ASSERT_FALSE(x == options.end()); // option 2 should exist
EXPECT_EQ(60, x->second->getType()); // this should be option 60
ASSERT_EQ(3, x->second->getData().size()); // it should be of length 3
EXPECT_EQ(5, x->second->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4Opts+7, 3)); // data len=3
+ EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4_opts + 7, 3)); // data len=3
x = options.find(14);
ASSERT_FALSE(x == options.end()); // option 3 should exist
- EXPECT_EQ(14, x->second->getType()); // this should be option 14
- ASSERT_EQ(3, x->second->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->second->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4Opts+12, 3)); // data len=3
+ OptionStringPtr option14 = boost::static_pointer_cast<OptionString>(x->second);
+ ASSERT_TRUE(option14);
+ EXPECT_EQ(14, option14->getType()); // this should be option 14
+ ASSERT_EQ(3, option14->getValue().length()); // it should be of length 3
+ EXPECT_EQ(5, option14->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&option14->getValue()[0], v4_opts + 12, 3)); // data len=3
x = options.find(254);
ASSERT_FALSE(x == options.end()); // option 3 should exist
EXPECT_EQ(254, x->second->getType()); // this should be option 254
ASSERT_EQ(3, x->second->getData().size()); // it should be of length 3
EXPECT_EQ(5, x->second->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4Opts+17, 3)); // data len=3
+ EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4_opts + 17, 3)); // data len=3
x = options.find(128);
ASSERT_FALSE(x == options.end()); // option 3 should exist
EXPECT_EQ(128, x->second->getType()); // this should be option 254
ASSERT_EQ(3, x->second->getData().size()); // it should be of length 3
EXPECT_EQ(5, x->second->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4Opts+22, 3)); // data len=3
-
+ EXPECT_EQ(0, memcmp(&x->second->getData()[0], v4_opts + 22, 3)); // data len=3
+
+ // Checking DHCP Relay Agent Information Option.
+ x = options.find(DHO_DHCP_AGENT_OPTIONS);
+ ASSERT_FALSE(x == options.end());
+ EXPECT_EQ(DHO_DHCP_AGENT_OPTIONS, x->second->getType());
+ // RAI is represented by OptionCustom.
+ OptionCustomPtr rai = boost::dynamic_pointer_cast<OptionCustom>(x->second);
+ ASSERT_TRUE(rai);
+ // RAI should have 3 sub-options: Circuit ID, Agent Remote ID, Vendor
+ // Specific Information option. Note that by parsing these suboptions we
+ // are checking that unpackOptions4 differentiates between standard option
+ // space called "dhcp4" and other option spaces. These sub-options do not
+ // belong to standard option space and should be parsed using different
+ // option definitions.
+ // @todo Currently, definitions for option space "dhcp-agent-options-space"
+ // are not defined. Therefore all suboptions will be represented here by
+ // the generic Option class.
+
+ // Check that Circuit ID option is among parsed options.
+ OptionPtr rai_option = rai->getOption(RAI_OPTION_AGENT_CIRCUIT_ID);
+ ASSERT_TRUE(rai_option);
+ EXPECT_EQ(RAI_OPTION_AGENT_CIRCUIT_ID, rai_option->getType());
+ ASSERT_EQ(6, rai_option->len());
+ EXPECT_EQ(0, memcmp(&rai_option->getData()[0], v4_opts + 29, 4));
+
+ // Check that Remote ID option is among parsed options.
+ rai_option = rai->getOption(RAI_OPTION_REMOTE_ID);
+ ASSERT_TRUE(rai_option);
+ EXPECT_EQ(RAI_OPTION_REMOTE_ID, rai_option->getType());
+ ASSERT_EQ(8, rai_option->len());
+ EXPECT_EQ(0, memcmp(&rai_option->getData()[0], v4_opts + 35, 6));
+
+ // Check that Vendor Specific Information option is among parsed options.
+ rai_option = rai->getOption(RAI_OPTION_VSI);
+ ASSERT_TRUE(rai_option);
+ EXPECT_EQ(RAI_OPTION_VSI, rai_option->getType());
+ ASSERT_EQ(11, rai_option->len());
+ EXPECT_EQ(0, memcmp(&rai_option->getData()[0], v4_opts + 43, 9));
+
+ // Make sure, that option other than those above is not present.
+ EXPECT_FALSE(rai->getOption(10));
+
+ // Check the same for the global option space.
x = options.find(0);
EXPECT_TRUE(x == options.end()); // option 0 not found
@@ -464,6 +635,7 @@ TEST_F(LibDhcpTest, unpackOptions4) {
x = options.find(2);
EXPECT_TRUE(x == options.end()); // option 2 not found
+
}
TEST_F(LibDhcpTest, isStandardOption4) {
@@ -474,7 +646,10 @@ TEST_F(LibDhcpTest, isStandardOption4) {
187, 188, 189, 190, 191, 192, 193, 194, 195,
196, 197, 198, 199, 200, 201, 202, 203, 204,
205, 206, 207, 214, 215, 216, 217, 218, 219,
- 222, 223 };
+ 222, 223, 224, 225, 226, 227, 228, 229, 230,
+ 231, 232, 233, 234, 235, 236, 237, 238, 239,
+ 240, 241, 242, 243, 244, 245, 246, 247, 248,
+ 249, 250, 251, 252, 253, 254 };
const size_t unassigned_num = sizeof(unassigned_codes) / sizeof(unassigned_codes[0]);
// Try all possible option codes.
@@ -532,13 +707,13 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
// It will be used to create most of the options.
std::vector<uint8_t> buf(48, 1);
OptionBufferConstIter begin = buf.begin();
- OptionBufferConstIter end = buf.begin();
+ OptionBufferConstIter end = buf.end();
LibDhcpTest::testStdOptionDefs4(DHO_SUBNET_MASK, begin, end,
typeid(OptionCustom));
LibDhcpTest::testStdOptionDefs4(DHO_TIME_OFFSET, begin, begin + 4,
- typeid(OptionInt<uint32_t>));
+ typeid(OptionInt<int32_t>));
LibDhcpTest::testStdOptionDefs4(DHO_ROUTERS, begin, end,
typeid(Option4AddrLst));
@@ -568,25 +743,25 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
typeid(Option4AddrLst));
LibDhcpTest::testStdOptionDefs4(DHO_HOST_NAME, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_BOOT_SIZE, begin, begin + 2,
typeid(OptionInt<uint16_t>));
LibDhcpTest::testStdOptionDefs4(DHO_MERIT_DUMP, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_DOMAIN_NAME, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_SWAP_SERVER, begin, end,
typeid(OptionCustom));
LibDhcpTest::testStdOptionDefs4(DHO_ROOT_PATH, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_EXTENSIONS_PATH, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_IP_FORWARDING, begin, end,
typeid(OptionCustom));
@@ -645,14 +820,14 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
LibDhcpTest::testStdOptionDefs4(DHO_DEFAULT_TCP_TTL, begin, begin + 1,
typeid(OptionInt<uint8_t>));
- LibDhcpTest::testStdOptionDefs4(DHO_TCP_KEEPALIVE_INTERVAL, begin, begin + 4,
- typeid(OptionInt<uint32_t>));
+ LibDhcpTest::testStdOptionDefs4(DHO_TCP_KEEPALIVE_INTERVAL, begin,
+ begin + 4, typeid(OptionInt<uint32_t>));
LibDhcpTest::testStdOptionDefs4(DHO_TCP_KEEPALIVE_GARBAGE, begin, begin + 1,
typeid(OptionCustom));
LibDhcpTest::testStdOptionDefs4(DHO_NIS_DOMAIN, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_NIS_SERVERS, begin, end,
typeid(Option4AddrLst));
@@ -660,8 +835,13 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
LibDhcpTest::testStdOptionDefs4(DHO_NTP_SERVERS, begin, end,
typeid(Option4AddrLst));
- LibDhcpTest::testStdOptionDefs4(DHO_VENDOR_ENCAPSULATED_OPTIONS, begin, end,
- typeid(Option),
+ // The following option requires well formed buffer to be created from.
+ // Not just a dummy one. This buffer includes some suboptions.
+ OptionBuffer vendor_opts_buf = createVendorOption();
+ LibDhcpTest::testStdOptionDefs4(DHO_VENDOR_ENCAPSULATED_OPTIONS,
+ vendor_opts_buf.begin(),
+ vendor_opts_buf.end(),
+ typeid(OptionCustom),
"vendor-encapsulated-options-space");
LibDhcpTest::testStdOptionDefs4(DHO_NETBIOS_NAME_SERVERS, begin, end,
@@ -674,7 +854,7 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
typeid(OptionInt<uint8_t>));
LibDhcpTest::testStdOptionDefs4(DHO_NETBIOS_SCOPE, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_FONT_SERVERS, begin, end,
typeid(Option4AddrLst));
@@ -698,10 +878,10 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
typeid(OptionCustom));
LibDhcpTest::testStdOptionDefs4(DHO_DHCP_PARAMETER_REQUEST_LIST, begin, end,
- typeid(Option));
+ typeid(OptionUint8Array));
LibDhcpTest::testStdOptionDefs4(DHO_DHCP_MESSAGE, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_DHCP_MAX_MESSAGE_SIZE, begin, begin + 2,
typeid(OptionInt<uint16_t>));
@@ -713,25 +893,37 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
typeid(OptionInt<uint32_t>));
LibDhcpTest::testStdOptionDefs4(DHO_VENDOR_CLASS_IDENTIFIER, begin, end,
- typeid(Option));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_DHCP_CLIENT_IDENTIFIER, begin, end,
typeid(Option));
LibDhcpTest::testStdOptionDefs4(DHO_NWIP_DOMAIN_NAME, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs4(DHO_NWIP_SUBOPTIONS, begin, end,
typeid(Option));
+ LibDhcpTest::testStdOptionDefs4(DHO_TFTP_SERVER_NAME, begin, end,
+ typeid(OptionString));
+
+ LibDhcpTest::testStdOptionDefs4(DHO_BOOT_FILE_NAME, begin, end,
+ typeid(OptionString));
+
LibDhcpTest::testStdOptionDefs4(DHO_USER_CLASS, begin, end,
typeid(Option));
- LibDhcpTest::testStdOptionDefs4(DHO_FQDN, begin, end,
- typeid(OptionCustom));
+ LibDhcpTest::testStdOptionDefs4(DHO_FQDN, begin, begin + 3,
+ typeid(Option4ClientFqdn));
- LibDhcpTest::testStdOptionDefs4(DHO_DHCP_AGENT_OPTIONS, begin, end,
- typeid(Option), "dhcp-agent-options-space");
+ // The following option requires well formed buffer to be created from.
+ // Not just a dummy one. This buffer includes some suboptions.
+ OptionBuffer agent_info_buf = createAgentInformationOption();
+ LibDhcpTest::testStdOptionDefs4(DHO_DHCP_AGENT_OPTIONS,
+ agent_info_buf.begin(),
+ agent_info_buf.end(),
+ typeid(OptionCustom),
+ "dhcp-agent-options-space");
LibDhcpTest::testStdOptionDefs4(DHO_AUTHENTICATE, begin, end,
typeid(Option));
@@ -753,7 +945,7 @@ TEST_F(LibDhcpTest, stdOptionDefs4) {
typeid(Option));
LibDhcpTest::testStdOptionDefs4(DHO_VIVSO_SUBOPTIONS, begin, end,
- typeid(Option));
+ typeid(OptionVendor));
}
// Test that definitions of standard options have been initialized
@@ -830,7 +1022,7 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
typeid(OptionCustom));
LibDhcpTest::testStdOptionDefs6(D6O_VENDOR_OPTS, begin, end,
- typeid(OptionInt<uint32_t>),
+ typeid(OptionVendor),
"vendor-opts-space");
LibDhcpTest::testStdOptionDefs6(D6O_INTERFACE_ID, begin, end,
@@ -858,8 +1050,8 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
LibDhcpTest::testStdOptionDefs6(D6O_IA_PD, begin, end,
typeid(Option6IA));
- LibDhcpTest::testStdOptionDefs6(D6O_IAPREFIX, begin, end,
- typeid(OptionCustom));
+ LibDhcpTest::testStdOptionDefs6(D6O_IAPREFIX, begin, begin + 25,
+ typeid(Option6IAPrefix));
LibDhcpTest::testStdOptionDefs6(D6O_NIS_SERVERS, begin, end,
typeid(Option6AddrLst));
@@ -899,7 +1091,8 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
typeid(Option));
LibDhcpTest::testStdOptionDefs6(D6O_CLIENT_FQDN, client_fqdn_buf.begin(),
- client_fqdn_buf.end(), typeid(OptionCustom));
+ client_fqdn_buf.end(),
+ typeid(Option6ClientFqdn));
LibDhcpTest::testStdOptionDefs6(D6O_PANA_AGENT, begin, end,
typeid(Option6AddrLst));
@@ -908,10 +1101,10 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
typeid(Option6AddrLst));
LibDhcpTest::testStdOptionDefs6(D6O_NEW_POSIX_TIMEZONE, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs6(D6O_NEW_TZDB_TIMEZONE, begin, end,
- typeid(OptionCustom));
+ typeid(OptionString));
LibDhcpTest::testStdOptionDefs6(D6O_ERO, begin, end,
typeid(OptionIntArray<uint16_t>));
@@ -932,4 +1125,45 @@ TEST_F(LibDhcpTest, stdOptionDefs6) {
typeid(Option6AddrLst));
}
+// tests whether v6 vendor-class option can be parsed properly.
+TEST_F(LibDhcpTest, vendorClass6) {
+
+ isc::dhcp::OptionCollection options; // Will store parsed option here
+
+ // Exported from wireshark: vendor-class option with enterprise-id = 4491
+ // and a single data entry containing "eRouter1.0"
+ string vendor_class_hex = "001000100000118b000a65526f75746572312e30";
+ OptionBuffer bin;
+
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(vendor_class_hex, bin);
+
+ ASSERT_NO_THROW ({
+ LibDHCP::unpackOptions6(bin, "dhcp6", options);
+ });
+
+ EXPECT_EQ(options.size(), 1); // There should be 1 option.
+
+ // Option vendor-class should be there
+ ASSERT_FALSE(options.find(D6O_VENDOR_CLASS) == options.end());
+
+ // It should be of type OptionCustom
+ boost::shared_ptr<OptionCustom> vclass =
+ boost::dynamic_pointer_cast<OptionCustom> (options.begin()->second);
+ ASSERT_TRUE(vclass);
+
+ // Let's investigate if the option content is correct
+
+ // 3 fields expected: vendor-id, data-len and data
+ ASSERT_EQ(3, vclass->getDataFieldsNum());
+
+ EXPECT_EQ(4491, vclass->readInteger<uint32_t>
+ (VENDOR_CLASS_ENTERPRISE_ID_INDEX)); // vendor-id=4491
+ EXPECT_EQ(10, vclass->readInteger<uint16_t>
+ (VENDOR_CLASS_DATA_LEN_INDEX)); // data len = 10
+ EXPECT_EQ("eRouter1.0", vclass->readString
+ (VENDOR_CLASS_STRING_INDEX)); // data="eRouter1.0"
}
+
+} // end of anonymous space
diff --git a/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc b/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc
new file mode 100644
index 0000000..cdd7c64
--- /dev/null
+++ b/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc
@@ -0,0 +1,959 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/option4_client_fqdn.h>
+#include <dns/name.h>
+#include <util/buffer.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::dhcp;
+
+// This test verifies that constructor accepts empty partial domain-name but
+// does not accept empty fully qualified domain name.
+TEST(Option4ClientFqdnTest, constructEmptyName) {
+ // Create an instance of the source option.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_TRUE(option->getDomainName().empty());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ // Constructor should not accept empty fully qualified domain name.
+ EXPECT_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "",
+ Option4ClientFqdn::FULL),
+ InvalidOption4FqdnDomainName);
+ // This check is similar to previous one, but using domain-name comprising
+ // a single space character. This should be treated as empty domain-name.
+ EXPECT_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ " ",
+ Option4ClientFqdn::FULL),
+ InvalidOption4FqdnDomainName);
+
+ // Try different constructor.
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER()))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getDomainName().empty());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that copy constructor makes a copy of the option and
+// the source option instance can be deleted (both instances don't share
+// any resources).
+TEST(Option4ClientFqdnTest, copyConstruct) {
+ // Create an instance of the source option.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+
+ // Use copy constructor to create a second instance of the option.
+ boost::scoped_ptr<Option4ClientFqdn> option_copy;
+ ASSERT_NO_THROW(
+ option_copy.reset(new Option4ClientFqdn(*option))
+ );
+ ASSERT_TRUE(option_copy);
+
+ // Copy construction should result in no shared resources between
+ // two objects. In particular, pointer to implementation should not
+ // be shared. Thus, we can release the source object now.
+ option.reset();
+
+ // Verify that all parameters have been copied to the target object.
+ EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example.com.", option_copy->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option_copy->getDomainNameType());
+
+ // Do another test with different parameters to verify that parameters
+ // change when copied object is changed.
+
+ // Create an option with different parameters.
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "example",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+
+ // Call copy-constructor to copy the option.
+ ASSERT_NO_THROW(
+ option_copy.reset(new Option4ClientFqdn(*option))
+ );
+ ASSERT_TRUE(option_copy);
+
+ option.reset();
+
+ EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("example", option_copy->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option_copy->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format with the domain-name
+// encoded in the canonical format is parsed correctly.
+TEST(Option4ClientFqdnTest, constructFromWire) {
+ // The E flag sets the domain-name format to canonical.
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format with the domain-name
+// encoded in the ASCII format is parsed correctly.
+TEST(Option4ClientFqdnTest, constructFromWireASCII) {
+ // The E flag is set to zero which indicates that the domain name
+ // is encoded in the ASCII format. The "dot" character at the end
+ // indicates that the domain-name is fully qualified.
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 109, 121, 104, 111, 115, 116, 46, // myhost.
+ 101, 120, 97, 109, 112, 108, 101, 46, // example.
+ 99, 111, 109, 46 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+}
+
+// This test verifies that truncated option is rejected.
+TEST(Option4ClientFqdnTest, constructFromWireTruncated) {
+ // Empty buffer is invalid. It should be at least one octet long.
+ OptionBuffer in_buf;
+ for (uint8_t i = 0; i < 3; ++i) {
+ EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()),
+ OutOfRange) << "Test of the truncated buffer failed for"
+ << " buffer length " << static_cast<int>(i);
+ in_buf.push_back(0);
+ }
+
+ // Buffer is now 3 bytes long, so it should not fail now.
+ EXPECT_NO_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()));
+}
+
+// This test verifies that exception is thrown when invalid domain-name
+// in canonical format is carried in the option.
+TEST(Option4ClientFqdnTest, constructFromWireInvalidName) {
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 5, 99, 111, 109, 0 // com. (invalid label length 5)
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption4FqdnDomainName);
+}
+
+// This test verifies that exception is thrown when invalid domain-name
+// in ASCII format is carried in the option.
+TEST(Option4ClientFqdnTest, constructFromWireInvalidASCIIName) {
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 109, 121, 104, 111, 115, 116, 46, 46, // myhost.. (double dot!)
+ 101, 120, 97, 109, 112, 108, 101, 46, // example.
+ 99, 111, 109, 46 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption4FqdnDomainName);
+}
+
+// This test verifies that the option in the on-wire format with partial
+// domain-name encoded in canonical format is parsed correctly.
+TEST(Option4ClientFqdnTest, constructFromWirePartial) {
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_N | Option4ClientFqdn:: FLAG_E, // flags
+ 255, // RCODE1
+ 255, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format with partial
+// domain-name encoded in ASCII format is parsed correctly.
+TEST(Option4ClientFqdnTest, constructFromWirePartialASCII) {
+ // There is no "dot" character at the end, so the domain-name is partial.
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_N, // flags
+ 255, // RCODE1
+ 255, // RCODE2
+ 109, 121, 104, 111, 115, 116, 46, // myhost.
+ 101, 120, 97, 109, 112, 108, 101 // example
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format with empty
+// domain-name is parsed correctly.
+TEST(Option4ClientFqdnTest, constructFromWireEmpty) {
+ // Initialize the 3-byte long buffer. All bytes initialized to 0:
+ // Flags, RCODE1 and RCODE2.
+ OptionBuffer in_buf(3, 0);
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ // domain-name field should be empty because on-wire data comprised
+ // flags field only.
+ EXPECT_TRUE(option->getDomainName().empty());
+}
+
+// This test verifies that assignment operator can be used to assign one
+// instance of the option to another.
+TEST(Option4ClientFqdnTest, assignment) {
+ // Usually the smart pointer is used to declare options and call
+ // constructor within assert. Thanks to this approach, the option instance
+ // is in the function scope and only initialization is done within assert.
+ // In this particular test we can't use smart pointers because we are
+ // testing assignment operator like this:
+ //
+ // option2 = option;
+ //
+ // The two asserts below do not create the instances that we will used to
+ // test assignment. They just attempt to create instances of the options
+ // with the same parameters as those that will be created for the actual
+ // assignment test. If these asserts do not fail, we can create options
+ // for the assignment test, do not surround them with asserts and be sure
+ // they will not throw.
+ ASSERT_NO_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL));
+
+ ASSERT_NO_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_N |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL));
+
+ // Create options with the same parameters as tested above.
+
+ // Create first option.
+ Option4ClientFqdn option(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL);
+
+ // Verify that the values have been set correctly.
+ ASSERT_TRUE(option.getFlag(Option4ClientFqdn::FLAG_S));
+ ASSERT_TRUE(option.getFlag(Option4ClientFqdn::FLAG_E));
+ ASSERT_FALSE(option.getFlag(Option4ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option.getFlag(Option4ClientFqdn::FLAG_N));
+ ASSERT_EQ("myhost.example.com.", option.getDomainName());
+ ASSERT_EQ(Option4ClientFqdn::FULL, option.getDomainNameType());
+
+ // Create a second option.
+ Option4ClientFqdn option2(Option4ClientFqdn::FLAG_N |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL);
+
+ // Verify tha the values have been set correctly.
+ ASSERT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_S));
+ ASSERT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_E));
+ ASSERT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_O));
+ ASSERT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_N));
+ ASSERT_EQ("myhost", option2.getDomainName());
+ ASSERT_EQ(Option4ClientFqdn::PARTIAL, option2.getDomainNameType());
+
+
+ // Make the assignment.
+ ASSERT_NO_THROW(option2 = option);
+
+ // Both options should now have the same values.
+ EXPECT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ(option.getDomainName(), option2.getDomainName());
+ EXPECT_EQ(option.getDomainNameType(), option2.getDomainNameType());
+
+ // Make self-assignment.
+ ASSERT_NO_THROW(option2 = option2);
+ EXPECT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_EQ(option.getDomainName(), option2.getDomainName());
+ EXPECT_EQ(option.getDomainNameType(), option2.getDomainNameType());
+}
+
+// This test verifies that constructor will throw an exception if invalid
+// DHCPv6 Client FQDN Option flags are specified.
+TEST(Option4ClientFqdnTest, constructInvalidFlags) {
+ // First, check that constructor does not throw an exception when
+ // valid flags values are provided. That way we eliminate the issue
+ // that constructor always throws exception.
+ uint8_t flags = 0;
+ ASSERT_NO_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"));
+
+ // Invalid flags: The maximal value is 0xF when all flag bits are set
+ // (00001111b). The flag value of 0x18 sets the bit from the Must Be
+ // Zero (MBZ) bitset (00011000b).
+ flags = 0x18;
+ EXPECT_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"),
+ InvalidOption4FqdnFlags);
+
+ // According to RFC 4702, section 2.1. if the N bit is set the S bit MUST
+ // be zero. If both are set, constructor is expected to throw.
+ flags = Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S;
+ EXPECT_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"),
+ InvalidOption4FqdnFlags);
+}
+
+// This test verifies that constructor which parses option from on-wire format
+// will throw exception if parsed flags field is invalid.
+TEST(Option4ClientFqdnTest, constructFromWireInvalidFlags) {
+ // Create a buffer which holds flags field only. Set valid flag field at
+ // at first to make sure that constructor doesn't always throw an exception.
+ OptionBuffer in_buf(3, 0);
+ in_buf[0] = Option4ClientFqdn::FLAG_S;
+ ASSERT_NO_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()));
+
+ // Replace the flags with invalid value and verify that constructor throws
+ // appropriate exception.
+ in_buf[0] = Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S;
+ EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption4FqdnFlags);
+}
+
+// This test verifies that if invalid domain name is used the constructor
+// will throw appropriate exception.
+TEST(Option4ClientFqdnTest, constructInvalidName) {
+ // First, check that constructor does not throw when valid domain name
+ // is specified. That way we eliminate the possibility that constructor
+ // always throws exception.
+ ASSERT_NO_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"));
+
+ // Specify invalid domain name and expect that exception is thrown.
+ EXPECT_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "my...host.example.com"),
+ InvalidOption4FqdnDomainName);
+
+ // Do the same test for the domain-name in ASCII format.
+ ASSERT_NO_THROW(Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"));
+
+ EXPECT_THROW(Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(),
+ "my...host.example.com"),
+ InvalidOption4FqdnDomainName);
+}
+
+// This test verifies that getFlag throws an exception if flag value other
+// than N, E, O, S was specified.
+TEST(Option4ClientFqdnTest, getFlag) {
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // The value of 0x3 (binary 0011) is invalid because it specifies two bits
+ // in the flags field which value is to be checked.
+ EXPECT_THROW(option->getFlag(0x3), InvalidOption4FqdnFlags);
+}
+
+// This test verifies that flags can be modified and that incorrect flags
+// are rejected.
+TEST(Option4ClientFqdnTest, setFlag) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(0,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // All flags should be set to 0 initially.
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_E));
+
+ // Set E = 1
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_E, true));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+
+ // Set N = 1
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, true));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_N));
+
+ // Set O = 1
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_O, true));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O));
+
+ // Set S = 1, this should throw exception because S and N must not
+ // be set in the same time.
+ ASSERT_THROW(option->setFlag(Option4ClientFqdn::FLAG_S, true),
+ InvalidOption4FqdnFlags);
+
+ // Set E = 0
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, false));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+
+ // Set N = 0
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, false));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+
+ // Set S = 1, this should not result in exception because N has been
+ // cleared.
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_S, true));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+
+ // Set N = 1, this should result in exception because S = 1
+ ASSERT_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, true),
+ InvalidOption4FqdnFlags);
+
+ // Set O = 0
+ ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_O, false));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+
+ // Try out of bounds settings.
+ uint8_t flags = 0;
+ ASSERT_THROW(option->setFlag(flags, true), InvalidOption4FqdnFlags);
+
+ flags = 0x18;
+ ASSERT_THROW(option->setFlag(flags, true), InvalidOption4FqdnFlags);
+}
+
+// This test verifies that flags field of the option is set to 0 when resetFlags
+// function is called.
+TEST(Option4ClientFqdnTest, resetFlags) {
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+
+ // Check that flags we set in the constructor are set.
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+
+ option->resetFlags();
+
+ // After reset, all flags should be 0.
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_E));
+}
+
+// This test verifies that current domain-name can be replaced with a new
+// domain-name.
+TEST(Option4ClientFqdnTest, setDomainName) {
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_SERVER(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+ ASSERT_EQ("myhost.example.com.", option->getDomainName());
+ ASSERT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+
+ // Partial domain-name.
+ ASSERT_NO_THROW(option->setDomainName("myhost",
+ Option4ClientFqdn::PARTIAL));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ // Fully qualified domain-name.
+ ASSERT_NO_THROW(option->setDomainName("example.com",
+ Option4ClientFqdn::FULL));
+ EXPECT_EQ("example.com.", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+
+ // Empty domain name (partial). This should be successful.
+ ASSERT_NO_THROW(option->setDomainName("", Option4ClientFqdn::PARTIAL));
+ EXPECT_TRUE(option->getDomainName().empty());
+
+ // Fully qualified domain-names must not be empty.
+ EXPECT_THROW(option->setDomainName("", Option4ClientFqdn::FULL),
+ InvalidOption4FqdnDomainName);
+ EXPECT_THROW(option->setDomainName(" ", Option4ClientFqdn::FULL),
+ InvalidOption4FqdnDomainName);
+}
+
+// This test verifies that current domain-name can be reset to empty one.
+TEST(Option4ClientFqdnTest, resetDomainName) {
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com",
+ Option4ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+ ASSERT_EQ("myhost.example.com.", option->getDomainName());
+ ASSERT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+
+ // Set the domain-name to empty one.
+ ASSERT_NO_THROW(option->resetDomainName());
+ EXPECT_TRUE(option->getDomainName().empty());
+}
+
+// This test verifies on-wire format of the option is correctly created.
+TEST(Option4ClientFqdnTest, pack) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E;
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 81, 23, // header
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+TEST(Option4ClientFqdnTest, packASCII) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option4ClientFqdn::FLAG_S;
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 81, 23, // header
+ Option4ClientFqdn::FLAG_S, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 109, 121, 104, 111, 115, 116, 46, // myhost.
+ 101, 120, 97, 109, 112, 108, 101, 46, // example.
+ 99, 111, 109, 46 // com.
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+
+}
+
+// This test verifies on-wire format of the option with partial domain name
+// is correctly created.
+TEST(Option4ClientFqdnTest, packPartial) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E;
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 81, 10, // header
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+// This test verifies that it is possible to encode option with empty
+// domain-name in the on-wire format.
+TEST(Option4ClientFqdnTest, packEmpty) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E;
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT()))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 81, 3, // header
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0 // RCODE2
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+// This test verifies that on-wire option data holding fully qualified domain
+// name is parsed correctly.
+TEST(Option4ClientFqdnTest, unpack) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ // Make sure that the parameters have been set correctly. Later in this
+ // test we will check that they will be replaced with new values when
+ // unpack is called.
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Initialize new values from the on-wire format.
+ ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end()));
+
+ // Check that new values are correct.
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+}
+
+// This test verifies that on-wire option data holding partial domain name
+// is parsed correctly.
+TEST(Option4ClientFqdnTest, unpackPartial) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+ // Make sure that the parameters have been set correctly. Later in this
+ // test we will check that they will be replaced with new values when
+ // unpack is called.
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType());
+
+ const uint8_t in_data[] = {
+ Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E, // flags
+ 0, // RCODE1
+ 0, // RCODE2
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Initialize new values from the on-wire format.
+ ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end()));
+
+ // Check that new values are correct.
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_E));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that the empty buffer is rejected when decoding an option
+// from on-wire format.
+TEST(Option4ClientFqdnTest, unpackTruncated) {
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT()))
+ );
+ ASSERT_TRUE(option);
+
+ // Empty buffer is invalid. It should be at least 1 octet long.
+ OptionBuffer in_buf;
+ EXPECT_THROW(option->unpack(in_buf.begin(), in_buf.end()), OutOfRange);
+}
+
+// This test verifies that string representation of the option returned by
+// toText method is correctly formatted.
+TEST(Option4ClientFqdnTest, toText) {
+ // Create option instance. Check that constructor doesn't throw.
+ uint8_t flags = Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_O |
+ Option4ClientFqdn::FLAG_E;
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // The base indentation of the option will be set to 2. It should appear
+ // as follows.
+ std::string ref_string =
+ " type=81 (CLIENT_FQDN), flags: (N=1, E=1, O=1, S=0), "
+ "domain-name='myhost.example.com.' (full)";
+ const int indent = 2;
+ EXPECT_EQ(ref_string, option->toText(indent));
+
+ // Create another option with different parameters:
+ // - flags set to 0
+ // - domain-name is now partial, not fully qualified
+ // Also, remove base indentation.
+ flags = 0;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(flags,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ref_string =
+ "type=81 (CLIENT_FQDN), flags: (N=0, E=0, O=0, S=0), "
+ "domain-name='myhost' (partial)";
+ EXPECT_EQ(ref_string, option->toText());
+}
+
+// This test verifies that the correct length of the option in on-wire
+// format is returned.
+TEST(Option4ClientFqdnTest, len) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option4ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+ // This option comprises a header (2 octets), flag field (1 octet),
+ // RCODE1 and RCODE2 (2 octets) and wire representation of the
+ // domain name (length equal to the length of the string representation
+ // of the domain name + 1).
+ EXPECT_EQ(25, option->len());
+
+ // Let's check that the size will change when domain name of a different
+ // size is used.
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "example.com"))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_EQ(18, option->len());
+
+ ASSERT_NO_THROW(
+ option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_E,
+ Option4ClientFqdn::RCODE_CLIENT(),
+ "myhost",
+ Option4ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_EQ(12, option->len());
+ }
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/option6_client_fqdn_unittest.cc b/src/lib/dhcp/tests/option6_client_fqdn_unittest.cc
new file mode 100644
index 0000000..d644a2e
--- /dev/null
+++ b/src/lib/dhcp/tests/option6_client_fqdn_unittest.cc
@@ -0,0 +1,809 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/option6_client_fqdn.h>
+#include <dns/name.h>
+#include <util/buffer.h>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::dhcp;
+
+// This test verifies that constructor accepts empty partial domain-name but
+// does not accept empty fully qualified domain name.
+TEST(Option6ClientFqdnTest, constructEmptyName) {
+ // Create an instance of the source option.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S, "",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getDomainName().empty());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ // Constructor should not accept empty fully qualified domain name.
+ EXPECT_THROW(Option6ClientFqdn(Option6ClientFqdn::FLAG_S, "",
+ Option6ClientFqdn::FULL),
+ InvalidOption6FqdnDomainName);
+ // This check is similar to previous one, but using domain-name comprising
+ // a single space character. This should be treated as empty domain-name.
+ EXPECT_THROW(Option6ClientFqdn(Option6ClientFqdn::FLAG_S, " ",
+ Option6ClientFqdn::FULL),
+ InvalidOption6FqdnDomainName);
+
+ // Try different constructor.
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_O))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option->getDomainName().empty());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that copy constructor makes a copy of the option and
+// the source option instance can be deleted (both instances don't share
+// any resources).
+TEST(Option6ClientFqdnTest, copyConstruct) {
+ // Create an instance of the source option.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+
+ // Use copy constructor to create a second instance of the option.
+ boost::scoped_ptr<Option6ClientFqdn> option_copy;
+ ASSERT_NO_THROW(
+ option_copy.reset(new Option6ClientFqdn(*option))
+ );
+ ASSERT_TRUE(option_copy);
+
+ // Copy construction should result in no shared resources between
+ // two objects. In particular, pointer to implementation should not
+ // be shared. Thus, we can release the source object now.
+ option.reset();
+
+ // Verify that all parameters have been copied to the target object.
+ EXPECT_TRUE(option_copy->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example.com.", option_copy->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, option_copy->getDomainNameType());
+
+ // Do another test with different parameters to verify that parameters
+ // change when copied object is changed.
+
+ // Create an option with different parameters.
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_O,
+ "example",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+
+ // Call copy-constructor to copy the option.
+ ASSERT_NO_THROW(
+ option_copy.reset(new Option6ClientFqdn(*option))
+ );
+ ASSERT_TRUE(option_copy);
+
+ option.reset();
+
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_TRUE(option_copy->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("example", option_copy->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option_copy->getDomainNameType());
+}
+
+// This test verifies that copy constructor makes a copy of the option, when
+// domain-name is empty.
+TEST(Option6ClientFqdnTest, copyConstructEmptyDomainName) {
+ // Create an instance of the source option.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S));
+ );
+ ASSERT_TRUE(option);
+
+ // Use copy constructor to create a second instance of the option.
+ boost::scoped_ptr<Option6ClientFqdn> option_copy;
+ ASSERT_NO_THROW(
+ option_copy.reset(new Option6ClientFqdn(*option))
+ );
+ ASSERT_TRUE(option_copy);
+
+ // Copy construction should result in no shared resources between
+ // two objects. In particular, pointer to implementation should not
+ // be shared. Thus, we can release the source object now.
+ option.reset();
+
+ // Verify that all parameters have been copied to the target object.
+ EXPECT_TRUE(option_copy->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option_copy->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("", option_copy->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option_copy->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format is parsed correctly.
+TEST(Option6ClientFqdnTest, constructFromWire) {
+ const uint8_t in_data[] = {
+ Option6ClientFqdn::FLAG_S, // flags
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+}
+
+// Verify that exception is thrown if the domain-name label is
+// longer than 63.
+TEST(Option6ClientFqdnTest, constructFromWireTooLongLabel) {
+ OptionBuffer in_buf(Option6ClientFqdn::FLAG_S);
+ in_buf.push_back(70);
+ in_buf.insert(in_buf.end(), 70, 109);
+ in_buf.push_back(0);
+
+ EXPECT_THROW(Option6ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption6FqdnDomainName);
+}
+
+// Verify that exception is thrown if the overall length of the domain-name
+// is over 255.
+TEST(Option6ClientFqdnTest, constructFromWireTooLongDomainName) {
+ OptionBuffer in_buf(Option6ClientFqdn::FLAG_S);
+ for (int i = 0; i < 26; ++i) {
+ in_buf.push_back(10);
+ in_buf.insert(in_buf.end(), 10, 109);
+ }
+ in_buf.push_back(0);
+
+ try {
+ Option6ClientFqdn(in_buf.begin(), in_buf.end());
+ } catch (const Exception& ex) {
+ std::cout << ex.what() << std::endl;
+ }
+ EXPECT_THROW(Option6ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption6FqdnDomainName);
+}
+
+// This test verifies that truncated option is rejected.
+TEST(Option6ClientFqdnTest, constructFromWireTruncated) {
+ // Empty buffer is invalid. It should be at least one octet long.
+ OptionBuffer in_buf;
+ ASSERT_THROW(Option6ClientFqdn(in_buf.begin(), in_buf.end()),
+ OutOfRange);
+}
+
+// This test verifies that the option in the on-wire format with partial
+// domain-name is parsed correctly.
+TEST(Option6ClientFqdnTest, constructFromWirePartial) {
+ const uint8_t in_data[] = {
+ Option6ClientFqdn::FLAG_N, // flags
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that the option in the on-wire format with empty
+// domain-name is parsed correctly.
+TEST(Option6ClientFqdnTest, constructFromWireEmpty) {
+ OptionBuffer in_buf(Option6ClientFqdn::FLAG_S);
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(in_buf.begin(), in_buf.end()))
+ );
+ ASSERT_TRUE(option);
+
+ // domain-name field should be empty because on-wire data comprised
+ // flags field only.
+ EXPECT_TRUE(option->getDomainName().empty());
+}
+
+// This test verifies that assignment operator can be used to assign one
+// instance of the option to another.
+TEST(Option6ClientFqdnTest, assignment) {
+ // Usually the smart pointer is used to declare options and call
+ // constructor within assert. Thanks to this approach, the option instance
+ // is in the function scope and only initialization is done within assert.
+ // In this particular test we can't use smart pointers because we are
+ // testing assignment operator like this:
+ //
+ // option2 = option;
+ //
+ // The two asserts below do not create the instances that we will used to
+ // test assignment. They just attempt to create instances of the options
+ // with the same parameters as those that will be created for the actual
+ // assignment test. If these asserts do not fail, we can create options
+ // for the assignment test, do not surround them with asserts and be sure
+ // they will not throw.
+ ASSERT_NO_THROW(Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL));
+
+ ASSERT_NO_THROW(Option6ClientFqdn(Option6ClientFqdn::FLAG_N,
+ "myhost",
+ Option6ClientFqdn::PARTIAL));
+
+ // Create options with the same parameters as tested above.
+
+ // Create first option.
+ Option6ClientFqdn option(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL);
+
+ // Verify that the values have been set correctly.
+ ASSERT_TRUE(option.getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option.getFlag(Option6ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option.getFlag(Option6ClientFqdn::FLAG_N));
+ ASSERT_EQ("myhost.example.com.", option.getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::FULL, option.getDomainNameType());
+
+ // Create a second option.
+ Option6ClientFqdn option2(Option6ClientFqdn::FLAG_N,
+ "myhost",
+ Option6ClientFqdn::PARTIAL);
+
+ // Verify tha the values have been set correctly.
+ ASSERT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_O));
+ ASSERT_TRUE(option2.getFlag(Option6ClientFqdn::FLAG_N));
+ ASSERT_EQ("myhost", option2.getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::PARTIAL, option2.getDomainNameType());
+
+
+ // Make the assignment.
+ ASSERT_NO_THROW(option2 = option);
+
+ // Both options should now have the same values.
+ EXPECT_TRUE(option2.getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ(option.getDomainName(), option2.getDomainName());
+ EXPECT_EQ(option.getDomainNameType(), option2.getDomainNameType());
+}
+
+// This test verifies that assignment operator can be used to assign one
+// instance of the option to another, when the domain-name is empty.
+TEST(Option6ClientFqdnTest, assignmentEmptyDomainName) {
+ ASSERT_NO_THROW(
+ Option6ClientFqdn(static_cast<uint8_t>(Option6ClientFqdn::FLAG_S))
+ );
+
+ ASSERT_NO_THROW(Option6ClientFqdn(Option6ClientFqdn::FLAG_N,
+ "myhost",
+ Option6ClientFqdn::PARTIAL));
+
+ // Create options with the same parameters as tested above.
+
+ // Create first option.
+ Option6ClientFqdn option(Option6ClientFqdn::FLAG_S);
+
+ // Verify that the values have been set correctly.
+ ASSERT_TRUE(option.getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option.getFlag(Option6ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option.getFlag(Option6ClientFqdn::FLAG_N));
+ ASSERT_EQ("", option.getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::PARTIAL, option.getDomainNameType());
+
+ // Create a second option.
+ Option6ClientFqdn option2(Option6ClientFqdn::FLAG_N,
+ "myhost",
+ Option6ClientFqdn::PARTIAL);
+
+ // Verify that the values have been set correctly.
+ ASSERT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_O));
+ ASSERT_TRUE(option2.getFlag(Option6ClientFqdn::FLAG_N));
+ ASSERT_EQ("myhost", option2.getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::PARTIAL, option2.getDomainNameType());
+
+
+ // Make the assignment.
+ ASSERT_NO_THROW(option2 = option);
+
+ // Both options should now have the same values.
+ EXPECT_TRUE(option2.getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option2.getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_EQ("", option2.getDomainName());
+ EXPECT_EQ(option.getDomainNameType(), option2.getDomainNameType());
+}
+
+
+// This test verifies that constructor will throw an exception if invalid
+// DHCPv6 Client FQDN Option flags are specified.
+TEST(Option6ClientFqdnTest, constructInvalidFlags) {
+ // First, check that constructor does not throw an exception when
+ // valid flags values are provided. That way we eliminate the issue
+ // that constructor always throws exception.
+ uint8_t flags = 0;
+ ASSERT_NO_THROW(Option6ClientFqdn(flags, "myhost.example.com"));
+
+ // Invalid flags: The maximal value is 0x7 when all flag bits are set
+ // (00000111b). The flag value of 0x14 sets the bit from the Must Be
+ // Zero (MBZ) bitset (00001100b).
+ flags = 0x14;
+ EXPECT_THROW(Option6ClientFqdn(flags, "myhost.example.com"),
+ InvalidOption6FqdnFlags);
+
+ // According to RFC 4704, section 4.1. if the N bit is set the S bit MUST
+ // be zero. If both are set, constructor is expected to throw.
+ flags = Option6ClientFqdn::FLAG_N | Option6ClientFqdn::FLAG_S;
+ EXPECT_THROW(Option6ClientFqdn(flags, "myhost.example.com"),
+ InvalidOption6FqdnFlags);
+}
+
+// This test verifies that constructor which parses option from on-wire format
+// will throw exception if parsed flags field is invalid.
+TEST(Option6ClientFqdnTest, constructFromWireInvalidFlags) {
+ // Create a buffer which holds flags field only. Set valid flag field at
+ // at first to make sure that constructor doesn't always throw an exception.
+ OptionBuffer in_buf(Option6ClientFqdn::FLAG_N);
+ ASSERT_NO_THROW(Option6ClientFqdn(in_buf.begin(), in_buf.end()));
+
+ // Replace the flags with invalid value and verify that constructor throws
+ // appropriate exception.
+ in_buf[0] = Option6ClientFqdn::FLAG_N | Option6ClientFqdn::FLAG_S;
+ EXPECT_THROW(Option6ClientFqdn(in_buf.begin(), in_buf.end()),
+ InvalidOption6FqdnFlags);
+}
+
+// This test verifies that if invalid domain name is used the constructor
+// will throw appropriate exception.
+TEST(Option6ClientFqdnTest, constructInvalidName) {
+ // First, check that constructor does not throw when valid domain name
+ // is specified. That way we eliminate the possibility that constructor
+ // always throws exception.
+ ASSERT_NO_THROW(Option6ClientFqdn(0, "myhost.example.com"));
+
+ // Specify invalid domain name and expect that exception is thrown.
+ EXPECT_THROW(Option6ClientFqdn(0, "my...host.example.com"),
+ InvalidOption6FqdnDomainName);
+}
+
+// This test verifies that getFlag throws an exception if flag value other
+// than FLAG_N, FLAG_S, FLAG_O is specified.
+TEST(Option6ClientFqdnTest, getFlag) {
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(0, "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // The 0x3 (binary 011) specifies two distinct bits in the flags field.
+ // This value is ambiguous for getFlag function and this function doesn't
+ // know which flag the caller is attempting to check.
+ EXPECT_THROW(option->getFlag(0x3), InvalidOption6FqdnFlags);
+}
+
+// This test verifies that flags can be modified and that incorrect flags
+// are rejected.
+TEST(Option6ClientFqdnTest, setFlag) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(0, "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // All flags should be set to 0 initially.
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+
+ // Set N = 1
+ ASSERT_NO_THROW(option->setFlag(Option6ClientFqdn::FLAG_N, true));
+ ASSERT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_N));
+
+ // Set O = 1
+ ASSERT_NO_THROW(option->setFlag(Option6ClientFqdn::FLAG_O, true));
+ ASSERT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_O));
+
+ // Set S = 1, this should throw exception because S and N must not
+ // be set in the same time.
+ ASSERT_THROW(option->setFlag(Option6ClientFqdn::FLAG_S, true),
+ InvalidOption6FqdnFlags);
+
+ // Set N = 0
+ ASSERT_NO_THROW(option->setFlag(Option6ClientFqdn::FLAG_N, false));
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+
+ // Set S = 1, this should not result in exception because N has been
+ // cleared.
+ ASSERT_NO_THROW(option->setFlag(Option6ClientFqdn::FLAG_S, true));
+ ASSERT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+
+ // Set N = 1, this should result in exception because S = 1
+ ASSERT_THROW(option->setFlag(Option6ClientFqdn::FLAG_N, true),
+ InvalidOption6FqdnFlags);
+
+ // Set O = 0
+ ASSERT_NO_THROW(option->setFlag(Option6ClientFqdn::FLAG_O, false));
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+
+ // Try out of bounds settings.
+ uint8_t flags = 0;
+ ASSERT_THROW(option->setFlag(flags, true), InvalidOption6FqdnFlags);
+
+ flags = 0x14;
+ ASSERT_THROW(option->setFlag(flags, true), InvalidOption6FqdnFlags);
+}
+
+// This test verifies that flags field of the option is set to 0 when resetFlags
+// function is called.
+TEST(Option6ClientFqdnTest, resetFlags) {
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S |
+ Option6ClientFqdn::FLAG_O,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+
+ // Check that flags we set in the constructor are set.
+ ASSERT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ ASSERT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ ASSERT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+
+ option->resetFlags();
+
+ // After reset, all flags should be 0.
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+}
+
+// This test verifies that current domain-name can be replaced with a new
+// domain-name.
+TEST(Option6ClientFqdnTest, setDomainName) {
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+ ASSERT_EQ("myhost.example.com.", option->getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+
+ // Partial domain-name.
+ ASSERT_NO_THROW(option->setDomainName("myhost",
+ Option6ClientFqdn::PARTIAL));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ // Fully qualified domain-name.
+ ASSERT_NO_THROW(option->setDomainName("example.com",
+ Option6ClientFqdn::FULL));
+ EXPECT_EQ("example.com.", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+
+ // Empty domain name (partial). This should be successful.
+ ASSERT_NO_THROW(option->setDomainName("", Option6ClientFqdn::PARTIAL));
+ EXPECT_TRUE(option->getDomainName().empty());
+
+ // Fully qualified domain-names must not be empty.
+ EXPECT_THROW(option->setDomainName("", Option6ClientFqdn::FULL),
+ InvalidOption6FqdnDomainName);
+ EXPECT_THROW(option->setDomainName(" ", Option6ClientFqdn::FULL),
+ InvalidOption6FqdnDomainName);
+}
+
+// This test verifies that current domain-name can be reset to empty one.
+TEST(Option6ClientFqdnTest, resetDomainName) {
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_S,
+ "myhost.example.com",
+ Option6ClientFqdn::FULL))
+ );
+ ASSERT_TRUE(option);
+ ASSERT_EQ("myhost.example.com.", option->getDomainName());
+ ASSERT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+
+ // Set the domain-name to empty one.
+ ASSERT_NO_THROW(option->resetDomainName());
+ EXPECT_TRUE(option->getDomainName().empty());
+}
+
+// This test verifies on-wire format of the option is correctly created.
+TEST(Option6ClientFqdnTest, pack) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option6ClientFqdn::FLAG_S;
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(flags, "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 0, 39, 0, 21, // header
+ Option6ClientFqdn::FLAG_S, // flags
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+// This test verifies on-wire format of the option with partial domain name
+// is correctly created.
+TEST(Option6ClientFqdnTest, packPartial) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option6ClientFqdn::FLAG_S;
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(flags, "myhost",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(10);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 0, 39, 0, 8, // header
+ Option6ClientFqdn::FLAG_S, // flags
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+// This test verifies that it is possible to encode the option which carries
+// empty domain-name in the wire format.
+TEST(Option6ClientFqdnTest, packEmpty) {
+ // Create option instance. Check that constructor doesn't throw.
+ const uint8_t flags = Option6ClientFqdn::FLAG_S;
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(flags))
+ );
+ ASSERT_TRUE(option);
+
+ // Prepare on-wire format of the option.
+ isc::util::OutputBuffer buf(5);
+ ASSERT_NO_THROW(option->pack(buf));
+
+ // Prepare reference data.
+ const uint8_t ref_data[] = {
+ 0, 39, 0, 1, // header
+ Option6ClientFqdn::FLAG_S // flags
+ };
+ size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]);
+
+ // Check if the buffer has the same length as the reference data,
+ // so as they can be compared directly.
+ ASSERT_EQ(ref_data_size, buf.getLength());
+ EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength()));
+}
+
+// This test verifies that on-wire option data holding fully qualified domain
+// name is parsed correctly.
+TEST(Option6ClientFqdnTest, unpack) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_O,
+ "myhost",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ // Make sure that the parameters have been set correctly. Later in this
+ // test we will check that they will be replaced with new values when
+ // unpack is called.
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+
+ const uint8_t in_data[] = {
+ Option6ClientFqdn::FLAG_S, // flags
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Initialize new values from the on-wire format.
+ ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end()));
+
+ // Check that new values are correct.
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+}
+
+// This test verifies that on-wire option data holding partial domain name
+// is parsed correctly.
+TEST(Option6ClientFqdnTest, unpackPartial) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_O,
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+ // Make sure that the parameters have been set correctly. Later in this
+ // test we will check that they will be replaced with new values when
+ // unpack is called.
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_EQ("myhost.example.com.", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::FULL, option->getDomainNameType());
+
+ const uint8_t in_data[] = {
+ Option6ClientFqdn::FLAG_S, // flags
+ 6, 109, 121, 104, 111, 115, 116 // myhost
+ };
+ size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]);
+ OptionBuffer in_buf(in_data, in_data + in_data_size);
+
+ // Initialize new values from the on-wire format.
+ ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end()));
+
+ // Check that new values are correct.
+ EXPECT_TRUE(option->getFlag(Option6ClientFqdn::FLAG_S));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_N));
+ EXPECT_FALSE(option->getFlag(Option6ClientFqdn::FLAG_O));
+ EXPECT_EQ("myhost", option->getDomainName());
+ EXPECT_EQ(Option6ClientFqdn::PARTIAL, option->getDomainNameType());
+}
+
+// This test verifies that the empty buffer is rejected when decoding an option
+// from on-wire format.
+TEST(Option6ClientFqdnTest, unpackTruncated) {
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(Option6ClientFqdn::FLAG_O))
+ );
+ ASSERT_TRUE(option);
+
+ // Empty buffer is invalid. It should be at least 1 octet long.
+ OptionBuffer in_buf;
+ EXPECT_THROW(option->unpack(in_buf.begin(), in_buf.end()), OutOfRange);
+}
+
+// This test verifies that string representation of the option returned by
+// toText method is correctly formatted.
+TEST(Option6ClientFqdnTest, toText) {
+ // Create option instance. Check that constructor doesn't throw.
+ uint8_t flags = Option6ClientFqdn::FLAG_N | Option6ClientFqdn::FLAG_O;
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(flags,
+ "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+
+ // The base indentation of the option will be set to 2. It should appear
+ // as follows.
+ std::string ref_string =
+ " type=39(CLIENT_FQDN), flags: (N=1, O=1, S=0), "
+ "domain-name='myhost.example.com.' (full)";
+ const int indent = 2;
+ EXPECT_EQ(ref_string, option->toText(indent));
+
+ // Create another option with different parameters:
+ // - flags set to 0
+ // - domain-name is now partial, not fully qualified
+ // Also, remove base indentation.
+ flags = 0;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(flags, "myhost",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ref_string =
+ "type=39(CLIENT_FQDN), flags: (N=0, O=0, S=0), "
+ "domain-name='myhost' (partial)";
+ EXPECT_EQ(ref_string, option->toText());
+}
+
+// This test verifies that the correct length of the option in on-wire
+// format is returned.
+TEST(Option6ClientFqdnTest, len) {
+ // Create option instance. Check that constructor doesn't throw.
+ boost::scoped_ptr<Option6ClientFqdn> option;
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(0, "myhost.example.com"))
+ );
+ ASSERT_TRUE(option);
+ // This option comprises a header (4 octets), flag field (1 octet),
+ // and wire representation of the domain name (length equal to the
+ // length of the string representation of the domain name + 1).
+ EXPECT_EQ(25, option->len());
+
+ // Let's check that the size will change when domain name of a different
+ // size is used.
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(0, "example.com"))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_EQ(18, option->len());
+
+ ASSERT_NO_THROW(
+ option.reset(new Option6ClientFqdn(0, "myhost",
+ Option6ClientFqdn::PARTIAL))
+ );
+ ASSERT_TRUE(option);
+ EXPECT_EQ(12, option->len());
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/option6_ia_unittest.cc b/src/lib/dhcp/tests/option6_ia_unittest.cc
index 071edb9..2a91505 100644
--- a/src/lib/dhcp/tests/option6_ia_unittest.cc
+++ b/src/lib/dhcp/tests/option6_ia_unittest.cc
@@ -18,6 +18,7 @@
#include <dhcp/option.h>
#include <dhcp/option6_ia.h>
#include <dhcp/option6_iaaddr.h>
+#include <dhcp/option6_iaprefix.h>
#include <util/buffer.h>
#include <boost/scoped_ptr.hpp>
@@ -43,71 +44,96 @@ public:
buf_[i] = 255 - i;
}
}
- OptionBuffer buf_;
- OutputBuffer outBuf_;
-};
-TEST_F(Option6IATest, basic) {
- buf_[0] = 0xa1; // iaid
- buf_[1] = 0xa2;
- buf_[2] = 0xa3;
- buf_[3] = 0xa4;
+ /// @brief performs basic checks on IA option
+ ///
+ /// Check that an option can be built based on incoming buffer and that
+ /// the option contains expected values.
+ /// @param type specifies option type (IA_NA or IA_PD)
+ void checkIA(uint16_t type) {
+ buf_[0] = 0xa1; // iaid
+ buf_[1] = 0xa2;
+ buf_[2] = 0xa3;
+ buf_[3] = 0xa4;
+
+ buf_[4] = 0x81; // T1
+ buf_[5] = 0x02;
+ buf_[6] = 0x03;
+ buf_[7] = 0x04;
+
+ buf_[8] = 0x84; // T2
+ buf_[9] = 0x03;
+ buf_[10] = 0x02;
+ buf_[11] = 0x01;
+
+ // Create an option
+ // unpack() is called from constructor
+ scoped_ptr<Option6IA> opt;
+ ASSERT_NO_THROW(opt.reset(new Option6IA(type, buf_.begin(),
+ buf_.begin() + 12)));
- buf_[4] = 0x81; // T1
- buf_[5] = 0x02;
- buf_[6] = 0x03;
- buf_[7] = 0x04;
+ EXPECT_EQ(Option::V6, opt->getUniverse());
+ EXPECT_EQ(type, opt->getType());
+ EXPECT_EQ(0xa1a2a3a4, opt->getIAID());
+ EXPECT_EQ(0x81020304, opt->getT1());
+ EXPECT_EQ(0x84030201, opt->getT2());
- buf_[8] = 0x84; // T2
- buf_[9] = 0x03;
- buf_[10] = 0x02;
- buf_[11] = 0x01;
+ // Pack this option again in the same buffer, but in
+ // different place
- // Create an option
- // unpack() is called from constructor
- scoped_ptr<Option6IA> opt(new Option6IA(D6O_IA_NA,
- buf_.begin(),
- buf_.begin() + 12));
+ // Test for pack()
+ ASSERT_NO_THROW(opt->pack(outBuf_));
- EXPECT_EQ(Option::V6, opt->getUniverse());
- EXPECT_EQ(D6O_IA_NA, opt->getType());
- EXPECT_EQ(0xa1a2a3a4, opt->getIAID());
- EXPECT_EQ(0x81020304, opt->getT1());
- EXPECT_EQ(0x84030201, opt->getT2());
+ // 12 bytes header + 4 bytes content
+ EXPECT_EQ(12, opt->len() - opt->getHeaderLen());
+ EXPECT_EQ(type, opt->getType());
- // Pack this option again in the same buffer, but in
- // different place
+ EXPECT_EQ(16, outBuf_.getLength()); // lenght(IA_NA) = 16
- // Test for pack()
- opt->pack(outBuf_);
+ // Check if pack worked properly:
+ InputBuffer out(outBuf_.getData(), outBuf_.getLength());
- // 12 bytes header + 4 bytes content
- EXPECT_EQ(12, opt->len() - opt->getHeaderLen());
- EXPECT_EQ(D6O_IA_NA, opt->getType());
+ // - if option type is correct
+ EXPECT_EQ(type, out.readUint16());
- EXPECT_EQ(16, outBuf_.getLength()); // lenght(IA_NA) = 16
+ // - if option length is correct
+ EXPECT_EQ(12, out.readUint16());
- // Check if pack worked properly:
- InputBuffer out(outBuf_.getData(), outBuf_.getLength());
+ // - if iaid is correct
+ EXPECT_EQ(0xa1a2a3a4, out.readUint32() );
- // - if option type is correct
- EXPECT_EQ(D6O_IA_NA, out.readUint16());
+ // - if T1 is correct
+ EXPECT_EQ(0x81020304, out.readUint32() );
- // - if option length is correct
- EXPECT_EQ(12, out.readUint16());
+ // - if T1 is correct
+ EXPECT_EQ(0x84030201, out.readUint32() );
- // - if iaid is correct
- EXPECT_EQ(0xa1a2a3a4, out.readUint32() );
+ EXPECT_NO_THROW(opt.reset());
+ }
- // - if T1 is correct
- EXPECT_EQ(0x81020304, out.readUint32() );
+ OptionBuffer buf_;
+ OutputBuffer outBuf_;
+};
- // - if T1 is correct
- EXPECT_EQ(0x84030201, out.readUint32() );
+TEST_F(Option6IATest, basic) {
+ checkIA(D6O_IA_NA);
+}
- EXPECT_NO_THROW(opt.reset());
+TEST_F(Option6IATest, pdBasic) {
+ checkIA(D6O_IA_PD);
}
+// Check that this class cannot be used for IA_TA (IA_TA has no T1, T2 fields
+// and people tend to think that if it's good for IA_NA and IA_PD, it can
+// be used for IA_TA as well and that is not true)
+TEST_F(Option6IATest, taForbidden) {
+ EXPECT_THROW(Option6IA(D6O_IA_TA, buf_.begin(), buf_.begin() + 50),
+ BadValue);
+
+ EXPECT_THROW(Option6IA(D6O_IA_TA, 123), BadValue);
+}
+
+// Check that getters/setters are working as expected.
TEST_F(Option6IATest, simple) {
scoped_ptr<Option6IA> ia(new Option6IA(D6O_IA_NA, 1234));
@@ -131,12 +157,8 @@ TEST_F(Option6IATest, simple) {
EXPECT_NO_THROW(ia.reset());
}
-
-// test if option can build suboptions
-TEST_F(Option6IATest, suboptions_pack) {
- buf_[0] = 0xff;
- buf_[1] = 0xfe;
- buf_[2] = 0xfc;
+// test if the option can build suboptions
+TEST_F(Option6IATest, suboptionsPack) {
scoped_ptr<Option6IA> ia(new Option6IA(D6O_IA_NA, 0x13579ace));
ia->setT1(0x2345);
@@ -154,6 +176,7 @@ TEST_F(Option6IATest, suboptions_pack) {
ASSERT_EQ(4, sub1->len());
ASSERT_EQ(48, ia->len());
+ // This contains expected on-wire format
uint8_t expected[] = {
D6O_IA_NA/256, D6O_IA_NA%256, // type
0, 44, // length
@@ -175,18 +198,69 @@ TEST_F(Option6IATest, suboptions_pack) {
};
ia->pack(outBuf_);
- ASSERT_EQ(48, outBuf_.getLength());
+ ASSERT_EQ(48, outBuf_.getLength());
EXPECT_EQ(0, memcmp(outBuf_.getData(), expected, 48));
-
EXPECT_NO_THROW(ia.reset());
}
+// test if IA_PD option can build IAPREFIX suboptions
+TEST_F(Option6IATest, pdSuboptionsPack) {
+
+ // Let's build IA_PD
+ scoped_ptr<Option6IA> ia;
+ ASSERT_NO_THROW(ia.reset(new Option6IA(D6O_IA_PD, 0x13579ace)));
+ ia->setT1(0x2345);
+ ia->setT2(0x3456);
+
+ // Put some dummy option in it
+ OptionPtr sub1(new Option(Option::V6, 0xcafe));
+
+ // Put a valid IAPREFIX option in it
+ boost::shared_ptr<Option6IAPrefix> addr1(
+ new Option6IAPrefix(D6O_IAPREFIX, IOAddress("2001:db8:1234:5678::abcd"),
+ 91, 0x5000, 0x7000));
+
+ ia->addOption(sub1);
+ ia->addOption(addr1);
+
+ ASSERT_EQ(29, addr1->len());
+ ASSERT_EQ(4, sub1->len());
+ ASSERT_EQ(49, ia->len());
+
+ uint8_t expected[] = {
+ D6O_IA_PD/256, D6O_IA_PD%256, // type
+ 0, 45, // length
+ 0x13, 0x57, 0x9a, 0xce, // iaid
+ 0, 0, 0x23, 0x45, // T1
+ 0, 0, 0x34, 0x56, // T2
+
+ // iaprefix suboption
+ D6O_IAPREFIX/256, D6O_IAPREFIX%256, // type
+ 0, 25, // len
+ 0, 0, 0x50, 0, // preferred-lifetime
+ 0, 0, 0x70, 0, // valid-lifetime
+ 91, // prefix length
+ 0x20, 0x01, 0xd, 0xb8, 0x12,0x34, 0x56, 0x78,
+ 0, 0, 0, 0, 0, 0, 0xab, 0xcd, // IP address
+
+ // suboption
+ 0xca, 0xfe, // type
+ 0, 0 // len
+ };
+
+ ia->pack(outBuf_);
+ ASSERT_EQ(49, outBuf_.getLength());
+
+ EXPECT_EQ(0, memcmp(outBuf_.getData(), expected, 49));
+
+ EXPECT_NO_THROW(ia.reset());
+}
// test if option can parse suboptions
TEST_F(Option6IATest, suboptions_unpack) {
// sizeof (expected) = 48 bytes
- uint8_t expected[] = {
+ const uint8_t expected[] = {
D6O_IA_NA / 256, D6O_IA_NA % 256, // type
0, 28, // length
0x13, 0x57, 0x9a, 0xce, // iaid
diff --git a/src/lib/dhcp/tests/option6_iaaddr_unittest.cc b/src/lib/dhcp/tests/option6_iaaddr_unittest.cc
index e28e2e0..d2e6a15 100644
--- a/src/lib/dhcp/tests/option6_iaaddr_unittest.cc
+++ b/src/lib/dhcp/tests/option6_iaaddr_unittest.cc
@@ -106,4 +106,19 @@ TEST_F(Option6IAAddrTest, basic) {
EXPECT_NO_THROW(opt.reset());
}
+/// @todo: Write test for (type, addr, pref, valid) constructor
+/// See option6_iaprefix_unittest.cc for similar test
+
+// Tests if broken usage causes exception to be thrown
+TEST_F(Option6IAAddrTest, negative) {
+
+ // Too short. Minimum length is 24
+ EXPECT_THROW(Option6IAAddr(D6O_IAADDR, buf_.begin(), buf_.begin() + 23),
+ OutOfRange);
+
+ // This option is for IPv6 addresses only
+ EXPECT_THROW(Option6IAAddr(D6O_IAADDR, isc::asiolink::IOAddress("192.0.2.1"),
+ 1000, 2000), BadValue);
+}
+
}
diff --git a/src/lib/dhcp/tests/option6_iaprefix_unittest.cc b/src/lib/dhcp/tests/option6_iaprefix_unittest.cc
new file mode 100644
index 0000000..e5dd05e
--- /dev/null
+++ b/src/lib/dhcp/tests/option6_iaprefix_unittest.cc
@@ -0,0 +1,188 @@
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <dhcp/dhcp6.h>
+#include <dhcp/option.h>
+#include <dhcp/option6_iaprefix.h>
+#include <util/buffer.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+#include <arpa/inet.h>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc::asiolink;
+
+namespace {
+class Option6IAPrefixTest : public ::testing::Test {
+public:
+ Option6IAPrefixTest() : buf_(255), outBuf_(255) {
+ for (int i = 0; i < 255; i++) {
+ buf_[i] = 255 - i;
+ }
+ }
+
+ /// @brief creates on-wire representation of IAPREFIX option
+ ///
+ /// buf_ field is set up to have IAPREFIX with preferred=1000,
+ /// valid=3000000000 and prefix beign 2001:db8:1::dead:beef/77
+ void setExampleBuffer() {
+ for (int i = 0; i < 255; i++) {
+ buf_[i] = 0;
+ }
+
+ buf_[ 0] = 0x00;
+ buf_[ 1] = 0x00;
+ buf_[ 2] = 0x03;
+ buf_[ 3] = 0xe8; // preferred lifetime = 1000
+
+ buf_[ 4] = 0xb2;
+ buf_[ 5] = 0xd0;
+ buf_[ 6] = 0x5e;
+ buf_[ 7] = 0x00; // valid lifetime = 3,000,000,000
+
+ buf_[ 8] = 77; // Prefix length = 77
+
+ buf_[ 9] = 0x20;
+ buf_[10] = 0x01;
+ buf_[11] = 0x0d;
+ buf_[12] = 0xb8;
+ buf_[13] = 0x00;
+ buf_[14] = 0x01;
+ buf_[21] = 0xde;
+ buf_[22] = 0xad;
+ buf_[23] = 0xbe;
+ buf_[24] = 0xef; // 2001:db8:1::dead:beef
+ }
+
+
+ /// @brief Checks whether specified IAPREFIX option meets expected values
+ ///
+ /// To be used with option generated by setExampleBuffer
+ ///
+ /// @param opt IAPREFIX option being tested
+ /// @param expected_type expected option type
+ void checkOption(Option6IAPrefix& opt, uint16_t expected_type) {
+
+ // Check if all fields have expected values
+ EXPECT_EQ(Option::V6, opt.getUniverse());
+ EXPECT_EQ(expected_type, opt.getType());
+ EXPECT_EQ("2001:db8:1::dead:beef", opt.getAddress().toText());
+ EXPECT_EQ(1000, opt.getPreferred());
+ EXPECT_EQ(3000000000U, opt.getValid());
+ EXPECT_EQ(77, opt.getLength());
+
+ // 4 bytes header + 25 bytes content
+ EXPECT_EQ(Option::OPTION6_HDR_LEN + Option6IAPrefix::OPTION6_IAPREFIX_LEN,
+ opt.len());
+ }
+
+ /// @brief Checks whether content of output buffer is correct
+ ///
+ /// Output buffer is expected to be filled with an option matchin
+ /// buf_ content as defined in setExampleBuffer().
+ ///
+ /// @param expected_type expected option type
+ void checkOutputBuffer(uint16_t expected_type) {
+ // Check if pack worked properly:
+ const uint8_t* out = (const uint8_t*)outBuf_.getData();
+
+ // - if option type is correct
+ EXPECT_EQ(expected_type, out[0]*256 + out[1]);
+
+ // - if option length is correct
+ EXPECT_EQ(25, out[2]*256 + out[3]);
+
+ // - if option content is correct
+ EXPECT_EQ(0, memcmp(out + 4, &buf_[0], 25));
+ }
+
+ OptionBuffer buf_;
+ OutputBuffer outBuf_;
+};
+
+// Tests if receiving option can be parsed correctly
+TEST_F(Option6IAPrefixTest, basic) {
+
+ setExampleBuffer();
+
+ // Create an option (unpack content)
+ boost::scoped_ptr<Option6IAPrefix> opt;
+ ASSERT_NO_THROW(opt.reset(new Option6IAPrefix(D6O_IAPREFIX, buf_.begin(),
+ buf_.begin() + 25)));
+ ASSERT_TRUE(opt);
+
+ // Pack this option
+ opt->pack(outBuf_);
+ EXPECT_EQ(29, outBuf_.getLength());
+
+ checkOption(*opt, D6O_IAPREFIX);
+
+ checkOutputBuffer(D6O_IAPREFIX);
+
+ // Check that option can be disposed safely
+ EXPECT_NO_THROW(opt.reset());
+}
+
+// Checks whether a new option can be built correctly
+TEST_F(Option6IAPrefixTest, build) {
+
+ boost::scoped_ptr<Option6IAPrefix> opt;
+ setExampleBuffer();
+
+ ASSERT_NO_THROW(opt.reset(new Option6IAPrefix(12345,
+ IOAddress("2001:db8:1::dead:beef"), 77, 1000, 3000000000u)));
+ ASSERT_TRUE(opt);
+
+ checkOption(*opt, 12345);
+
+ // Check if we can build it properly
+ EXPECT_NO_THROW(opt->pack(outBuf_));
+ EXPECT_EQ(29, outBuf_.getLength());
+ checkOutputBuffer(12345);
+
+ // Check that option can be disposed safely
+ EXPECT_NO_THROW(opt.reset());
+}
+
+// Checks negative cases
+TEST_F(Option6IAPrefixTest, negative) {
+
+ // Truncated option (at least 25 bytes is needed)
+ EXPECT_THROW(Option6IAPrefix(D6O_IAPREFIX, buf_.begin(), buf_.begin() + 24),
+ OutOfRange);
+
+ // Empty option
+ EXPECT_THROW(Option6IAPrefix(D6O_IAPREFIX, buf_.begin(), buf_.begin()),
+ OutOfRange);
+
+ // This is for IPv6 prefixes only
+ EXPECT_THROW(Option6IAPrefix(12345, IOAddress("192.0.2.1"), 77, 1000, 2000),
+ BadValue);
+
+ // Prefix length can't be larger than 128
+ EXPECT_THROW(Option6IAPrefix(12345, IOAddress("192.0.2.1"), 255, 1000, 2000),
+ BadValue);
+}
+
+}
diff --git a/src/lib/dhcp/tests/option_custom_unittest.cc b/src/lib/dhcp/tests/option_custom_unittest.cc
index 2a9f771..6b181e6 100644
--- a/src/lib/dhcp/tests/option_custom_unittest.cc
+++ b/src/lib/dhcp/tests/option_custom_unittest.cc
@@ -32,6 +32,59 @@ public:
/// @brief Constructor.
OptionCustomTest() { }
+ /// @brief Appends DHCPv4 suboption in the on-wire format to the buffer.
+ ///
+ /// @param buf A buffer to which suboption is appended.
+ void appendV4Suboption(OptionBuffer& buf) {
+ const uint8_t subopt_data[] = {
+ 0x01, 0x02, // Option type = 1, length = 2
+ 0x01, 0x02 // Two bytes of data
+ };
+ buf.insert(buf.end(), subopt_data, subopt_data + sizeof(subopt_data));
+ }
+
+ /// @brief Check if the parsed option has a suboption.
+ ///
+ /// @param opt An option in which suboption is expected.
+ /// @return Assertion result indicating that the suboption is
+ /// present (success) or missing (failure).
+ ::testing::AssertionResult hasV4Suboption(OptionCustom* opt) {
+ OptionPtr subopt = opt->getOption(1);
+ if (!subopt) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Suboption of OptionCustom"
+ " is missing"));
+ }
+ return (::testing::AssertionSuccess());
+ }
+
+ /// @brief Appends DHCPv6 suboption in the on-wire format to the buffer.
+ ///
+ /// @param buf A buffer to which suboption is appended.
+ void appendV6Suboption(OptionBuffer& buf) {
+ const uint8_t subopt_data[] = {
+ 0x00, 0x01, // Option type = 1
+ 0x00, 0x04, // Option length = 4
+ 0x01, 0x02, 0x03, 0x04 // Four bytes of data
+ };
+ buf.insert(buf.end(), subopt_data, subopt_data + sizeof(subopt_data));
+ }
+
+ /// @brief Check if the parsed option has a suboption.
+ ///
+ /// @param opt An option in which suboption is expected.
+ /// @return Assertion result indicating that the suboption is
+ /// present (success) or missing (failure).
+ ::testing::AssertionResult hasV6Suboption(OptionCustom* opt) {
+ OptionPtr subopt = opt->getOption(1);
+ if (!subopt) {
+ return (::testing::AssertionFailure(::testing::Message()
+ << "Suboption of OptionCustom"
+ " is missing"));
+ }
+ return (::testing::AssertionSuccess());
+ }
+
/// @brief Write IP address into a buffer.
///
/// @param address address to be written.
@@ -114,23 +167,30 @@ TEST_F(OptionCustomTest, constructor) {
// The purpose of this test is to verify that 'empty' option definition can
// be used to create an instance of custom option.
TEST_F(OptionCustomTest, emptyData) {
- OptionDefinition opt_def("OPTION_FOO", 232, "empty");
+ OptionDefinition opt_def("option-foo", 232, "empty", "option-foo-space");
+ // Create a buffer holding 1 suboption.
OptionBuffer buf;
+ appendV4Suboption(buf);
+
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
- option.reset(new OptionCustom(opt_def, Option::V4, buf.begin(), buf.end()));
+ option.reset(new OptionCustom(opt_def, Option::V4, buf.begin(),
+ buf.end()));
);
ASSERT_TRUE(option);
// Option is 'empty' so no data fields are expected.
EXPECT_EQ(0, option->getDataFieldsNum());
+
+ // Check that suboption has been parsed.
+ EXPECT_TRUE(hasV4Suboption(option.get()));
}
// The purpose of this test is to verify that the option definition comprising
// a binary value can be used to create an instance of custom option.
TEST_F(OptionCustomTest, binaryData) {
- OptionDefinition opt_def("OPTION_FOO", 231, "binary");
+ OptionDefinition opt_def("option-foo", 231, "binary", "option-foo-space");
// Create a buffer holding some binary data. This data will be
// used as reference when we read back the data from a created
@@ -139,6 +199,11 @@ TEST_F(OptionCustomTest, binaryData) {
for (int i = 0; i < 14; ++i) {
buf_in[i] = i;
}
+
+ // Append suboption data. This data should NOT be recognized when
+ // option has a binary format.
+ appendV4Suboption(buf_in);
+
// Use scoped pointer because it allows to declare the option
// in the function scope and initialize it under ASSERT.
boost::scoped_ptr<OptionCustom> option;
@@ -169,20 +234,24 @@ TEST_F(OptionCustomTest, binaryData) {
buf_in.end())),
isc::OutOfRange
);
+
+ // Suboptions are not recognized for the binary formats because as it is
+ // a variable length format. Therefore, we expect that there are no
+ // suboptions in the parsed option.
+ EXPECT_FALSE(option->getOption(1));
}
// The purpose of this test is to verify that an option definition comprising
// a single boolean value can be used to create an instance of custom option.
TEST_F(OptionCustomTest, booleanData) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "boolean");
+ OptionDefinition opt_def("option-foo", 1000, "boolean", "option-foo-space");
OptionBuffer buf;
// Push back the value that represents 'false'.
buf.push_back(0);
- // Push back the 'true' value. Note that this value should
- // be ignored by custom option because it holds single boolean
- // value (according to option definition).
- buf.push_back(1);
+
+ // Append suboption. It should be present in the parsed packet.
+ appendV6Suboption(buf);
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
@@ -202,10 +271,14 @@ TEST_F(OptionCustomTest, booleanData) {
ASSERT_NO_THROW(value = option->readBoolean(0));
EXPECT_FALSE(value);
+ // There should be one suboption present.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
// Check that the option with "no data" is rejected.
buf.clear();
EXPECT_THROW(
- option.reset(new OptionCustom(opt_def, Option::V6, buf.begin(), buf.end())),
+ option.reset(new OptionCustom(opt_def, Option::V6, buf.begin(),
+ buf.end())),
isc::OutOfRange
);
}
@@ -213,7 +286,7 @@ TEST_F(OptionCustomTest, booleanData) {
// The purpose of this test is to verify that the data from a buffer
// can be read as FQDN.
TEST_F(OptionCustomTest, fqdnData) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "fqdn");
+ OptionDefinition opt_def("option-foo", 1000, "fqdn", "option-foo-space");
const char data[] = {
8, 109, 121, 100, 111, 109, 97, 105, 110, // "mydomain"
@@ -224,6 +297,10 @@ TEST_F(OptionCustomTest, fqdnData) {
std::vector<uint8_t> buf(data, data + sizeof(data));
+ // The FQDN has a certain boundary. Right after FQDN it should be
+ // possible to append suboption and parse it correctly.
+ appendV6Suboption(buf);
+
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
option.reset(new OptionCustom(opt_def, Option::V6, buf.begin(), buf.end()));
@@ -235,6 +312,9 @@ TEST_F(OptionCustomTest, fqdnData) {
std::string domain0 = option->readFqdn(0);
EXPECT_EQ("mydomain.example.com.", domain0);
+ // This option should have one suboption.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
// Check that the option with truncated data can't be created.
EXPECT_THROW(
option.reset(new OptionCustom(opt_def, Option::V6,
@@ -246,12 +326,15 @@ TEST_F(OptionCustomTest, fqdnData) {
// The purpose of this test is to verify that the option definition comprising
// 16-bit signed integer value can be used to create an instance of custom option.
TEST_F(OptionCustomTest, int16Data) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "int16");
+ OptionDefinition opt_def("option-foo", 1000, "int16", "option-foo-space");
OptionBuffer buf;
// Store signed integer value in the input buffer.
writeInt<int16_t>(-234, buf);
+ // Append suboption.
+ appendV6Suboption(buf);
+
// Create custom option.
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
@@ -268,6 +351,9 @@ TEST_F(OptionCustomTest, int16Data) {
ASSERT_NO_THROW(value = option->readInteger<int16_t>(0));
EXPECT_EQ(-234, value);
+ // Parsed option should have one suboption.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
// Check that the option is not created when a buffer is
// too short (1 byte instead of 2 bytes).
EXPECT_THROW(
@@ -279,11 +365,13 @@ TEST_F(OptionCustomTest, int16Data) {
// The purpose of this test is to verify that the option definition comprising
// 32-bit signed integer value can be used to create an instance of custom option.
TEST_F(OptionCustomTest, int32Data) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "int32");
+ OptionDefinition opt_def("option-foo", 1000, "int32", "option-foo-space");
OptionBuffer buf;
writeInt<int32_t>(-234, buf);
- writeInt<int32_t>(100, buf);
+
+ // Append one suboption.
+ appendV6Suboption(buf);
// Create custom option.
boost::scoped_ptr<OptionCustom> option;
@@ -301,6 +389,9 @@ TEST_F(OptionCustomTest, int32Data) {
ASSERT_NO_THROW(value = option->readInteger<int32_t>(0));
EXPECT_EQ(-234, value);
+ // The parsed option should have one suboption.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
// Check that the option is not created when a buffer is
// too short (3 bytes instead of 4 bytes).
EXPECT_THROW(
@@ -312,12 +403,16 @@ TEST_F(OptionCustomTest, int32Data) {
// The purpose of this test is to verify that the option definition comprising
// single IPv4 address can be used to create an instance of custom option.
TEST_F(OptionCustomTest, ipv4AddressData) {
- OptionDefinition opt_def("OPTION_FOO", 231, "ipv4-address");
+ OptionDefinition opt_def("OPTION_FOO", 231, "ipv4-address",
+ "option-foo-space");
// Create input buffer.
OptionBuffer buf;
writeAddress(IOAddress("192.168.100.50"), buf);
+ // Append one suboption.
+ appendV4Suboption(buf);
+
// Create custom option.
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
@@ -334,6 +429,9 @@ TEST_F(OptionCustomTest, ipv4AddressData) {
EXPECT_EQ("192.168.100.50", address.toText());
+ // Parsed option should have one suboption.
+ EXPECT_TRUE(hasV4Suboption(option.get()));
+
// Check that option is not created if the provided buffer is
// too short (use 3 bytes instead of 4).
EXPECT_THROW(
@@ -345,12 +443,16 @@ TEST_F(OptionCustomTest, ipv4AddressData) {
// The purpose of this test is to verify that the option definition comprising
// single IPv6 address can be used to create an instance of custom option.
TEST_F(OptionCustomTest, ipv6AddressData) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "ipv6-address");
+ OptionDefinition opt_def("option-foo", 1000, "ipv6-address",
+ "option-foo-space");
// Initialize input buffer.
OptionBuffer buf;
writeAddress(IOAddress("2001:db8:1::100"), buf);
+ // Append suboption.
+ appendV6Suboption(buf);
+
// Create custom option using input buffer.
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
@@ -369,6 +471,9 @@ TEST_F(OptionCustomTest, ipv6AddressData) {
EXPECT_EQ("2001:db8:1::100", address.toText());
+ // Parsed option should have one suboption.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
// Check that option is not created if the provided buffer is
// too short (use 15 bytes instead of 16).
EXPECT_THROW(
@@ -382,12 +487,19 @@ TEST_F(OptionCustomTest, ipv6AddressData) {
// The purpose of this test is to verify that the option definition comprising
// string value can be used to create an instance of custom option.
TEST_F(OptionCustomTest, stringData) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "string");
+ OptionDefinition opt_def("option-foo", 1000, "string", "option-foo-space");
// Create an input buffer holding some string value.
OptionBuffer buf;
writeString("hello world!", buf);
+ // Append suboption. It should not be detected because the string field
+ // has variable length.
+ appendV6Suboption(buf);
+
+ // Append suboption. Since the option has variable length string field,
+ // the suboption should not be recognized.
+
// Create custom option using input buffer.
boost::scoped_ptr<OptionCustom> option;
ASSERT_NO_THROW(
@@ -403,7 +515,14 @@ TEST_F(OptionCustomTest, stringData) {
std::string value;
ASSERT_NO_THROW(value = option->readString(0));
- EXPECT_EQ("hello world!", value);
+ // The initial part of the string should contain the actual string.
+ // The rest of it is a garbage from an attempt to decode suboption
+ // as a string.
+ ASSERT_EQ(20, value.size());
+ EXPECT_EQ("hello world!", value.substr(0, 12));
+
+ // No suboption should be present.
+ EXPECT_FALSE(option->getOption(1));
// Check that option will not be created if empty buffer is provided.
buf.clear();
@@ -416,7 +535,7 @@ TEST_F(OptionCustomTest, stringData) {
// The purpose of this test is to verify that the option definition comprising
// an array of boolean values can be used to create an instance of custom option.
TEST_F(OptionCustomTest, booleanDataArray) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "boolean", true);
+ OptionDefinition opt_def("option-foo", 1000, "boolean", true);
// Create a buffer with 5 values that represent array of
// booleans.
@@ -472,7 +591,7 @@ TEST_F(OptionCustomTest, booleanDataArray) {
// an array of 32-bit signed integer values can be used to create an instance
// of custom option.
TEST_F(OptionCustomTest, uint32DataArray) {
- OptionDefinition opt_def("OPTION_FOO", 1000, "uint32", true);
+ OptionDefinition opt_def("option-foo", 1000, "uint32", true);
// Create an input buffer that holds 4 uint32 values that
// represent an array.
@@ -548,7 +667,7 @@ TEST_F(OptionCustomTest, ipv4AddressDataArray) {
for (int i = 0; i < 3; ++i) {
IOAddress address("10.10.10.10");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
// Check that it is ok if buffer length is not a multiple of IPv4
@@ -598,7 +717,7 @@ TEST_F(OptionCustomTest, ipv6AddressDataArray) {
for (int i = 0; i < 3; ++i) {
IOAddress address("fe80::4");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
// Check that it is ok if buffer length is not a multiple of IPv6
@@ -656,6 +775,47 @@ TEST_F(OptionCustomTest, fqdnDataArray) {
EXPECT_EQ("example.com.", domain1);
}
+// The purpose of this test is to verify that the opton definition comprising
+// a record of fixed-size fields can be used to create an option with a
+// suboption.
+TEST_F(OptionCustomTest, recordDataWithSuboption) {
+ OptionDefinition opt_def("option-foo", 1000, "record", "option-foo-space");
+ ASSERT_NO_THROW(opt_def.addRecordField("uint32"));
+ ASSERT_NO_THROW(opt_def.addRecordField("ipv4-address"));
+
+ // Create a buffer with two fields: 4-byte number and IPv4 address.
+ OptionBuffer buf;
+ writeInt<uint32_t>(0x01020304, buf);
+ writeAddress(IOAddress("192.168.0.1"), buf);
+
+ // Append a suboption. It should be correctly parsed because option fields
+ // preceding this option have fixed (known) size.
+ appendV6Suboption(buf);
+
+ boost::scoped_ptr<OptionCustom> option;
+ ASSERT_NO_THROW(
+ option.reset(new OptionCustom(opt_def, Option::V6, buf.begin(),
+ buf.end()));
+ );
+ ASSERT_TRUE(option);
+
+ // We should have two data fields parsed.
+ ASSERT_EQ(2, option->getDataFieldsNum());
+
+ // Validate values in fields.
+ uint32_t value0 = 0;
+ ASSERT_NO_THROW(value0 = option->readInteger<uint32_t>(0));
+ EXPECT_EQ(0x01020304, value0);
+
+ IOAddress value1 = 0;
+ ASSERT_NO_THROW(value1 = option->readAddress(1));
+ EXPECT_EQ("192.168.0.1", value1.toText());
+
+ // Parsed option should have one suboption.
+ EXPECT_TRUE(hasV6Suboption(option.get()));
+
+}
+
// The purpose of this test is to verify that the option definition comprising
// a record of various data fields can be used to create an instance of
// custom option.
@@ -1291,7 +1451,7 @@ TEST_F(OptionCustomTest, unpack) {
for (int i = 0; i < 3; ++i) {
IOAddress address("10.10.10.10");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
// Remove all addresses we had added. We are going to replace
@@ -1318,13 +1478,13 @@ TEST_F(OptionCustomTest, unpack) {
for (int i = 0; i < 2; ++i) {
IOAddress address("10.10.10.10");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
}
// The purpose of this test is to verify that new data can be set for
// a custom option.
-TEST_F(OptionCustomTest, setData) {
+TEST_F(OptionCustomTest, initialize) {
OptionDefinition opt_def("OPTION_FOO", 1000, "ipv6-address", true);
// Initialize reference data.
@@ -1353,7 +1513,7 @@ TEST_F(OptionCustomTest, setData) {
for (int i = 0; i < 3; ++i) {
IOAddress address("fe80::4");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
// Clear addresses we had previously added.
@@ -1370,7 +1530,7 @@ TEST_F(OptionCustomTest, setData) {
}
// Replace the option data.
- ASSERT_NO_THROW(option->setData(buf.begin(), buf.end()));
+ ASSERT_NO_THROW(option->initialize(buf.begin(), buf.end()));
// Now we should have only 2 data fields.
ASSERT_EQ(2, option->getDataFieldsNum());
@@ -1379,7 +1539,7 @@ TEST_F(OptionCustomTest, setData) {
for (int i = 0; i < 2; ++i) {
IOAddress address("10.10.10.10");
ASSERT_NO_THROW(address = option->readAddress(i));
- EXPECT_EQ(addresses[i].toText(), address.toText());
+ EXPECT_EQ(addresses[i], address);
}
}
diff --git a/src/lib/dhcp/tests/option_data_types_unittest.cc b/src/lib/dhcp/tests/option_data_types_unittest.cc
index fd5294c..a6a33b2 100644
--- a/src/lib/dhcp/tests/option_data_types_unittest.cc
+++ b/src/lib/dhcp/tests/option_data_types_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -62,6 +62,20 @@ public:
}
};
+// The goal of this test is to verify that the getLabelCount returns the
+// correct number of labels in the domain name specified as a string
+// parameter.
+TEST_F(OptionDataTypesTest, getLabelCount) {
+ EXPECT_EQ(0, OptionDataTypeUtil::getLabelCount(""));
+ EXPECT_EQ(1, OptionDataTypeUtil::getLabelCount("."));
+ EXPECT_EQ(2, OptionDataTypeUtil::getLabelCount("example"));
+ EXPECT_EQ(3, OptionDataTypeUtil::getLabelCount("example.com"));
+ EXPECT_EQ(3, OptionDataTypeUtil::getLabelCount("example.com."));
+ EXPECT_EQ(4, OptionDataTypeUtil::getLabelCount("myhost.example.com"));
+ EXPECT_THROW(OptionDataTypeUtil::getLabelCount(".abc."),
+ isc::dhcp::BadDataTypeCast);
+}
+
// The goal of this test is to verify that an IPv4 address being
// stored in a buffer (wire format) can be read into IOAddress
// object.
@@ -79,7 +93,7 @@ TEST_F(OptionDataTypesTest, readAddress) {
// Check that the read address matches address that
// we used as input.
- EXPECT_EQ(address.toText(), address_out.toText());
+ EXPECT_EQ(address, address_out);
// Check that an attempt to read the buffer as IPv6 address
// causes an error as the IPv6 address needs at least 16 bytes
@@ -95,7 +109,7 @@ TEST_F(OptionDataTypesTest, readAddress) {
address = asiolink::IOAddress("2001:db8:1:0::1");
writeAddress(address, buf);
EXPECT_NO_THROW(address_out = OptionDataTypeUtil::readAddress(buf, AF_INET6));
- EXPECT_EQ(address.toText(), address_out.toText());
+ EXPECT_EQ(address, address_out);
// Truncate the buffer and expect an error to be reported when
// trying to read it.
diff --git a/src/lib/dhcp/tests/option_definition_unittest.cc b/src/lib/dhcp/tests/option_definition_unittest.cc
index 174bafb..357ed56 100644
--- a/src/lib/dhcp/tests/option_definition_unittest.cc
+++ b/src/lib/dhcp/tests/option_definition_unittest.cc
@@ -25,6 +25,7 @@
#include <dhcp/option_definition.h>
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
+#include <dhcp/option_string.h>
#include <exceptions/exceptions.h>
#include <boost/pointer_cast.hpp>
@@ -438,7 +439,7 @@ TEST_F(OptionDefinitionTest, ipv4AddressArrayTokenized) {
EXPECT_TRUE(std::equal(addrs.begin(), addrs.end(), addrs_returned.begin()));
}
-// The purpose of thie test is to verify that option definition for
+// The purpose of this test is to verify that option definition for
// 'empty' option can be created and that it returns 'empty' option.
TEST_F(OptionDefinitionTest, empty) {
OptionDefinition opt_def("OPTION_RAPID_COMMIT", D6O_RAPID_COMMIT, "empty");
@@ -463,6 +464,49 @@ TEST_F(OptionDefinitionTest, empty) {
EXPECT_EQ(0, option_v4->getData().size());
}
+// The purpose of this test is to verify that when the empty option encapsulates
+// some option space, an instance of the OptionCustom is returned and its
+// suboptions are decoded.
+TEST_F(OptionDefinitionTest, emptyWithSuboptions) {
+ // Create an instance of the 'empty' option definition. This option
+ // encapsulates 'option-foo-space' so when we create a new option
+ // with this definition the OptionCustom should be returned. The
+ // Option Custom is generic option which support variety of formats
+ // and supports decoding suboptions.
+ OptionDefinition opt_def("option-foo", 1024, "empty", "option-foo-space");
+
+ // Define a suboption.
+ const uint8_t subopt_data[] = {
+ 0x04, 0x01, // Option code 1025
+ 0x00, 0x04, // Option len = 4
+ 0x01, 0x02, 0x03, 0x04 // Option data
+ };
+
+ // Create an option, having option code 1024 from the definition. Pass
+ // the option buffer containing suboption.
+ OptionPtr option_v6;
+ ASSERT_NO_THROW(
+ option_v6 = opt_def.optionFactory(Option::V6, 1024,
+ OptionBuffer(subopt_data,
+ subopt_data +
+ sizeof(subopt_data)))
+ );
+ // Returned option should be of the OptionCustom type.
+ ASSERT_TRUE(typeid(*option_v6) == typeid(OptionCustom));
+ // Sanity-check length, universe etc.
+ EXPECT_EQ(Option::V6, option_v6->getUniverse());
+ EXPECT_EQ(4, option_v6->getHeaderLen());
+ // This option should have one suboption with the code of 1025.
+ OptionPtr subopt_v6 = option_v6->getOption(1025);
+ EXPECT_TRUE(subopt_v6);
+ // Check that this suboption holds valid data.
+ EXPECT_EQ(1025, subopt_v6->getType());
+ EXPECT_EQ(Option::V6, subopt_v6->getUniverse());
+ EXPECT_EQ(0, memcmp(&subopt_v6->getData()[0], subopt_data + 4, 4));
+
+ // @todo consider having a similar test for V4.
+}
+
// The purpose of this test is to verify that definition can be
// creates for the option that holds binary data.
TEST_F(OptionDefinitionTest, binary) {
@@ -927,7 +971,7 @@ TEST_F(OptionDefinitionTest, utf8StringTokenized) {
// Let's create some dummy option.
const uint16_t opt_code = 80;
OptionDefinition opt_def("OPTION_WITH_STRING", opt_code, "string");
-
+
std::vector<std::string> values;
values.push_back("Hello World");
values.push_back("this string should not be included in the option");
@@ -936,11 +980,10 @@ TEST_F(OptionDefinitionTest, utf8StringTokenized) {
option_v6 = opt_def.optionFactory(Option::V6, opt_code, values);
);
ASSERT_TRUE(option_v6);
- ASSERT_TRUE(typeid(*option_v6) == typeid(OptionCustom));
- std::vector<uint8_t> data = option_v6->getData();
- std::vector<uint8_t> ref_data(values[0].c_str(), values[0].c_str()
- + values[0].length());
- EXPECT_TRUE(std::equal(ref_data.begin(), ref_data.end(), data.begin()));
+ ASSERT_TRUE(typeid(*option_v6) == typeid(OptionString));
+ OptionStringPtr option_v6_string =
+ boost::static_pointer_cast<OptionString>(option_v6);
+ EXPECT_TRUE(values[0] == option_v6_string->getValue());
}
// The purpose of this test is to check that non-integer data type can't
@@ -951,8 +994,8 @@ TEST_F(OptionDefinitionTest, integerInvalidType) {
// see if it rejects it.
OptionBuffer buf(1);
EXPECT_THROW(
- OptionDefinition::factoryInteger<bool>(Option::V6, D6O_PREFERENCE,
- buf.begin(), buf.end()),
+ OptionDefinition::factoryInteger<bool>(Option::V6, D6O_PREFERENCE, "dhcp6",
+ buf.begin(), buf.end(), NULL),
isc::dhcp::InvalidDataType
);
}
@@ -960,7 +1003,7 @@ TEST_F(OptionDefinitionTest, integerInvalidType) {
// The purpose of this test is to verify that helper methods
// haveIA6Format and haveIAAddr6Format can be used to determine
// IA_NA and IAADDR option formats.
-TEST_F(OptionDefinitionTest, recognizeFormat) {
+TEST_F(OptionDefinitionTest, haveIAFormat) {
// IA_NA option format.
OptionDefinition opt_def1("OPTION_IA_NA", D6O_IA_NA, "record");
for (int i = 0; i < 3; ++i) {
@@ -984,4 +1027,19 @@ TEST_F(OptionDefinitionTest, recognizeFormat) {
EXPECT_FALSE(opt_def4.haveIAAddr6Format());
}
+// This test verifies that haveClientFqdnFormat function recognizes that option
+// definition describes the format of DHCPv6 Client Fqdn Option Format.
+TEST_F(OptionDefinitionTest, haveClientFqdnFormat) {
+ OptionDefinition opt_def("OPTION_CLIENT_FQDN", D6O_CLIENT_FQDN, "record");
+ opt_def.addRecordField("uint8");
+ opt_def.addRecordField("fqdn");
+ EXPECT_TRUE(opt_def.haveClientFqdnFormat());
+
+ // Create option format which is not matching the Client FQDN option format
+ // to verify that tested function does dont always return true.
+ OptionDefinition opt_def_invalid("OPTION_CLIENT_FQDN", D6O_CLIENT_FQDN,
+ "uint8");
+ EXPECT_FALSE(opt_def_invalid.haveClientFqdnFormat());
+}
+
} // anonymous namespace
diff --git a/src/lib/dhcp/tests/option_string_unittest.cc b/src/lib/dhcp/tests/option_string_unittest.cc
new file mode 100644
index 0000000..0be3c3d
--- /dev/null
+++ b/src/lib/dhcp/tests/option_string_unittest.cc
@@ -0,0 +1,160 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/option_string.h>
+
+#include <boost/scoped_ptr.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+/// @brief OptionString test class.
+class OptionStringTest : public ::testing::Test {
+public:
+ /// @brief Constructor.
+ ///
+ /// Initializes the test buffer with some data.
+ OptionStringTest() {
+ std::string test_string("This is a test string");
+ buf_.assign(test_string.begin(), test_string.end());
+ }
+
+ OptionBuffer buf_;
+
+};
+
+// This test verifies that the constructor which creates an option instance
+// from a string value will create it properly.
+TEST_F(OptionStringTest, constructorFromString) {
+ const std::string optv4_value = "some option";
+ OptionString optv4(Option::V4, 123, optv4_value);
+ EXPECT_EQ(Option::V4, optv4.getUniverse());
+ EXPECT_EQ(123, optv4.getType());
+ EXPECT_EQ(optv4_value, optv4.getValue());
+ EXPECT_EQ(Option::OPTION4_HDR_LEN + optv4_value.size(), optv4.len());
+
+ // Do another test with the same constructor to make sure that
+ // different set of parameters would initialize the class members
+ // to different values.
+ const std::string optv6_value = "other option";
+ OptionString optv6(Option::V6, 234, optv6_value);
+ EXPECT_EQ(Option::V6, optv6.getUniverse());
+ EXPECT_EQ(234, optv6.getType());
+ EXPECT_EQ("other option", optv6.getValue());
+ EXPECT_EQ(Option::OPTION6_HDR_LEN + optv6_value.size(), optv6.len());
+
+ // Check that an attempt to use empty string in the constructor
+ // will result in an exception.
+ EXPECT_THROW(OptionString(Option::V6, 123, ""), isc::OutOfRange);
+}
+
+// This test verifies that the constructor which creates an option instance
+// from a buffer, holding option payload, will create it properly.
+// This function calls unpack() internally thus test test is considered
+// to cover testing of unpack() functionality.
+TEST_F(OptionStringTest, constructorFromBuffer) {
+ // Attempt to create an option using empty buffer should result in
+ // an exception.
+ EXPECT_THROW(
+ OptionString(Option::V4, 234, buf_.begin(), buf_.begin()),
+ isc::OutOfRange
+ );
+
+ // Declare option as a scoped pointer here so as its scope is
+ // function wide. The initialization (constructor invocation)
+ // is pushed to the ASSERT_NO_THROW macro below, as it may
+ // throw exception if buffer is truncated.
+ boost::scoped_ptr<OptionString> optv4;
+ ASSERT_NO_THROW(
+ optv4.reset(new OptionString(Option::V4, 234, buf_.begin(), buf_.end()));
+ );
+ // Make sure that it has been initialized to non-NULL value.
+ ASSERT_TRUE(optv4);
+
+ // Test the instance of the created option.
+ const std::string optv4_value = "This is a test string";
+ EXPECT_EQ(Option::V4, optv4->getUniverse());
+ EXPECT_EQ(234, optv4->getType());
+ EXPECT_EQ(Option::OPTION4_HDR_LEN + buf_.size(), optv4->len());
+ EXPECT_EQ(optv4_value, optv4->getValue());
+
+ // Do the same test for V6 option.
+ boost::scoped_ptr<OptionString> optv6;
+ ASSERT_NO_THROW(
+ // Let's reduce the size of the buffer by one byte and see if our option
+ // will absorb this little change.
+ optv6.reset(new OptionString(Option::V6, 123, buf_.begin(), buf_.end() - 1));
+ );
+ // Make sure that it has been initialized to non-NULL value.
+ ASSERT_TRUE(optv6);
+
+ // Test the instance of the created option.
+ const std::string optv6_value = "This is a test strin";
+ EXPECT_EQ(Option::V6, optv6->getUniverse());
+ EXPECT_EQ(123, optv6->getType());
+ EXPECT_EQ(Option::OPTION6_HDR_LEN + buf_.size() - 1, optv6->len());
+ EXPECT_EQ(optv6_value, optv6->getValue());
+}
+
+// This test verifies that the current option value can be overriden
+// with new value, using setValue method.
+TEST_F(OptionStringTest, setValue) {
+ // Create an instance of the option and set some initial value.
+ OptionString optv4(Option::V4, 123, "some option");
+ EXPECT_EQ("some option", optv4.getValue());
+ // Replace the value with the new one, and make sure it has
+ // been successful.
+ EXPECT_NO_THROW(optv4.setValue("new option value"));
+ EXPECT_EQ("new option value", optv4.getValue());
+ // Try to set to an empty string. It should throw exception.
+ EXPECT_THROW(optv4.setValue(""), isc::OutOfRange);
+}
+
+// This test verifies that the pack function encodes the option in
+// a on-wire format properly.
+TEST_F(OptionStringTest, pack) {
+ // Create an instance of the option.
+ std::string option_value("sample option value");
+ OptionString optv4(Option::V4, 123, option_value);
+ // Encode the option in on-wire format.
+ OutputBuffer buf(Option::OPTION4_HDR_LEN);
+ EXPECT_NO_THROW(optv4.pack(buf));
+
+ // Sanity check the length of the buffer.
+ ASSERT_EQ(Option::OPTION4_HDR_LEN + option_value.length(),
+ buf.getLength());
+ // Copy the contents of the OutputBuffer to InputBuffer because
+ // the latter has API to read data from it.
+ InputBuffer test_buf(buf.getData(), buf.getLength());
+ // First byte holds option code.
+ EXPECT_EQ(123, test_buf.readUint8());
+ // Second byte holds option length.
+ EXPECT_EQ(option_value.size(), test_buf.readUint8());
+ // Read the option data.
+ std::vector<uint8_t> data;
+ test_buf.readVector(data, test_buf.getLength() - test_buf.getPosition());
+ // And create a string from it.
+ std::string test_string(data.begin(), data.end());
+ // This string should be equal to the string used to create
+ // option's instance.
+ EXPECT_TRUE(option_value == test_string);
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/option_unittest.cc b/src/lib/dhcp/tests/option_unittest.cc
index 237e73b..a3aea9f 100644
--- a/src/lib/dhcp/tests/option_unittest.cc
+++ b/src/lib/dhcp/tests/option_unittest.cc
@@ -15,10 +15,12 @@
#include <config.h>
#include <dhcp/dhcp6.h>
+#include <dhcp/libdhcp++.h>
#include <dhcp/option.h>
#include <exceptions/exceptions.h>
#include <util/buffer.h>
+#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
@@ -35,6 +37,68 @@ using namespace isc::util;
using boost::scoped_ptr;
namespace {
+
+/// @brief A class which contains a custom callback function to unpack options.
+///
+/// This is a class used by the tests which verify that the custom callback
+/// functions can be installed to unpack options from a message. When the
+/// callback function is called, the executed_ member is set to true to allow
+/// verification that the callback was really called. Internally, this class
+/// uses libdhcp++ to unpack options so the options parsing algorithm remains
+/// unchanged after installation of the callback.
+class CustomUnpackCallback {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Marks that callback hasn't been called.
+ CustomUnpackCallback()
+ : executed_(false) {
+ }
+
+ /// @brief A callback
+ ///
+ /// Contains custom implementation of the callback.
+ ///
+ /// @param buf a A buffer holding options in on-wire format.
+ /// @param option_space A name of the option space being encapsulated by
+ /// the option being parsed.
+ /// @param [out] options A reference to the collection where parsed options
+ /// will be stored.
+ /// @param relay_msg_offset Reference to a size_t structure. If specified,
+ /// offset to beginning of relay_msg option will be stored in it.
+ /// @param relay_msg_len reference to a size_t structure. If specified,
+ /// length of the relay_msg option will be stored in it.
+ /// @return An offset to the first byte after last parsed option.
+ size_t execute(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
+ size_t* relay_msg_offset,
+ size_t* relay_msg_len) {
+ // Set the executed_ member to true to allow verification that the
+ // callback has been actually called.
+ executed_ = true;
+ // Use default implementation of the unpack algorithm to parse options.
+ return (LibDHCP::unpackOptions6(buf, option_space, options, relay_msg_offset,
+ relay_msg_len));
+ }
+
+ /// A flag which indicates if callback function has been called.
+ bool executed_;
+};
+
+/// @brief A class which derives from option and exposes protected members.
+class NakedOption : public Option {
+public:
+ /// @brief Constructor
+ ///
+ /// Sets the universe and option type to arbitrary test values.
+ NakedOption() : Option(Option::V6, 258) {
+ }
+
+ using Option::unpackOptions;
+};
+
class OptionTest : public ::testing::Test {
public:
OptionTest(): buf_(255), outBuf_(255) {
@@ -505,4 +569,73 @@ TEST_F(OptionTest, equal) {
EXPECT_TRUE(opt2->equal(opt5));
}
+
+// This test verifies that the name of the option space being encapsulated by
+// the particular option can be set.
+TEST_F(OptionTest, setEncapsulatedSpace) {
+ Option optv6(Option::V6, 258);
+ EXPECT_TRUE(optv6.getEncapsulatedSpace().empty());
+
+ optv6.setEncapsulatedSpace("dhcp6");
+ EXPECT_EQ("dhcp6", optv6.getEncapsulatedSpace());
+
+ Option optv4(Option::V4, 125);
+ EXPECT_TRUE(optv4.getEncapsulatedSpace().empty());
+
+ optv4.setEncapsulatedSpace("dhcp4");
+ EXPECT_EQ("dhcp4", optv4.getEncapsulatedSpace());
+
+}
+
+// This test verifies that it is possible to specify custom implementation of
+// the option parsing algorithm by installing a callback function.
+TEST_F(OptionTest, unpackCallback) {
+ // Create a buffer which holds two sub options.
+ const char opt_data[] = {
+ 0x00, 0x01, // sub option code = 1
+ 0x00, 0x02, // sub option length = 2
+ 0x00, 0x01, // sub option data (2 bytes)
+ 0x00, 0x02, // sub option code = 2
+ 0x00, 0x02, // sub option length = 2
+ 0x01, 0x01 // sub option data (2 bytes)
+ };
+ OptionBuffer opt_buf(opt_data, opt_data + sizeof(opt_data));
+
+ // Make sure that the flag which indicates if the callback function has
+ // been called is not set. Otherwise, our test doesn't make sense.
+ CustomUnpackCallback cb;
+ ASSERT_FALSE(cb.executed_);
+ // Create an option and install a callback.
+ NakedOption option;
+ // Parameters from _1 to _5 are placeholders for the actual values
+ // to be passed to the callback function. See: boost::bind documentation
+ // at http://www.boost.org/doc/libs/1_54_0/libs/bind/bind.html.
+ // Also, see UnpackOptionsCallback in option.h for description of the
+ // parameter values.
+ option.setCallback(boost::bind(&CustomUnpackCallback::execute, &cb,
+ _1, _2, _3, _4, _5));
+ // Parse options. It should result in a call to our callback function.
+ // This function uses LibDHCP to parse options so they should be parsed
+ // correctly.
+ ASSERT_NO_THROW(option.unpackOptions(opt_buf));
+ EXPECT_TRUE(option.getOption(1));
+ EXPECT_TRUE(option.getOption(2));
+ EXPECT_FALSE(option.getOption(3));
+ // The callback should have been registered.
+ EXPECT_TRUE(cb.executed_);
+ // Reset the flag because now we are going to uninstall the callback and
+ // verify that it was NOT called.
+ cb.executed_ = false;
+ // Uninstall the callback.
+ option.setCallback(NULL);
+ ASSERT_NO_THROW(option.unpackOptions(opt_buf));
+ // Options should still get unpacked...
+ EXPECT_TRUE(option.getOption(1));
+ EXPECT_TRUE(option.getOption(2));
+ EXPECT_FALSE(option.getOption(3));
+ // ... but not via callback.
+ EXPECT_FALSE(cb.executed_);
+}
+
+
}
diff --git a/src/lib/dhcp/tests/option_vendor_unittest.cc b/src/lib/dhcp/tests/option_vendor_unittest.cc
new file mode 100644
index 0000000..9b0f5fa
--- /dev/null
+++ b/src/lib/dhcp/tests/option_vendor_unittest.cc
@@ -0,0 +1,240 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/option_vendor.h>
+#include <dhcp/option_int_array.h>
+#include <exceptions/exceptions.h>
+#include <util/buffer.h>
+#include <util/encode/hex.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::util;
+using boost::scoped_ptr;
+
+namespace {
+
+class OptionVendorTest : public ::testing::Test {
+public:
+ OptionVendorTest() {
+ }
+
+ OptionBuffer createV4VendorOptions() {
+
+ // Copied from wireshark, file docsis-*-CG3000DCR-Registration-Filtered.cap
+ // packet #1
+ /* V-I Vendor-specific Information (125)
+ Length: 127
+ Enterprise ID: Cable Television Laboratories, Inc. (4491)
+ Suboption 1: Option Request
+ Suboption 5: Modem capabilties */
+ string from_wireshark = "7d7f0000118b7a01010205750101010201030301010401"
+ "0105010106010107010f0801100901030a01010b01180c01010d0200400e020010"
+ "0f010110040000000211010014010015013f1601011701011801041901041a0104"
+ "1b01201c01021d01081e01201f0110200110210102220101230100240100250101"
+ "260200ff270101";
+
+ OptionBuffer bin;
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(from_wireshark, bin);
+
+ return (bin);
+ }
+
+ OptionBuffer createV6VendorOption() {
+
+ // Copied from wireshark, docsis-CG3000DCR-Registration-v6CMM-Filtered.cap
+ // packet #1 (v6 vendor option with lots of cable modem specific data)
+ string from_wireshark = "001100ff0000118b0001000a0020002100220025002600"
+ "02000345434d0003000b45434d3a45524f555445520004000d3242523232395534"
+ "303034344300050004312e30340006000856312e33332e303300070007322e332e"
+ "3052320008000630303039354200090009434733303030444352000a00074e6574"
+ "6765617200230077057501010102010303010104010105010106010107010f0801"
+ "100901030a01010b01180c01010d0200400e0200100f0101100400000002110100"
+ "14010015013f1601011701011801041901041a01041b01201c01021d01081e0120"
+ "1f0110200110210102220101230100240100250101260200ff2701010024000620"
+ "e52ab81514";
+ /* Vendor-specific Information
+ Option: Vendor-specific Information (17)
+ Length: 255
+ Value: 0000118b0001000a00200021002200250026000200034543...
+ Enterprise ID: Cable Television Laboratories, Inc. (4491)
+ Suboption 1: Option Request = 32 33 34 37 38
+ Suboption 2: Device Type = "ECM"
+ Suboption 3: Embedded Components = "ECM:EROUTER"
+ Suboption 4: Serial Number = "2BR229U40044C"
+ Suboption 5: Hardware Version = "1.04"
+ Suboption 6: Software Version = "V1.33.03"
+ Suboption 7: Boot ROM Version = "2.3.0R2"
+ Suboption 8: Organization Unique Identifier = "00095B"
+ Suboption 9: Model Number = "CG3000DCR"
+ Suboption 10: Vendor Name = "Netgear"
+ Suboption 35: TLV5 = 057501010102010303010104010105010106010107010f08...
+ Suboption 36: Device Identifier = 20e52ab81514 */
+
+ OptionBuffer bin;
+ // Decode the hex string and store it in bin (which happens
+ // to be OptionBuffer format)
+ isc::util::encode::decodeHex(from_wireshark, bin);
+
+ return (bin);
+ }
+};
+
+// Basic test for v4 vendor option functionality
+TEST_F(OptionVendorTest, v4Basic) {
+
+ uint32_t vendor_id = 1234;
+
+ scoped_ptr<Option> opt;
+ EXPECT_NO_THROW(opt.reset(new OptionVendor(Option::V4, vendor_id)));
+
+ EXPECT_EQ(Option::V4, opt->getUniverse());
+ EXPECT_EQ(DHO_VIVSO_SUBOPTIONS, opt->getType());
+
+ // Minimal length is 7: 1(type) + 1(length) + 4(vendor-id) + datalen(1)
+ EXPECT_EQ(7, opt->len());
+
+ // Check destructor
+ EXPECT_NO_THROW(opt.reset());
+}
+
+// Basic test for v6 vendor option functionality
+TEST_F(OptionVendorTest, v6Basic) {
+
+ uint32_t vendor_id = 1234;
+
+ scoped_ptr<Option> opt;
+ EXPECT_NO_THROW(opt.reset(new OptionVendor(Option::V6, vendor_id)));
+
+ EXPECT_EQ(Option::V6, opt->getUniverse());
+ EXPECT_EQ(D6O_VENDOR_OPTS, opt->getType());
+
+ // Minimal length is 8: 2(type) + 2(length) + 4(vendor-id)
+ EXPECT_EQ(8, opt->len());
+
+ // Check destructor
+ EXPECT_NO_THROW(opt.reset());
+}
+
+// Tests whether we can parse v4 vendor options properly
+TEST_F(OptionVendorTest, v4Parse) {
+ OptionBuffer binary = createV4VendorOptions();
+
+ // Let's create vendor option based on incoming buffer
+ OptionVendorPtr vendor;
+ ASSERT_NO_THROW(vendor.reset(new OptionVendor(Option::V4, binary.begin() + 2,
+ binary.end())));
+
+ // We know that there are supposed to be 2 options inside
+ EXPECT_TRUE(vendor->getOption(DOCSIS3_V4_ORO));
+ EXPECT_TRUE(vendor->getOption(5));
+}
+
+// Tests whether we can parse and then pack a v4 option.
+TEST_F(OptionVendorTest, packUnpack4) {
+ OptionBuffer binary = createV4VendorOptions();
+
+ OptionVendorPtr vendor;
+
+ // Create vendor option (ignore the first 2 bytes, these are option code
+ // and option length
+ ASSERT_NO_THROW(vendor.reset(new OptionVendor(Option::V4, binary.begin() + 2,
+ binary.end())));
+
+ OutputBuffer output(0);
+
+ EXPECT_NO_THROW(vendor->pack(output));
+
+ ASSERT_EQ(binary.size(), output.getLength());
+
+ // We're lucky, because the packet capture we have happens to have options
+ // with monotonically increasing values (1 and 5), so our pack() method
+ // will pack them in exactly the same order as in the original.
+ EXPECT_FALSE(memcmp(&binary[0], output.getData(), output.getLength()));
+}
+
+// Tests whether we can parse v6 vendor options properly
+TEST_F(OptionVendorTest, v6Parse) {
+ OptionBuffer binary = createV6VendorOption();
+
+ OptionVendorPtr vendor;
+ // Create vendor option (ignore the first 4 bytes. These are option code
+ // (2 bytes) and option length (2 bytes).
+ ASSERT_NO_THROW(vendor.reset(new OptionVendor(Option::V6, binary.begin() + 4,
+ binary.end())));
+
+ OptionPtr opt;
+ opt = vendor->getOption(DOCSIS3_V6_ORO);
+ ASSERT_TRUE(opt);
+ OptionUint16ArrayPtr oro =
+ boost::dynamic_pointer_cast<OptionUint16Array>(opt);
+
+ // Check that all remaining expected options are there
+ EXPECT_TRUE(vendor->getOption(2));
+ EXPECT_TRUE(vendor->getOption(3));
+ EXPECT_TRUE(vendor->getOption(4));
+ EXPECT_TRUE(vendor->getOption(5));
+ EXPECT_TRUE(vendor->getOption(6));
+ EXPECT_TRUE(vendor->getOption(7));
+ EXPECT_TRUE(vendor->getOption(8));
+ EXPECT_TRUE(vendor->getOption(9));
+ EXPECT_TRUE(vendor->getOption(10));
+ EXPECT_TRUE(vendor->getOption(35));
+ EXPECT_TRUE(vendor->getOption(36));
+
+ // Check that there are no other options there
+ for (uint16_t i = 11; i < 35; ++i) {
+ EXPECT_FALSE(vendor->getOption(i));
+ }
+
+ for (uint16_t i = 37; i < 65535; ++i) {
+ EXPECT_FALSE(vendor->getOption(i));
+ }
+}
+
+// Tests whether we can parse and then pack a v6 option.
+TEST_F(OptionVendorTest, packUnpack6) {
+ OptionBuffer binary = createV6VendorOption();
+
+ OptionVendorPtr vendor;
+
+ // Create vendor option (ignore the first 4 bytes. These are option code
+ // (2 bytes) and option length (2 bytes).
+ ASSERT_NO_THROW(vendor.reset(new OptionVendor(Option::V6, binary.begin() + 4,
+ binary.end())));
+
+ OutputBuffer output(0);
+
+ EXPECT_NO_THROW(vendor->pack(output));
+
+ ASSERT_EQ(binary.size(), output.getLength());
+ EXPECT_FALSE(memcmp(&binary[0], output.getData(), output.getLength()));
+}
+
+}
diff --git a/src/lib/dhcp/tests/pkt4_unittest.cc b/src/lib/dhcp/tests/pkt4_unittest.cc
index 5c95f7d..bfebe77 100644
--- a/src/lib/dhcp/tests/pkt4_unittest.cc
+++ b/src/lib/dhcp/tests/pkt4_unittest.cc
@@ -16,6 +16,9 @@
#include <asiolink/io_address.h>
#include <dhcp/dhcp4.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <dhcp/option_string.h>
#include <dhcp/pkt4.h>
#include <exceptions/exceptions.h>
#include <util/buffer.h>
@@ -41,40 +44,60 @@ using boost::scoped_ptr;
namespace {
-TEST(Pkt4Test, constructor) {
-
- ASSERT_EQ(236U, static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) );
- scoped_ptr<Pkt4> pkt;
-
- // Just some dummy payload.
- uint8_t testData[250];
- for (int i = 0; i < 250; i++) {
- testData[i] = i;
+/// @brief A class which contains a custom callback function to unpack options.
+///
+/// This is a class used by the tests which verify that the custom callback
+/// functions can be installed to unpack options from a message. When the
+/// callback function is called, the executed_ member is set to true to allow
+/// verification that the callback was really called. Internally, this class
+/// uses libdhcp++ to unpack options so the options parsing algorithm remains
+/// unchanged after installation of the callback.
+class CustomUnpackCallback {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Marks that callback hasn't been called.
+ CustomUnpackCallback()
+ : executed_(false) {
}
- // Positive case1. Normal received packet.
- EXPECT_NO_THROW(pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN)));
-
- EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), pkt->len());
-
- EXPECT_NO_THROW(pkt.reset());
-
- // Positive case2. Normal outgoing packet.
- EXPECT_NO_THROW(pkt.reset(new Pkt4(DHCPDISCOVER, 0xffffffff)));
+ /// @brief A callback
+ ///
+ /// Contains custom implementation of the callback.
+ ///
+ /// @param buf a A buffer holding options in on-wire format.
+ /// @param option_space A name of the option space being encapsulated by
+ /// the option being parsed.
+ /// @param [out] options A reference to the collection where parsed options
+ /// will be stored.
+ /// @return An offset to the first byte after last parsed option.
+ size_t execute(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options) {
+ // Set the executed_ member to true to allow verification that the
+ // callback has been actually called.
+ executed_ = true;
+ // Use default implementation of the unpack algorithm to parse options.
+ return (LibDHCP::unpackOptions4(buf, option_space, options));
+ }
- // DHCPv4 packet must be at least 236 bytes long, with Message Type
- // Option taking extra 3 bytes it is 239
- EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) + 3, pkt->len());
- EXPECT_EQ(DHCPDISCOVER, pkt->getType());
- EXPECT_EQ(0xffffffff, pkt->getTransid());
- EXPECT_NO_THROW(pkt.reset());
+ /// A flag which indicates if callback function has been called.
+ bool executed_;
+};
- // Negative case. Should drop truncated messages.
- EXPECT_THROW(
- pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN - 1)),
- OutOfRange
- );
-}
+/// V4 Options being used for pack/unpack testing.
+/// For test simplicity, all selected options have
+/// variable length data so as there are no restrictions
+/// on a length of their data.
+static uint8_t v4_opts[] = {
+ 12, 3, 0, 1, 2, // Hostname
+ 14, 3, 10, 11, 12, // Merit Dump File
+ 53, 1, 2, // Message Type (required to not throw exception during unpack)
+ 60, 3, 20, 21, 22, // Class Id
+ 128, 3, 30, 31, 32, // Vendor specific
+ 254, 3, 40, 41, 42, // Reserved
+};
// Sample data
const uint8_t dummyOp = BOOTREQUEST;
@@ -109,82 +132,179 @@ const uint8_t dummySname[] = "Lorem ipsum dolor sit amet, consectetur "
BOOST_STATIC_ASSERT(sizeof(dummyFile) == Pkt4::MAX_FILE_LEN + 1);
BOOST_STATIC_ASSERT(sizeof(dummySname) == Pkt4::MAX_SNAME_LEN + 1);
-/// @brief Generates test packet.
-///
-/// Allocates and generates test packet, with all fixed fields set to non-zero
-/// values. Content is not always reasonable.
-///
-/// See generateTestPacket2() function that returns exactly the same packet in
-/// on-wire format.
-///
-/// @return pointer to allocated Pkt4 object.
-boost::shared_ptr<Pkt4>
-generateTestPacket1() {
-
- boost::shared_ptr<Pkt4> pkt(new Pkt4(DHCPDISCOVER, dummyTransid));
-
- vector<uint8_t> vectorMacAddr(dummyMacAddr, dummyMacAddr
- +sizeof(dummyMacAddr));
-
- // hwType = 6(ETHERNET), hlen = 6(MAC address len)
- pkt->setHWAddr(dummyHtype, dummyHlen, vectorMacAddr);
- pkt->setHops(dummyHops); // 13 relays. Wow!
- // Transaction-id is already set.
- pkt->setSecs(dummySecs);
- pkt->setFlags(dummyFlags); // all flags set
- pkt->setCiaddr(dummyCiaddr);
- pkt->setYiaddr(dummyYiaddr);
- pkt->setSiaddr(dummySiaddr);
- pkt->setGiaddr(dummyGiaddr);
- // Chaddr already set with setHWAddr().
- pkt->setSname(dummySname, 64);
- pkt->setFile(dummyFile, 128);
-
- return (pkt);
-}
-/// @brief Generates test packet.
-///
-/// Allocates and generates on-wire buffer that represents test packet, with all
-/// fixed fields set to non-zero values. Content is not always reasonable.
-///
-/// See generateTestPacket1() function that returns exactly the same packet as
-/// Pkt4 object.
-///
-/// @return pointer to allocated Pkt4 object
-// Returns a vector containing a DHCPv4 packet header.
-vector<uint8_t>
-generateTestPacket2() {
-
- // That is only part of the header. It contains all "short" fields,
- // larger fields are constructed separately.
- uint8_t hdr[] = {
- 1, 6, 6, 13, // op, htype, hlen, hops,
- 0x12, 0x34, 0x56, 0x78, // transaction-id
- 0, 42, 0x80, 0x00, // 42 secs, BROADCAST flags
- 192, 0, 2, 1, // ciaddr
- 1, 2, 3, 4, // yiaddr
- 192, 0, 2, 255, // siaddr
- 255, 255, 255, 255, // giaddr
- };
+class Pkt4Test : public ::testing::Test {
+public:
+ Pkt4Test() {
+ }
+
+ /// @brief Generates test packet.
+ ///
+ /// Allocates and generates test packet, with all fixed fields set to non-zero
+ /// values. Content is not always reasonable.
+ ///
+ /// See generateTestPacket2() function that returns exactly the same packet in
+ /// on-wire format.
+ ///
+ /// @return pointer to allocated Pkt4 object.
+ Pkt4Ptr generateTestPacket1() {
+
+ boost::shared_ptr<Pkt4> pkt(new Pkt4(DHCPDISCOVER, dummyTransid));
+
+ vector<uint8_t> vectorMacAddr(dummyMacAddr, dummyMacAddr
+ + sizeof(dummyMacAddr));
+
+ // hwType = 6(ETHERNET), hlen = 6(MAC address len)
+ pkt->setHWAddr(dummyHtype, dummyHlen, vectorMacAddr);
+ pkt->setHops(dummyHops); // 13 relays. Wow!
+ // Transaction-id is already set.
+ pkt->setSecs(dummySecs);
+ pkt->setFlags(dummyFlags); // all flags set
+ pkt->setCiaddr(dummyCiaddr);
+ pkt->setYiaddr(dummyYiaddr);
+ pkt->setSiaddr(dummySiaddr);
+ pkt->setGiaddr(dummyGiaddr);
+ // Chaddr already set with setHWAddr().
+ pkt->setSname(dummySname, 64);
+ pkt->setFile(dummyFile, 128);
+
+ return (pkt);
+ }
+
+ /// @brief Generates test packet.
+ ///
+ /// Allocates and generates on-wire buffer that represents test packet, with all
+ /// fixed fields set to non-zero values. Content is not always reasonable.
+ ///
+ /// See generateTestPacket1() function that returns exactly the same packet as
+ /// Pkt4 object.
+ ///
+ /// @return pointer to allocated Pkt4 object
+ // Returns a vector containing a DHCPv4 packet header.
+ vector<uint8_t> generateTestPacket2() {
+
+ // That is only part of the header. It contains all "short" fields,
+ // larger fields are constructed separately.
+ uint8_t hdr[] = {
+ 1, 6, 6, 13, // op, htype, hlen, hops,
+ 0x12, 0x34, 0x56, 0x78, // transaction-id
+ 0, 42, 0x80, 0x00, // 42 secs, BROADCAST flags
+ 192, 0, 2, 1, // ciaddr
+ 1, 2, 3, 4, // yiaddr
+ 192, 0, 2, 255, // siaddr
+ 255, 255, 255, 255, // giaddr
+ };
+
+ // Initialize the vector with the header fields defined above.
+ vector<uint8_t> buf(hdr, hdr + sizeof(hdr));
+
+ // Append the large header fields.
+ copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
+ copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
+ copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));
+
+ // Should now have all the header, so check. The "static_cast" is used
+ // to get round an odd bug whereby the linker appears not to find the
+ // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
+ EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());
+
+ return (buf);
+ }
+
+ /// @brief Verify that the options are correct after parsing.
+ ///
+ /// @param pkt A packet holding parsed options.
+ void verifyParsedOptions(const Pkt4Ptr& pkt) {
+ EXPECT_TRUE(pkt->getOption(12));
+ EXPECT_TRUE(pkt->getOption(60));
+ EXPECT_TRUE(pkt->getOption(14));
+ EXPECT_TRUE(pkt->getOption(128));
+ EXPECT_TRUE(pkt->getOption(254));
+
+ boost::shared_ptr<Option> x = pkt->getOption(12);
+ ASSERT_TRUE(x); // option 1 should exist
+ // Option 12 is represented by the OptionString class so let's do
+ // the appropriate conversion.
+ OptionStringPtr option12 = boost::static_pointer_cast<OptionString>(x);
+ ASSERT_TRUE(option12);
+ EXPECT_EQ(12, option12->getType()); // this should be option 12
+ ASSERT_EQ(3, option12->getValue().length()); // it should be of length 3
+ EXPECT_EQ(5, option12->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&option12->getValue()[0], v4_opts + 2, 3)); // data len=3
+
+ x = pkt->getOption(14);
+ ASSERT_TRUE(x); // option 14 should exist
+ // Option 14 is represented by the OptionString class so let's do
+ // the appropriate conversion.
+ OptionStringPtr option14 = boost::static_pointer_cast<OptionString>(x);
+ ASSERT_TRUE(option14);
+ EXPECT_EQ(14, option14->getType()); // this should be option 14
+ ASSERT_EQ(3, option14->getValue().length()); // it should be of length 3
+ EXPECT_EQ(5, option14->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&option14->getValue()[0], v4_opts + 7, 3)); // data len=3
+
+ x = pkt->getOption(60);
+ ASSERT_TRUE(x); // option 60 should exist
+ EXPECT_EQ(60, x->getType()); // this should be option 60
+ ASSERT_EQ(3, x->getData().size()); // it should be of length 3
+ EXPECT_EQ(5, x->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&x->getData()[0], v4_opts + 15, 3)); // data len=3
+
+ x = pkt->getOption(128);
+ ASSERT_TRUE(x); // option 3 should exist
+ EXPECT_EQ(128, x->getType()); // this should be option 254
+ ASSERT_EQ(3, x->getData().size()); // it should be of length 3
+ EXPECT_EQ(5, x->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&x->getData()[0], v4_opts + 20, 3)); // data len=3
+
+ x = pkt->getOption(254);
+ ASSERT_TRUE(x); // option 3 should exist
+ EXPECT_EQ(254, x->getType()); // this should be option 254
+ ASSERT_EQ(3, x->getData().size()); // it should be of length 3
+ EXPECT_EQ(5, x->len()); // total option length 5
+ EXPECT_EQ(0, memcmp(&x->getData()[0], v4_opts + 25, 3)); // data len=3
+ }
+
+};
- // Initialize the vector with the header fields defined above.
- vector<uint8_t> buf(hdr, hdr + sizeof(hdr));
- // Append the large header fields.
- copy(dummyChaddr, dummyChaddr + Pkt4::MAX_CHADDR_LEN, back_inserter(buf));
- copy(dummySname, dummySname + Pkt4::MAX_SNAME_LEN, back_inserter(buf));
- copy(dummyFile, dummyFile + Pkt4::MAX_FILE_LEN, back_inserter(buf));
+TEST_F(Pkt4Test, constructor) {
- // Should now have all the header, so check. The "static_cast" is used
- // to get round an odd bug whereby the linker appears not to find the
- // definition of DHCPV4_PKT_HDR_LEN if it appears within an EXPECT_EQ().
- EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), buf.size());
+ ASSERT_EQ(236U, static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) );
+ scoped_ptr<Pkt4> pkt;
+
+ // Just some dummy payload.
+ uint8_t testData[250];
+ for (int i = 0; i < 250; i++) {
+ testData[i] = i;
+ }
- return (buf);
+ // Positive case1. Normal received packet.
+ EXPECT_NO_THROW(pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN)));
+
+ EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN), pkt->len());
+
+ EXPECT_NO_THROW(pkt.reset());
+
+ // Positive case2. Normal outgoing packet.
+ EXPECT_NO_THROW(pkt.reset(new Pkt4(DHCPDISCOVER, 0xffffffff)));
+
+ // DHCPv4 packet must be at least 236 bytes long, with Message Type
+ // Option taking extra 3 bytes it is 239
+ EXPECT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) + 3, pkt->len());
+ EXPECT_EQ(DHCPDISCOVER, pkt->getType());
+ EXPECT_EQ(0xffffffff, pkt->getTransid());
+ EXPECT_NO_THROW(pkt.reset());
+
+ // Negative case. Should drop truncated messages.
+ EXPECT_THROW(
+ pkt.reset(new Pkt4(testData, Pkt4::DHCPV4_PKT_HDR_LEN - 1)),
+ OutOfRange
+ );
}
-TEST(Pkt4Test, fixedFields) {
+
+TEST_F(Pkt4Test, fixedFields) {
boost::shared_ptr<Pkt4> pkt = generateTestPacket1();
@@ -197,10 +317,10 @@ TEST(Pkt4Test, fixedFields) {
EXPECT_EQ(dummySecs, pkt->getSecs());
EXPECT_EQ(dummyFlags, pkt->getFlags());
- EXPECT_EQ(dummyCiaddr.toText(), pkt->getCiaddr().toText());
- EXPECT_EQ(dummyYiaddr.toText(), pkt->getYiaddr().toText());
- EXPECT_EQ(dummySiaddr.toText(), pkt->getSiaddr().toText());
- EXPECT_EQ(dummyGiaddr.toText(), pkt->getGiaddr().toText());
+ EXPECT_EQ(dummyCiaddr, pkt->getCiaddr());
+ EXPECT_EQ(dummyYiaddr, pkt->getYiaddr());
+ EXPECT_EQ(dummySiaddr, pkt->getSiaddr());
+ EXPECT_EQ(dummyGiaddr, pkt->getGiaddr());
// Chaddr contains link-layer addr (MAC). It is no longer always 16 bytes
// long and its length depends on hlen value (it is up to 16 bytes now).
@@ -214,7 +334,7 @@ TEST(Pkt4Test, fixedFields) {
EXPECT_EQ(DHCPDISCOVER, pkt->getType());
}
-TEST(Pkt4Test, fixedFieldsPack) {
+TEST_F(Pkt4Test, fixedFieldsPack) {
boost::shared_ptr<Pkt4> pkt = generateTestPacket1();
vector<uint8_t> expectedFormat = generateTestPacket2();
@@ -234,7 +354,7 @@ TEST(Pkt4Test, fixedFieldsPack) {
}
/// TODO Uncomment when ticket #1226 is implemented
-TEST(Pkt4Test, fixedFieldsUnpack) {
+TEST_F(Pkt4Test, fixedFieldsUnpack) {
vector<uint8_t> expectedFormat = generateTestPacket2();
expectedFormat.push_back(0x63); // magic cookie
@@ -263,10 +383,10 @@ TEST(Pkt4Test, fixedFieldsUnpack) {
EXPECT_EQ(dummySecs, pkt->getSecs());
EXPECT_EQ(dummyFlags, pkt->getFlags());
- EXPECT_EQ(dummyCiaddr.toText(), pkt->getCiaddr().toText());
- EXPECT_EQ(string("1.2.3.4"), pkt->getYiaddr().toText());
- EXPECT_EQ(string("192.0.2.255"), pkt->getSiaddr().toText());
- EXPECT_EQ(string("255.255.255.255"), pkt->getGiaddr().toText());
+ EXPECT_EQ(dummyCiaddr, pkt->getCiaddr());
+ EXPECT_EQ("1.2.3.4", pkt->getYiaddr().toText());
+ EXPECT_EQ("192.0.2.255", pkt->getSiaddr().toText());
+ EXPECT_EQ("255.255.255.255", pkt->getGiaddr().toText());
// chaddr is always 16 bytes long and contains link-layer addr (MAC)
EXPECT_EQ(0, memcmp(dummyChaddr, &pkt->getHWAddr()->hwaddr_[0], dummyHlen));
@@ -281,7 +401,7 @@ TEST(Pkt4Test, fixedFieldsUnpack) {
}
// This test is for hardware addresses (htype, hlen and chaddr fields)
-TEST(Pkt4Test, hwAddr) {
+TEST_F(Pkt4Test, hwAddr) {
vector<uint8_t> mac;
uint8_t expectedChaddr[Pkt4::MAX_CHADDR_LEN];
@@ -328,7 +448,7 @@ TEST(Pkt4Test, hwAddr) {
/// longer than 16 bytes should be stored in client-identifier option
}
-TEST(Pkt4Test, msgTypes) {
+TEST_F(Pkt4Test, msgTypes) {
struct msgType {
uint8_t dhcp;
@@ -365,7 +485,7 @@ TEST(Pkt4Test, msgTypes) {
}
// This test verifies handling of sname field
-TEST(Pkt4Test, sname) {
+TEST_F(Pkt4Test, sname) {
uint8_t sname[Pkt4::MAX_SNAME_LEN];
@@ -403,7 +523,7 @@ TEST(Pkt4Test, sname) {
EXPECT_THROW(pkt4.setSname(NULL, 0), InvalidParameter);
}
-TEST(Pkt4Test, file) {
+TEST_F(Pkt4Test, file) {
uint8_t file[Pkt4::MAX_FILE_LEN];
@@ -441,20 +561,7 @@ TEST(Pkt4Test, file) {
EXPECT_THROW(pkt4.setFile(NULL, 0), InvalidParameter);
}
-/// V4 Options being used for pack/unpack testing.
-/// For test simplicity, all selected options have
-/// variable length data so as there are no restrictions
-/// on a length of their data.
-static uint8_t v4Opts[] = {
- 12, 3, 0, 1, 2, // Hostname
- 14, 3, 10, 11, 12, // Merit Dump File
- 53, 1, 2, // Message Type (required to not throw exception during unpack)
- 60, 3, 20, 21, 22, // Class Id
- 128, 3, 30, 31, 32, // Vendor specific
- 254, 3, 40, 41, 42, // Reserved
-};
-
-TEST(Pkt4Test, options) {
+TEST_F(Pkt4Test, options) {
scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 0));
vector<uint8_t> payload[5];
@@ -495,10 +602,10 @@ TEST(Pkt4Test, options) {
);
const OutputBuffer& buf = pkt->getBuffer();
- // Check that all options are stored, they should take sizeof(v4Opts),
+ // Check that all options are stored, they should take sizeof(v4_opts),
// DHCP magic cookie (4 bytes), and OPTION_END added (just one byte)
ASSERT_EQ(static_cast<size_t>(Pkt4::DHCPV4_PKT_HDR_LEN) +
- sizeof(DHCP_OPTIONS_COOKIE) + sizeof(v4Opts) + 1,
+ sizeof(DHCP_OPTIONS_COOKIE) + sizeof(v4_opts) + 1,
buf.getLength());
// That that this extra data actually contain our options
@@ -507,8 +614,8 @@ TEST(Pkt4Test, options) {
// Rewind to end of fixed part.
ptr += Pkt4::DHCPV4_PKT_HDR_LEN + sizeof(DHCP_OPTIONS_COOKIE);
- EXPECT_EQ(0, memcmp(ptr, v4Opts, sizeof(v4Opts)));
- EXPECT_EQ(DHO_END, static_cast<uint8_t>(*(ptr + sizeof(v4Opts))));
+ EXPECT_EQ(0, memcmp(ptr, v4_opts, sizeof(v4_opts)));
+ EXPECT_EQ(DHO_END, static_cast<uint8_t>(*(ptr + sizeof(v4_opts))));
// delOption() checks
EXPECT_TRUE(pkt->getOption(12)); // Sanity check: option 12 is still there
@@ -519,7 +626,8 @@ TEST(Pkt4Test, options) {
EXPECT_NO_THROW(pkt.reset());
}
-TEST(Pkt4Test, unpackOptions) {
+// This test verifies that the options are unpacked from the packet correctly.
+TEST_F(Pkt4Test, unpackOptions) {
vector<uint8_t> expectedFormat = generateTestPacket2();
@@ -528,8 +636,8 @@ TEST(Pkt4Test, unpackOptions) {
expectedFormat.push_back(0x53);
expectedFormat.push_back(0x63);
- for (int i = 0; i < sizeof(v4Opts); i++) {
- expectedFormat.push_back(v4Opts[i]);
+ for (int i = 0; i < sizeof(v4_opts); i++) {
+ expectedFormat.push_back(v4_opts[i]);
}
// now expectedFormat contains fixed format and 5 options
@@ -541,51 +649,53 @@ TEST(Pkt4Test, unpackOptions) {
pkt->unpack()
);
- EXPECT_TRUE(pkt->getOption(12));
- EXPECT_TRUE(pkt->getOption(60));
- EXPECT_TRUE(pkt->getOption(14));
- EXPECT_TRUE(pkt->getOption(128));
- EXPECT_TRUE(pkt->getOption(254));
+ verifyParsedOptions(pkt);
+}
+
+// This test verifies that it is possible to specify custom implementation of
+// the option parsing algorithm by installing a callback function.
+TEST_F(Pkt4Test, unpackOptionsWithCallback) {
+ vector<uint8_t> expectedFormat = generateTestPacket2();
+
+ expectedFormat.push_back(0x63);
+ expectedFormat.push_back(0x82);
+ expectedFormat.push_back(0x53);
+ expectedFormat.push_back(0x63);
+
+ for (int i = 0; i < sizeof(v4_opts); i++) {
+ expectedFormat.push_back(v4_opts[i]);
+ }
+
+ // now expectedFormat contains fixed format and 5 options
+
+ boost::shared_ptr<Pkt4> pkt(new Pkt4(&expectedFormat[0],
+ expectedFormat.size()));
+
+ CustomUnpackCallback cb;
+ pkt->setCallback(boost::bind(&CustomUnpackCallback::execute, &cb,
+ _1, _2, _3));
+
+ ASSERT_FALSE(cb.executed_);
+
+ EXPECT_NO_THROW(pkt->unpack());
+
+ EXPECT_TRUE(cb.executed_);
+ verifyParsedOptions(pkt);
+
+ // Reset the indicator to perform another check: uninstall the callback.
+ cb.executed_ = false;
+ // By setting the callback to NULL we effectively uninstall the callback.
+ pkt->setCallback(NULL);
+ // Do another unpack.
+ EXPECT_NO_THROW(pkt->unpack());
+ // Callback should not be executed.
+ EXPECT_FALSE(cb.executed_);
- boost::shared_ptr<Option> x = pkt->getOption(12);
- ASSERT_TRUE(x); // option 1 should exist
- EXPECT_EQ(12, x->getType()); // this should be option 12
- ASSERT_EQ(3, x->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->getData()[0], v4Opts + 2, 3)); // data len=3
-
- x = pkt->getOption(14);
- ASSERT_TRUE(x); // option 13 should exist
- EXPECT_EQ(14, x->getType()); // this should be option 13
- ASSERT_EQ(3, x->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->getData()[0], v4Opts + 7, 3)); // data len=3
-
- x = pkt->getOption(60);
- ASSERT_TRUE(x); // option 60 should exist
- EXPECT_EQ(60, x->getType()); // this should be option 60
- ASSERT_EQ(3, x->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->getData()[0], v4Opts + 15, 3)); // data len=3
-
- x = pkt->getOption(128);
- ASSERT_TRUE(x); // option 3 should exist
- EXPECT_EQ(128, x->getType()); // this should be option 254
- ASSERT_EQ(3, x->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->getData()[0], v4Opts + 20, 3)); // data len=3
-
- x = pkt->getOption(254);
- ASSERT_TRUE(x); // option 3 should exist
- EXPECT_EQ(254, x->getType()); // this should be option 254
- ASSERT_EQ(3, x->getData().size()); // it should be of length 3
- EXPECT_EQ(5, x->len()); // total option length 5
- EXPECT_EQ(0, memcmp(&x->getData()[0], v4Opts + 25, 3)); // data len=3
}
// This test verifies methods that are used for manipulating meta fields
// i.e. fields that are not part of DHCPv4 (e.g. interface name).
-TEST(Pkt4Test, metaFields) {
+TEST_F(Pkt4Test, metaFields) {
scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
pkt->setIface("loooopback");
@@ -599,7 +709,7 @@ TEST(Pkt4Test, metaFields) {
EXPECT_EQ("4.3.2.1", pkt->getLocalAddr().toText());
}
-TEST(Pkt4Test, Timestamp) {
+TEST_F(Pkt4Test, Timestamp) {
scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
// Just after construction timestamp is invalid
@@ -625,7 +735,7 @@ TEST(Pkt4Test, Timestamp) {
EXPECT_TRUE(ts_period.length().total_microseconds() >= 0);
}
-TEST(Pkt4Test, hwaddr) {
+TEST_F(Pkt4Test, hwaddr) {
scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
const uint8_t hw[] = { 2, 4, 6, 8, 10, 12 }; // MAC
const uint8_t hw_type = 123; // hardware type
@@ -644,4 +754,101 @@ TEST(Pkt4Test, hwaddr) {
EXPECT_TRUE(hwaddr == pkt->getHWAddr());
}
+// This test verifies that the packet remte and local HW address can
+// be set and returned.
+TEST_F(Pkt4Test, hwaddrSrcRemote) {
+ scoped_ptr<Pkt4> pkt(new Pkt4(DHCPOFFER, 1234));
+ const uint8_t src_hw[] = { 1, 2, 3, 4, 5, 6 };
+ const uint8_t dst_hw[] = { 7, 8, 9, 10, 11, 12 };
+ const uint8_t hw_type = 123;
+
+ HWAddrPtr dst_hwaddr(new HWAddr(dst_hw, sizeof(src_hw), hw_type));
+ HWAddrPtr src_hwaddr(new HWAddr(src_hw, sizeof(src_hw), hw_type));
+
+ // Check that we can set the local address.
+ EXPECT_NO_THROW(pkt->setLocalHWAddr(dst_hwaddr));
+ EXPECT_TRUE(dst_hwaddr == pkt->getLocalHWAddr());
+
+ // Check that we can set the remote address.
+ EXPECT_NO_THROW(pkt->setRemoteHWAddr(src_hwaddr));
+ EXPECT_TRUE(src_hwaddr == pkt->getRemoteHWAddr());
+
+ // Can't set the NULL addres.
+ EXPECT_THROW(pkt->setRemoteHWAddr(HWAddrPtr()), BadValue);
+ EXPECT_THROW(pkt->setLocalHWAddr(HWAddrPtr()), BadValue);
+
+ // Test alternative way to set local address.
+ const uint8_t dst_hw2[] = { 19, 20, 21, 22, 23, 24 };
+ std::vector<uint8_t> dst_hw_vec(dst_hw2, dst_hw2 + sizeof(dst_hw2));
+ const uint8_t hw_type2 = 234;
+ EXPECT_NO_THROW(pkt->setLocalHWAddr(hw_type2, sizeof(dst_hw2), dst_hw_vec));
+ HWAddrPtr local_addr = pkt->getLocalHWAddr();
+ ASSERT_TRUE(local_addr);
+ EXPECT_EQ(hw_type2, local_addr->htype_);
+ EXPECT_TRUE(std::equal(dst_hw_vec.begin(), dst_hw_vec.end(),
+ local_addr->hwaddr_.begin()));
+
+ // Set remote address.
+ const uint8_t src_hw2[] = { 25, 26, 27, 28, 29, 30 };
+ std::vector<uint8_t> src_hw_vec(src_hw2, src_hw2 + sizeof(src_hw2));
+ EXPECT_NO_THROW(pkt->setRemoteHWAddr(hw_type2, sizeof(src_hw2), src_hw_vec));
+ HWAddrPtr remote_addr = pkt->getRemoteHWAddr();
+ ASSERT_TRUE(remote_addr);
+ EXPECT_EQ(hw_type2, remote_addr->htype_);
+ EXPECT_TRUE(std::equal(src_hw_vec.begin(), src_hw_vec.end(),
+ remote_addr->hwaddr_.begin()));
+}
+
+// This test verifies that the check for a message being relayed is correct.
+// It also checks that the exception is thrown if the combination of hops and
+// giaddr is invalid.
+TEST_F(Pkt4Test, isRelayed) {
+ Pkt4 pkt(DHCPDISCOVER, 1234);
+ // By default, the hops and giaddr should be 0.
+ ASSERT_EQ("0.0.0.0", pkt.getGiaddr().toText());
+ ASSERT_EQ(0, pkt.getHops());
+ // For hops = 0 and giaddr = 0, the message is non-relayed.
+ EXPECT_FALSE(pkt.isRelayed());
+ // Set giaddr but leave hops = 0. This should result in exception.
+ pkt.setGiaddr(IOAddress("10.0.0.1"));
+ EXPECT_THROW(pkt.isRelayed(), isc::BadValue);
+ // Set hops. Now both hops and giaddr is set. The message is relayed.
+ pkt.setHops(10);
+ EXPECT_TRUE(pkt.isRelayed());
+ // Set giaddr to 0. For hops being set to non-zero value the function
+ // should throw an exception.
+ pkt.setGiaddr(IOAddress("0.0.0.0"));
+ EXPECT_THROW(pkt.isRelayed(), isc::BadValue);
+}
+
+// Tests whether a packet can be assigned to a class and later
+// checked if it belongs to a given class
+TEST_F(Pkt4Test, clientClasses) {
+ Pkt4 pkt(DHCPOFFER, 1234);
+
+ // Default values (do not belong to any class)
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+ EXPECT_TRUE(pkt.classes_.empty());
+
+ // Add to the first class
+ pkt.addClass(DOCSIS3_CLASS_EROUTER);
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+ ASSERT_FALSE(pkt.classes_.empty());
+
+ // Add to a second class
+ pkt.addClass(DOCSIS3_CLASS_MODEM);
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+
+ // Check that it's ok to add to the same class repeatedly
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+
+ // Check that the packet belongs to 'foo'
+ EXPECT_TRUE(pkt.inClass("foo"));
+}
+
} // end of anonymous namespace
diff --git a/src/lib/dhcp/tests/pkt6_unittest.cc b/src/lib/dhcp/tests/pkt6_unittest.cc
index 024ddf0..17cb629 100644
--- a/src/lib/dhcp/tests/pkt6_unittest.cc
+++ b/src/lib/dhcp/tests/pkt6_unittest.cc
@@ -22,7 +22,10 @@
#include <dhcp/option_int.h>
#include <dhcp/option_int_array.h>
#include <dhcp/pkt6.h>
+#include <dhcp/docsis3_option_defs.h>
+#include <util/range_utilities.h>
+#include <boost/bind.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/scoped_ptr.hpp>
#include <util/encode/hex.h>
@@ -40,19 +43,125 @@ using namespace isc::dhcp;
using boost::scoped_ptr;
namespace {
-// empty class for now, but may be extended once Addr6 becomes bigger
+
+/// @brief A class which contains a custom callback function to unpack options.
+///
+/// This is a class used by the tests which verify that the custom callback
+/// functions can be installed to unpack options from a message. When the
+/// callback function is called, the executed_ member is set to true to allow
+/// verification that the callback was really called. Internally, this class
+/// uses libdhcp++ to unpack options so the options parsing algorithm remains
+/// unchanged after installation of the callback.
+class CustomUnpackCallback {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Marks that callback hasn't been called.
+ CustomUnpackCallback()
+ : executed_(false) {
+ }
+
+ /// @brief A callback
+ ///
+ /// Contains custom implementation of the callback.
+ ///
+ /// @param buf a A buffer holding options in on-wire format.
+ /// @param option_space A name of the option space encapsulated by the
+ /// option being parsed.
+ /// @param [out] options A reference to the collection where parsed options
+ /// will be stored.
+ /// @param relay_msg_offset Reference to a size_t structure. If specified,
+ /// offset to beginning of relay_msg option will be stored in it.
+ /// @param relay_msg_len reference to a size_t structure. If specified,
+ /// length of the relay_msg option will be stored in it.
+ /// @return An offset to the first byte after last parsed option.
+ size_t execute(const OptionBuffer& buf,
+ const std::string& option_space,
+ isc::dhcp::OptionCollection& options,
+ size_t* relay_msg_offset,
+ size_t* relay_msg_len) {
+ // Set the executed_ member to true to allow verification that the
+ // callback has been actually called.
+ executed_ = true;
+ // Use default implementation of the unpack algorithm to parse options.
+ return (LibDHCP::unpackOptions6(buf, option_space, options, relay_msg_offset,
+ relay_msg_len));
+ }
+
+ /// A flag which indicates if callback function has been called.
+ bool executed_;
+};
+
class Pkt6Test : public ::testing::Test {
public:
Pkt6Test() {
}
+
+ /// @brief generates an option with given code (and length) and random content
+ ///
+ /// @param code option code
+ /// @param len data length (data will be randomized)
+ ///
+ /// @return pointer to the new option
+ OptionPtr generateRandomOption(uint16_t code, size_t len = 10) {
+ OptionBuffer data(len);
+ util::fillRandom(data.begin(), data.end());
+ return OptionPtr(new Option(Option::V6, code, data));
+ }
+
+ /// @brief Create a wire representation of the test packet and clone it.
+ ///
+ /// The purpose of this function is to create a packet to be used to
+ /// check that packet parsing works correctly. The unpack() function
+ /// requires that the data_ field of the object holds the data to be
+ /// parsed. This function creates an on-wire representation of the
+ /// packet by calling pack(). But, the pack() function stores the
+ /// on-wire representation into the output buffer (not the data_ field).
+ /// For this reason, it is not enough to return the packet on which
+ /// pack() is called. This function returns a clone of this packet
+ /// which is created using a constructor taking a buffer and buffer
+ /// length as an input. This constructor is normally used to parse
+ /// received packets. It stores the packet in a data_ field and
+ /// therefore unpack() can be called to parse it.
+ Pkt6Ptr packAndClone() {
+ Pkt6Ptr parent(new Pkt6(DHCPV6_SOLICIT, 0x020304));
+
+ OptionPtr opt1(new Option(Option::V6, 1));
+ OptionPtr opt2(new Option(Option::V6, 2));
+ OptionPtr opt3(new Option(Option::V6, 100));
+ // Let's not use zero-length option type 3 as it is IA_NA
+
+ parent->addOption(opt1);
+ parent->addOption(opt2);
+ parent->addOption(opt3);
+
+ EXPECT_EQ(DHCPV6_SOLICIT, parent->getType());
+
+ // Calculated length should be 16
+ EXPECT_EQ(Pkt6::DHCPV6_PKT_HDR_LEN + 3 * Option::OPTION6_HDR_LEN,
+ parent->len());
+
+ EXPECT_NO_THROW(parent->pack());
+
+ EXPECT_EQ(Pkt6::DHCPV6_PKT_HDR_LEN + 3 * Option::OPTION6_HDR_LEN,
+ parent->len());
+
+ // Create second packet,based on assembled data from the first one
+ Pkt6Ptr clone(new Pkt6(static_cast<const uint8_t*>
+ (parent->getBuffer().getData()),
+ parent->getBuffer().getLength()));
+ return (clone);
+
+ }
};
TEST_F(Pkt6Test, constructor) {
uint8_t data[] = { 0, 1, 2, 3, 4, 5 };
scoped_ptr<Pkt6> pkt1(new Pkt6(data, sizeof(data)));
- EXPECT_EQ(6, pkt1->getData().size());
- EXPECT_EQ(0, memcmp( &pkt1->getData()[0], data, sizeof(data)));
+ EXPECT_EQ(6, pkt1->data_.size());
+ EXPECT_EQ(0, memcmp( &pkt1->data_[0], data, sizeof(data)));
}
/// @brief returns captured actual SOLICIT packet
@@ -191,44 +300,62 @@ TEST_F(Pkt6Test, unpack_solicit1) {
}
TEST_F(Pkt6Test, packUnpack) {
- scoped_ptr<Pkt6> parent(new Pkt6(DHCPV6_SOLICIT, 0x020304));
-
- OptionPtr opt1(new Option(Option::V6, 1));
- OptionPtr opt2(new Option(Option::V6, 2));
- OptionPtr opt3(new Option(Option::V6, 100));
- // Let's not use zero-length option type 3 as it is IA_NA
+ // Create an on-wire representation of the test packet and clone it.
+ Pkt6Ptr clone = packAndClone();
- parent->addOption(opt1);
- parent->addOption(opt2);
- parent->addOption(opt3);
-
- EXPECT_EQ(DHCPV6_SOLICIT, parent->getType());
-
- // Calculated length should be 16
- EXPECT_EQ(Pkt6::DHCPV6_PKT_HDR_LEN + 3 * Option::OPTION6_HDR_LEN,
- parent->len());
+ // Now recreate options list
+ EXPECT_TRUE(clone->unpack());
- EXPECT_TRUE(parent->pack());
+ // transid, message-type should be the same as before
+ EXPECT_EQ(0x020304, clone->getTransid());
+ EXPECT_EQ(DHCPV6_SOLICIT, clone->getType());
- EXPECT_EQ(Pkt6::DHCPV6_PKT_HDR_LEN + 3 * Option::OPTION6_HDR_LEN,
- parent->len());
+ EXPECT_TRUE(clone->getOption(1));
+ EXPECT_TRUE(clone->getOption(2));
+ EXPECT_TRUE(clone->getOption(100));
+ EXPECT_FALSE(clone->getOption(4));
+}
- // Create second packet,based on assembled data from the first one
- scoped_ptr<Pkt6> clone(new Pkt6(
- static_cast<const uint8_t*>(parent->getBuffer().getData()),
- parent->getBuffer().getLength()));
+// This test verifies that it is possible to specify custom implementation of
+// the option parsing algorithm by installing a callback function.
+TEST_F(Pkt6Test, packUnpackWithCallback) {
+ // Create an on-wire representation of the test packet and clone it.
+ Pkt6Ptr clone = packAndClone();
+
+ // Install the custom callback function. We expect that this function
+ // will be called to parse options in the packet instead of
+ // LibDHCP::unpackOptions6.
+ CustomUnpackCallback cb;
+ clone->setCallback(boost::bind(&CustomUnpackCallback::execute, &cb,
+ _1, _2, _3, _4, _5));
+ // Make sure that the flag which indicates if the callback function has
+ // been called is not set. Otherwise, our test doesn't make sense.
+ ASSERT_FALSE(cb.executed_);
// Now recreate options list
- EXPECT_TRUE( clone->unpack() );
+ EXPECT_TRUE(clone->unpack());
+
+ // An object which holds a callback should now have a flag set which
+ // indicates that callback has been called.
+ EXPECT_TRUE(cb.executed_);
// transid, message-type should be the same as before
- EXPECT_EQ(parent->getTransid(), parent->getTransid());
+ EXPECT_EQ(0x020304, clone->getTransid());
EXPECT_EQ(DHCPV6_SOLICIT, clone->getType());
EXPECT_TRUE(clone->getOption(1));
EXPECT_TRUE(clone->getOption(2));
EXPECT_TRUE(clone->getOption(100));
EXPECT_FALSE(clone->getOption(4));
+
+ // Reset the indicator to perform another check: uninstall the callback.
+ cb.executed_ = false;
+ // By setting the callback to NULL we effectively uninstall the callback.
+ clone->setCallback(NULL);
+ // Do another unpack.
+ EXPECT_TRUE(clone->unpack());
+ // Callback should not be executed.
+ EXPECT_FALSE(cb.executed_);
}
// This test verifies that options can be added (addOption()), retrieved
@@ -253,12 +380,12 @@ TEST_F(Pkt6Test, addGetDelOptions) {
// Now there are 2 options of type 2
parent->addOption(opt3);
- Option::OptionCollection options = parent->getOptions(2);
+ OptionCollection options = parent->getOptions(2);
EXPECT_EQ(2, options.size()); // there should be 2 instances
// Both options must be of type 2 and there must not be
// any other type returned
- for (Option::OptionCollection::const_iterator x= options.begin();
+ for (OptionCollection::const_iterator x= options.begin();
x != options.end(); ++x) {
EXPECT_EQ(2, x->second->getType());
}
@@ -487,8 +614,7 @@ TEST_F(Pkt6Test, relayPack) {
OptionPtr optRelay1(new Option(Option::V6, 200, relay_data));
- relay1.options_.insert(pair<int, boost::shared_ptr<Option> >(
- optRelay1->getType(), optRelay1));
+ relay1.options_.insert(make_pair(optRelay1->getType(), optRelay1));
OptionPtr opt1(new Option(Option::V6, 100));
OptionPtr opt2(new Option(Option::V6, 101));
@@ -503,7 +629,7 @@ TEST_F(Pkt6Test, relayPack) {
EXPECT_EQ(DHCPV6_ADVERTISE, parent->getType());
- EXPECT_TRUE(parent->pack());
+ EXPECT_NO_THROW(parent->pack());
EXPECT_EQ(Pkt6::DHCPV6_PKT_HDR_LEN + 3 * Option::OPTION6_HDR_LEN // ADVERTISE
+ Pkt6::DHCPV6_RELAY_HDR_LEN // Relay header
@@ -546,4 +672,142 @@ TEST_F(Pkt6Test, relayPack) {
EXPECT_EQ(0, memcmp(relay_opt_data, relay_opt_data, sizeof(relay_opt_data)));
}
+
+// This test verified that options added by relays to the message can be
+// accessed and retrieved properly
+TEST_F(Pkt6Test, getAnyRelayOption) {
+
+ boost::scoped_ptr<Pkt6> msg(new Pkt6(DHCPV6_ADVERTISE, 0x020304));
+ msg->addOption(generateRandomOption(300));
+
+ // generate options for relay1
+ Pkt6::RelayInfo relay1;
+
+ // generate 3 options with code 200,201,202 and random content
+ OptionPtr relay1_opt1(generateRandomOption(200));
+ OptionPtr relay1_opt2(generateRandomOption(201));
+ OptionPtr relay1_opt3(generateRandomOption(202));
+
+ relay1.options_.insert(make_pair(200, relay1_opt1));
+ relay1.options_.insert(make_pair(201, relay1_opt2));
+ relay1.options_.insert(make_pair(202, relay1_opt3));
+ msg->addRelayInfo(relay1);
+
+ // generate options for relay2
+ Pkt6::RelayInfo relay2;
+ OptionPtr relay2_opt1(new Option(Option::V6, 100));
+ OptionPtr relay2_opt2(new Option(Option::V6, 101));
+ OptionPtr relay2_opt3(new Option(Option::V6, 102));
+ OptionPtr relay2_opt4(new Option(Option::V6, 200)); // the same code as relay1_opt3
+ relay2.options_.insert(make_pair(100, relay2_opt1));
+ relay2.options_.insert(make_pair(101, relay2_opt2));
+ relay2.options_.insert(make_pair(102, relay2_opt3));
+ relay2.options_.insert(make_pair(200, relay2_opt4));
+ msg->addRelayInfo(relay2);
+
+ // generate options for relay3
+ Pkt6::RelayInfo relay3;
+ OptionPtr relay3_opt1(generateRandomOption(200, 7));
+ relay3.options_.insert(make_pair(200, relay3_opt1));
+ msg->addRelayInfo(relay3);
+
+ // Ok, so we now have a packet that traversed the following network:
+ // client---relay3---relay2---relay1---server
+
+ // First check that the getAnyRelayOption does not confuse client options
+ // and relay options
+ // 300 is a client option, present in the message itself.
+ OptionPtr opt = msg->getAnyRelayOption(300, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+ EXPECT_FALSE(opt);
+ opt = msg->getAnyRelayOption(300, Pkt6::RELAY_SEARCH_FROM_SERVER);
+ EXPECT_FALSE(opt);
+ opt = msg->getAnyRelayOption(300, Pkt6::RELAY_GET_FIRST);
+ EXPECT_FALSE(opt);
+ opt = msg->getAnyRelayOption(300, Pkt6::RELAY_GET_LAST);
+ EXPECT_FALSE(opt);
+
+ // Option 200 is added in every relay.
+
+ // We want to get that one inserted by relay3 (first match, starting from
+ // closest to the client.
+ opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay3_opt1));
+
+ // We want to ge that one inserted by relay1 (first match, starting from
+ // closest to the server.
+ opt = msg->getAnyRelayOption(200, Pkt6::RELAY_SEARCH_FROM_SERVER);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay1_opt1));
+
+ // We just want option from the first relay (closest to the client)
+ opt = msg->getAnyRelayOption(200, Pkt6::RELAY_GET_FIRST);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay3_opt1));
+
+ // We just want option from the last relay (closest to the server)
+ opt = msg->getAnyRelayOption(200, Pkt6::RELAY_GET_LAST);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay1_opt1));
+
+ // Let's try to ask for something that is inserted by the middle relay
+ // only.
+ opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_FROM_SERVER);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay2_opt1));
+
+ opt = msg->getAnyRelayOption(100, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+ ASSERT_TRUE(opt);
+ EXPECT_TRUE(opt->equal(relay2_opt1));
+
+ opt = msg->getAnyRelayOption(100, Pkt6::RELAY_GET_FIRST);
+ EXPECT_FALSE(opt);
+
+ opt = msg->getAnyRelayOption(100, Pkt6::RELAY_GET_LAST);
+ EXPECT_FALSE(opt);
+
+ // Finally, try to get an option that does not exist
+ opt = msg->getAnyRelayOption(500, Pkt6::RELAY_GET_FIRST);
+ EXPECT_FALSE(opt);
+
+ opt = msg->getAnyRelayOption(500, Pkt6::RELAY_GET_LAST);
+ EXPECT_FALSE(opt);
+
+ opt = msg->getAnyRelayOption(500, Pkt6::RELAY_SEARCH_FROM_SERVER);
+ EXPECT_FALSE(opt);
+
+ opt = msg->getAnyRelayOption(500, Pkt6::RELAY_SEARCH_FROM_CLIENT);
+ EXPECT_FALSE(opt);
+}
+
+// Tests whether a packet can be assigned to a class and later
+// checked if it belongs to a given class
+TEST_F(Pkt6Test, clientClasses) {
+ Pkt6 pkt(DHCPV6_ADVERTISE, 1234);
+
+ // Default values (do not belong to any class)
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+ EXPECT_TRUE(pkt.classes_.empty());
+
+ // Add to the first class
+ pkt.addClass(DOCSIS3_CLASS_EROUTER);
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_FALSE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+ ASSERT_FALSE(pkt.classes_.empty());
+
+ // Add to a second class
+ pkt.addClass(DOCSIS3_CLASS_MODEM);
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_EROUTER));
+ EXPECT_TRUE(pkt.inClass(DOCSIS3_CLASS_MODEM));
+
+ // Check that it's ok to add to the same class repeatedly
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+ EXPECT_NO_THROW(pkt.addClass("foo"));
+
+ // Check that the packet belongs to 'foo'
+ EXPECT_TRUE(pkt.inClass("foo"));
+}
+
}
diff --git a/src/lib/dhcp/tests/pkt_filter6_test_utils.cc b/src/lib/dhcp/tests/pkt_filter6_test_utils.cc
new file mode 100644
index 0000000..339a6e6
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter6_test_utils.cc
@@ -0,0 +1,212 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/io_address.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/tests/pkt_filter6_test_utils.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+PktFilter6Test::PktFilter6Test(const uint16_t port)
+ : port_(port),
+ sock_info_(isc::asiolink::IOAddress("::1"), port, -1, -1),
+ send_msg_sock_(-1) {
+ // Initialize ifname_ and ifindex_.
+ loInit();
+ // Initialize test_message_.
+ initTestMessage();
+}
+
+PktFilter6Test::~PktFilter6Test() {
+ // Cleanup after each test. This guarantees
+ // that the sockets do not hang after a test.
+ if (sock_info_.sockfd_ >= 0) {
+ close(sock_info_.sockfd_);
+ }
+ if (sock_info_.fallbackfd_ >=0) {
+ close(sock_info_.fallbackfd_);
+ }
+ if (send_msg_sock_ >= 0) {
+ close(send_msg_sock_);
+ }
+}
+
+void
+PktFilter6Test::initTestMessage() {
+ // Let's create a DHCPv6 message instance.
+ test_message_.reset(new Pkt6(DHCPV6_ADVERTISE, 123));
+
+ // Set required fields.
+ test_message_->setLocalAddr(IOAddress("::1"));
+ test_message_->setRemoteAddr(IOAddress("::1"));
+ test_message_->setRemotePort(port_);
+ test_message_->setLocalPort(port_ + 1);
+ test_message_->setIndex(ifindex_);
+ test_message_->setIface(ifname_);
+
+ try {
+ test_message_->pack();
+ } catch (const isc::Exception& ex) {
+ ADD_FAILURE() << "failed to create test message for PktFilter6Test";
+ }
+}
+
+void
+PktFilter6Test::loInit() {
+ if (if_nametoindex("lo") > 0) {
+ ifname_ = "lo";
+ ifindex_ = if_nametoindex("lo");
+
+ } else if (if_nametoindex("lo0") > 0) {
+ ifname_ = "lo0";
+ ifindex_ = if_nametoindex("lo0");
+
+ } else {
+ std::cout << "Failed to detect loopback interface. Neither "
+ << "lo nor lo0 worked. Giving up." << std::endl;
+ FAIL();
+
+ }
+}
+
+void
+PktFilter6Test::sendMessage() {
+ // DHCPv6 message will be sent over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("::1");
+
+ // Initialize the source address and port.
+ struct sockaddr_in6 addr6;
+ memset(&addr6, 0, sizeof(addr6));
+ addr6.sin6_family = AF_INET6;
+ addr6.sin6_port = htons(port_);
+ memcpy(&addr6.sin6_addr, &addr.toBytes()[0], sizeof(addr6.sin6_addr));
+
+ // Open socket and bind to source address and port.
+ send_msg_sock_ = socket(AF_INET6, SOCK_DGRAM, 0);
+ ASSERT_GE(send_msg_sock_, 0);
+
+ ASSERT_GE(bind(send_msg_sock_, (struct sockaddr *)&addr6,
+ sizeof(addr6)), 0);
+
+ // Set the destination address and port.
+ struct sockaddr_in6 dest_addr6;
+ memset(&dest_addr6, 0, sizeof(sockaddr_in6));
+ dest_addr6.sin6_family = AF_INET6;
+ dest_addr6.sin6_port = htons(port_ + 1);
+ memcpy(&dest_addr6.sin6_addr, &addr.toBytes()[0], 16);
+
+ // Initialize the message header structure, required by sendmsg.
+ struct msghdr m;
+ memset(&m, 0, sizeof(m));
+ m.msg_name = &dest_addr6;
+ m.msg_namelen = sizeof(dest_addr6);
+ // The iovec structure holds the packet data.
+ struct iovec v;
+ memset(&v, 0, sizeof(v));
+ v.iov_base = const_cast<void *>(test_message_->getBuffer().getData());
+ v.iov_len = test_message_->getBuffer().getLength();
+ // Assign the iovec to msghdr structure.
+ m.msg_iov = &v;
+ m.msg_iovlen = 1;
+ // We should be able to send the whole message. The sendmsg function should
+ // return the number of bytes sent, which is equal to the size of our
+ // message.
+ ASSERT_EQ(sendmsg(send_msg_sock_, &m, 0),
+ test_message_->getBuffer().getLength());
+ close(send_msg_sock_);
+ send_msg_sock_ = -1;
+
+}
+
+void
+PktFilter6Test::testDgramSocket(const int sock) const {
+ // Check that socket has been opened.
+ ASSERT_GE(sock, 0);
+
+ // Verify that the socket belongs to AF_INET family.
+ sockaddr_in6 sock_address;
+ socklen_t sock_address_len = sizeof(sock_address);
+ ASSERT_EQ(0, getsockname(sock,
+ reinterpret_cast<sockaddr*>(&sock_address),
+ &sock_address_len));
+ EXPECT_EQ(AF_INET6, sock_address.sin6_family);
+
+ // Verify that the socket is bound the appropriate address.
+ char straddr[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &sock_address.sin6_addr, straddr, sizeof(straddr));
+ std::string bind_addr(straddr);
+ EXPECT_EQ("::1", bind_addr);
+
+ // Verify that the socket is bound to appropriate port.
+ EXPECT_EQ(port_, ntohs(sock_address.sin6_port));
+
+ // Verify that the socket has SOCK_DGRAM type.
+ int sock_type;
+ socklen_t sock_type_len = sizeof(sock_type);
+ ASSERT_EQ(0, getsockopt(sock, SOL_SOCKET, SO_TYPE,
+ &sock_type, &sock_type_len));
+ EXPECT_EQ(SOCK_DGRAM, sock_type);
+}
+
+void
+PktFilter6Test::testRcvdMessage(const Pkt6Ptr& rcvd_msg) const {
+ // Currently, we don't send any payload in the message.
+ // Let's just check that the transaction id matches so as we
+ // are sure that we received the message that we expected.
+ EXPECT_EQ(test_message_->getTransid(), rcvd_msg->getTransid());
+}
+
+PktFilter6Stub::PktFilter6Stub()
+ : open_socket_count_ (0) {
+}
+
+SocketInfo
+PktFilter6Stub::openSocket(const Iface& iface, const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool) {
+ // Check if there is any other socket bound to the specified address
+ // and port on this interface.
+ const Iface::SocketCollection& sockets = iface.getSockets();
+ for (Iface::SocketCollection::const_iterator socket = sockets.begin();
+ socket != sockets.end(); ++socket) {
+ if ((socket->addr_ == addr) && (socket->port_ == port)) {
+ isc_throw(SocketConfigError, "test socket bind error");
+ }
+ }
+ ++open_socket_count_;
+ return (SocketInfo(addr, port, 0));
+}
+
+Pkt6Ptr
+PktFilter6Stub::receive(const SocketInfo&) {
+ return Pkt6Ptr();
+}
+
+int
+PktFilter6Stub::send(const Iface&, uint16_t, const Pkt6Ptr&) {
+ return (0);
+}
+
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/tests/pkt_filter6_test_utils.h b/src/lib/dhcp/tests/pkt_filter6_test_utils.h
new file mode 100644
index 0000000..bffd097
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter6_test_utils.h
@@ -0,0 +1,160 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PKT_FILTER6_TEST_UTILS_H
+#define PKT_FILTER6_TEST_UTILS_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter.h>
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Test fixture class for testing classes derived from PktFilter6 class.
+///
+/// This class implements a simple algorithm checking presence of the loopback
+/// interface and initializing its index. It assumes that the loopback interface
+/// name is one of 'lo' or 'lo0'. If none of those interfaces is found, the
+/// constructor will report a failure.
+///
+/// @todo The interface detection algorithm should be more generic. This will
+/// be possible once the cross-OS interface detection is implemented.
+class PktFilter6Test : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ ///
+ /// This constructor initializes sock_info_ structure to a default value.
+ /// The socket descriptors should be set to a negative value to indicate
+ /// that no socket has been opened. Specific tests will reinitialize this
+ /// structure with the values of the open sockets. For non-negative socket
+ /// descriptors, the class destructor will close associated sockets.
+ PktFilter6Test(const uint16_t port);
+
+ /// @brief Destructor
+ ///
+ /// Closes open sockets (if any).
+ virtual ~PktFilter6Test();
+
+ /// @brief Initializes DHCPv6 message used by tests.
+ void initTestMessage();
+
+ /// @brief Detect loopback interface.
+ ///
+ /// @todo this function will be removed once cross-OS interface
+ /// detection is implemented
+ void loInit();
+
+ /// @brief Sends a single DHCPv6 message to the loopback address.
+ ///
+ /// This function opens a datagram socket and binds it to the local loopback
+ /// address and client port. The client's port is assumed to be port_ + 1.
+ /// The send_msg_sock_ member holds the socket descriptor so as the socket
+ /// is closed automatically in the destructor. If the function succeeds to
+ /// send a DHCPv6 message, the socket is closed so as the function can be
+ /// called again within the same test.
+ void sendMessage();
+
+ /// @brief Test that the datagram socket is opened correctly.
+ ///
+ /// This function is used by multiple tests.
+ ///
+ /// @param sock A descriptor of the open socket.
+ void testDgramSocket(const int sock) const;
+
+ /// @brief Checks if the received message matches the test_message_.
+ ///
+ /// @param rcvd_msg An instance of the message to be tested.
+ void testRcvdMessage(const Pkt6Ptr& rcvd_msg) const;
+
+ std::string ifname_; ///< Loopback interface name.
+ uint16_t ifindex_; ///< Loopback interface index.
+ uint16_t port_; ///< A port number used for the test.
+ isc::dhcp::SocketInfo sock_info_; ///< A structure holding socket info.
+ int send_msg_sock_; ///< Holds a descriptor of the socket used by
+ ///< sendMessage function.
+ Pkt6Ptr test_message_; ///< A DHCPv6 message used by tests.
+
+};
+
+/// @brief A stub implementation of the PktFilter6 class.
+///
+/// This class implements abstract methods of the @c isc::dhcp::PktFilter class.
+/// The methods of this class mimic operations on sockets, but they neither
+/// open actual sockets, nor perform any send nor receive operations on them.
+class PktFilter6Stub : public PktFilter6 {
+public:
+
+ /// @brief Constructor
+ PktFilter6Stub();
+
+ /// @brief Simulate opening of a socket.
+ ///
+ /// This function simulates opening a socket. In reality, it doesn't open a
+ /// socket but the socket descriptor returned in the SocketInfo structure is
+ /// always set to 0. On each call to this function, the counter of
+ /// invocations is increased by one. This is useful to check if packet
+ /// filter object has been correctly installed and is used by @c IfaceMgr.
+ /// As in the case of opening a real socket, this function will check
+ /// if there is another fake socket "bound" to the same address and port.
+ /// If there is, it will throw an exception. This allows to simulate the
+ /// conditions when one of the sockets can't be open because there is
+ /// a socket already open and test how IfaceMgr will handle it.
+ ///
+ /// @param iface Interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number.
+ /// @param join_multicast A boolean parameter which indicates whether
+ /// socket should join All_DHCP_Relay_Agents_and_servers multicast
+ /// group.
+ ///
+ /// @return A structure describing a primary and fallback socket.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port,
+ const bool join_multicast);
+
+ /// @brief Simulate reception of the DHCPv6 message.
+ ///
+ /// @param socket_info A structure holding socket information.
+ ///
+ /// @return Always a NULL object.
+ virtual Pkt6Ptr receive(const SocketInfo& socket_info);
+
+ /// @brief Simulate sending a DHCPv6 message.
+ ///
+ /// This function does nothing.
+ ///
+ /// @param iface Interface to be used to send packet.
+ /// @param sockfd A socket descriptor
+ /// @param pkt A packet to be sent.
+ ///
+ /// @note All parameters are ignored.
+ ///
+ /// @return 0.
+ virtual int send(const Iface& iface, uint16_t sockfd, const Pkt6Ptr& pkt);
+
+ /// Holds the number of invocations to PktFilter6Stub::openSocket.
+ int open_socket_count_;
+
+};
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // PKT_FILTER6_TEST_UTILS_H
diff --git a/src/lib/dhcp/tests/pkt_filter_inet6_unittest.cc b/src/lib/dhcp/tests/pkt_filter_inet6_unittest.cc
new file mode 100644
index 0000000..e82cffe
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_inet6_unittest.cc
@@ -0,0 +1,140 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt6.h>
+#include <dhcp/pkt_filter_inet6.h>
+#include <dhcp/tests/pkt_filter6_test_utils.h>
+
+#include <gtest/gtest.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+/// Port number used by tests.
+const uint16_t PORT = 10546;
+/// Size of the buffer holding received packets.
+const size_t RECV_BUF_SIZE = 2048;
+
+// Test fixture class inherits from the class common for all packet
+// filter tests.
+class PktFilterInet6Test : public isc::dhcp::test::PktFilter6Test {
+public:
+ PktFilterInet6Test() : PktFilter6Test(PORT) {
+ }
+};
+
+// This test verifies that the INET6 datagram socket is correctly opened and
+// bound to the appropriate address and port.
+TEST_F(PktFilterInet6Test, openSocket) {
+ // Create object representing loopback interface.
+ Iface iface(ifname_, ifindex_);
+ // Set loopback address.
+ IOAddress addr("::1");
+
+ // Try to open socket.
+ PktFilterInet6 pkt_filter;
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, true);
+ // For the packet filter in use, the fallback socket shouldn't be opened.
+ // Fallback is typically opened for raw IPv4 sockets.
+ EXPECT_LT(sock_info_.fallbackfd_, 0);
+
+ // Test the primary socket.
+ testDgramSocket(sock_info_.sockfd_);
+}
+
+// This test verifies that the packet is correctly sent over the INET6
+// datagram socket.
+TEST_F(PktFilterInet6Test, send) {
+ // Packet will be sent over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("::1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterInet6 pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, true);
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send the packet over the socket.
+ ASSERT_NO_THROW(pkt_filter.send(iface, sock_info_.sockfd_, test_message_));
+
+ // Read the data from socket.
+ fd_set readfds;
+ FD_ZERO(&readfds);
+ FD_SET(sock_info_.sockfd_, &readfds);
+
+ struct timeval timeout;
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+ int result = select(sock_info_.sockfd_ + 1, &readfds, NULL, NULL, &timeout);
+ // We should receive some data from loopback interface.
+ ASSERT_GT(result, 0);
+
+ // Get the actual data.
+ uint8_t rcv_buf[RECV_BUF_SIZE];
+ result = recv(sock_info_.sockfd_, rcv_buf, RECV_BUF_SIZE, 0);
+ ASSERT_GT(result, 0);
+
+ // Create the DHCPv6 packet from the received data.
+ Pkt6Ptr rcvd_pkt(new Pkt6(rcv_buf, result));
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+
+}
+
+// This test verifies that the DHCPv6 packet is correctly received via
+// INET6 datagram socket and that it matches sent packet.
+TEST_F(PktFilterInet6Test, receive) {
+
+ // Packet will be received over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("::1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterInet6 pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT + 1, true);
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send a DHCPv6 message to the local loopback address and server's port.
+ // ASSERT_NO_THROW(pkt_filter.send(iface, sock_info_.sockfd_, test_message_));
+ sendMessage();
+
+ // Receive the packet.
+ Pkt6Ptr rcvd_pkt = pkt_filter.receive(sock_info_);
+ // Check that the packet has been correctly received.
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+ }
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/pkt_filter_inet_unittest.cc b/src/lib/dhcp/tests/pkt_filter_inet_unittest.cc
new file mode 100644
index 0000000..c21a996
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_inet_unittest.cc
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt_filter_inet.h>
+#include <dhcp/tests/pkt_filter_test_utils.h>
+
+#include <gtest/gtest.h>
+
+#include <sys/socket.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+
+namespace {
+
+/// Port number used by tests.
+const uint16_t PORT = 10067;
+/// Size of the buffer holding received packets.
+const size_t RECV_BUF_SIZE = 2048;
+
+// Test fixture class inherits from the class common for all packet
+// filter tests.
+class PktFilterInetTest : public isc::dhcp::test::PktFilterTest {
+public:
+ PktFilterInetTest() : PktFilterTest(PORT) {
+ }
+};
+
+// This test verifies that the PktFilterInet class reports its lack
+// of capability to send packets to the host having no IP address
+// assigned.
+TEST_F(PktFilterInetTest, isDirectResponseSupported) {
+ // Create object under test.
+ PktFilterInet pkt_filter;
+ // This Packet Filter class does not support direct responses
+ // under any conditions.
+ EXPECT_FALSE(pkt_filter.isDirectResponseSupported());
+}
+
+// This test verifies that the INET datagram socket is correctly opened and
+// bound to the appropriate address and port.
+TEST_F(PktFilterInetTest, openSocket) {
+ // Create object representing loopback interface.
+ Iface iface(ifname_, ifindex_);
+ // Set loopback address.
+ IOAddress addr("127.0.0.1");
+
+ // Try to open socket.
+ PktFilterInet pkt_filter;
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT,
+ false, false);
+ // For the packet filter in use, the fallback socket shouldn't be opened.
+ // Fallback is typically opened for raw sockets.
+ EXPECT_LT(sock_info_.fallbackfd_, 0);
+
+ // Test the primary socket.
+ testDgramSocket(sock_info_.sockfd_);
+}
+
+// This test verifies that the packet is correctly sent over the INET
+// datagram socket.
+TEST_F(PktFilterInetTest, send) {
+ // Packet will be sent over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("127.0.0.1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterInet pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, false, false);
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send the packet over the socket.
+ ASSERT_NO_THROW(pkt_filter.send(iface, sock_info_.sockfd_, test_message_));
+
+ // Read the data from socket.
+ fd_set readfds;
+ FD_ZERO(&readfds);
+ FD_SET(sock_info_.sockfd_, &readfds);
+
+ struct timeval timeout;
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+ int result = select(sock_info_.sockfd_ + 1, &readfds, NULL, NULL, &timeout);
+ // We should receive some data from loopback interface.
+ ASSERT_GT(result, 0);
+
+ // Get the actual data.
+ uint8_t rcv_buf[RECV_BUF_SIZE];
+ result = recv(sock_info_.sockfd_, rcv_buf, RECV_BUF_SIZE, 0);
+ ASSERT_GT(result, 0);
+
+ // Create the DHCPv4 packet from the received data.
+ Pkt4Ptr rcvd_pkt(new Pkt4(rcv_buf, result));
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+
+}
+
+// This test verifies that the DHCPv4 packet is correctly received via
+// INET datagram socket and that it matches sent packet.
+TEST_F(PktFilterInetTest, receive) {
+
+ // Packet will be received over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("127.0.0.1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterInet pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, false, false);
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send a DHCPv4 message to the local loopback address and server's port.
+ sendMessage();
+
+ // Receive the packet.
+ Pkt4Ptr rcvd_pkt = pkt_filter.receive(iface, sock_info_);
+ // Check that the packet has been correctly received.
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/pkt_filter_lpf_unittest.cc b/src/lib/dhcp/tests/pkt_filter_lpf_unittest.cc
new file mode 100644
index 0000000..b0e5c58
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_lpf_unittest.cc
@@ -0,0 +1,192 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt_filter_lpf.h>
+#include <dhcp/protocol_util.h>
+#include <dhcp/tests/pkt_filter_test_utils.h>
+#include <util/buffer.h>
+
+#include <gtest/gtest.h>
+
+#include <linux/if_packet.h>
+#include <sys/socket.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+/// Port number used by tests.
+const uint16_t PORT = 10067;
+/// Size of the buffer holding received packets.
+const size_t RECV_BUF_SIZE = 2048;
+
+// Test fixture class inherits from the class common for all packet
+// filter tests.
+class PktFilterLPFTest : public isc::dhcp::test::PktFilterTest {
+public:
+ PktFilterLPFTest() : PktFilterTest(PORT) {
+ }
+};
+
+// This test verifies that the PktFilterLPF class reports its capability
+// to send packets to the host having no IP address assigned.
+TEST_F(PktFilterLPFTest, isDirectResponseSupported) {
+ // Create object under test.
+ PktFilterLPF pkt_filter;
+ // Must support direct responses.
+ EXPECT_TRUE(pkt_filter.isDirectResponseSupported());
+}
+
+// All tests below require root privileges to execute successfully. If
+// they are run as non-root user they will fail due to insufficient privileges
+// to open raw network sockets. Therefore, they should remain disabled by default
+// and "DISABLED_" tags should not be removed. If one is willing to run these
+// tests please run "make check" as root and enable execution of disabled tests
+// by setting GTEST_ALSO_RUN_DISABLED_TESTS to a value other than 0. In order
+// to run tests from this particular file, set the GTEST_FILTER environmental
+// variable to "PktFilterLPFTest.*" apart from GTEST_ALSO_RUN_DISABLED_TESTS
+// setting.
+
+// This test verifies that the raw AF_PACKET family socket can
+// be opened and bound to the specific interface.
+TEST_F(PktFilterLPFTest, DISABLED_openSocket) {
+ // Create object representing loopback interface.
+ Iface iface(ifname_, ifindex_);
+ // Set loopback address.
+ IOAddress addr("127.0.0.1");
+
+ // Try to open socket.
+ PktFilterLPF pkt_filter;
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, false, false);
+
+ // Check that the primary socket has been opened.
+ ASSERT_GE(sock_info_.sockfd_, 0);
+ // Check that the fallback socket has been opened too.
+ ASSERT_GE(sock_info_.fallbackfd_, 0);
+
+ // Verify that the socket belongs to AF_PACKET family.
+ sockaddr_ll sock_address;
+ socklen_t sock_address_len = sizeof(sock_address);
+ ASSERT_EQ(0, getsockname(sock_info_.sockfd_,
+ reinterpret_cast<sockaddr*>(&sock_address),
+ &sock_address_len));
+ EXPECT_EQ(AF_PACKET, sock_address.sll_family);
+
+ // Verify that the socket is bound to appropriate interface.
+ EXPECT_EQ(ifindex_, sock_address.sll_ifindex);
+
+ // Verify that the socket has SOCK_RAW type.
+ int sock_type;
+ socklen_t sock_type_len = sizeof(sock_type);
+ ASSERT_EQ(0, getsockopt(sock_info_.sockfd_, SOL_SOCKET, SO_TYPE,
+ &sock_type, &sock_type_len));
+ EXPECT_EQ(SOCK_RAW, sock_type);
+}
+
+// This test verifies correctness of sending DHCP packet through the raw
+// socket, whereby all IP stack headers are hand-crafted.
+TEST_F(PktFilterLPFTest, DISABLED_send) {
+ // Packet will be sent over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("127.0.0.1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterLPF pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, false, false);
+
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send the packet over the socket.
+ ASSERT_NO_THROW(pkt_filter.send(iface, sock_info_.sockfd_, test_message_));
+
+ // Read the data from socket.
+ fd_set readfds;
+ FD_ZERO(&readfds);
+ FD_SET(sock_info_.sockfd_, &readfds);
+
+ struct timeval timeout;
+ timeout.tv_sec = 5;
+ timeout.tv_usec = 0;
+ int result = select(sock_info_.sockfd_ + 1, &readfds, NULL, NULL, &timeout);
+ // We should receive some data from loopback interface.
+ ASSERT_GT(result, 0);
+
+ // Get the actual data.
+ uint8_t rcv_buf[RECV_BUF_SIZE];
+ result = recv(sock_info_.sockfd_, rcv_buf, RECV_BUF_SIZE, 0);
+ ASSERT_GT(result, 0);
+
+ Pkt4Ptr dummy_pkt = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 0));
+
+ InputBuffer buf(rcv_buf, result);
+
+ // Decode ethernet, ip and udp headers.
+ decodeEthernetHeader(buf, dummy_pkt);
+ decodeIpUdpHeader(buf, dummy_pkt);
+
+ // Create the DHCPv4 packet from the received data.
+ std::vector<uint8_t> dhcp_buf;
+ buf.readVector(dhcp_buf, buf.getLength() - buf.getPosition());
+ Pkt4Ptr rcvd_pkt(new Pkt4(&dhcp_buf[0], dhcp_buf.size()));
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+}
+
+// This test verifies correctness of reception of the DHCP packet over
+// raw socket, whereby all IP stack headers are hand-crafted.
+TEST_F(PktFilterLPFTest, DISABLED_receive) {
+
+ // Packet will be received over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("127.0.0.1");
+
+ // Create an instance of the class which we are testing.
+ PktFilterLPF pkt_filter;
+ // Open socket. We don't check that the socket has appropriate
+ // options and family set because we have checked that in the
+ // openSocket test already.
+ sock_info_ = pkt_filter.openSocket(iface, addr, PORT, false, false);
+ ASSERT_GE(sock_info_.sockfd_, 0);
+
+ // Send DHCPv4 message to the local loopback address and server's port.
+ sendMessage();
+
+ // Receive the packet using LPF packet filter.
+ Pkt4Ptr rcvd_pkt = pkt_filter.receive(iface, sock_info_);
+ // Check that the packet has been correctly received.
+ ASSERT_TRUE(rcvd_pkt);
+
+ // Parse the packet.
+ ASSERT_NO_THROW(rcvd_pkt->unpack());
+
+ // Check if the received message is correct.
+ testRcvdMessage(rcvd_pkt);
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/pkt_filter_test_utils.cc b/src/lib/dhcp/tests/pkt_filter_test_utils.cc
new file mode 100644
index 0000000..eb91b9f
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_test_utils.cc
@@ -0,0 +1,194 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/io_address.h>
+#include <dhcp/pkt4.h>
+#include <dhcp/tests/pkt_filter_test_utils.h>
+
+using namespace isc::asiolink;
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+PktFilterTest::PktFilterTest(const uint16_t port)
+ : port_(port),
+ sock_info_(isc::asiolink::IOAddress("127.0.0.1"), port, -1, -1),
+ send_msg_sock_(-1) {
+ // Initialize ifname_ and ifindex_.
+ loInit();
+ // Initialize test_message_.
+ initTestMessage();
+}
+
+PktFilterTest::~PktFilterTest() {
+ // Cleanup after each test. This guarantees
+ // that the sockets do not hang after a test.
+ if (sock_info_.sockfd_ >= 0) {
+ close(sock_info_.sockfd_);
+ }
+ if (sock_info_.fallbackfd_ >=0) {
+ close(sock_info_.fallbackfd_);
+ }
+ if (send_msg_sock_ >= 0) {
+ close(send_msg_sock_);
+ }
+}
+
+void
+PktFilterTest::initTestMessage() {
+ // Let's create a DHCPv4 message instance.
+ test_message_.reset(new Pkt4(DHCPOFFER, 0));
+
+ // Set required fields.
+ test_message_->setLocalAddr(IOAddress("127.0.0.1"));
+ test_message_->setRemoteAddr(IOAddress("127.0.0.1"));
+ test_message_->setRemotePort(port_);
+ test_message_->setLocalPort(port_ + 1);
+ test_message_->setIndex(ifindex_);
+ test_message_->setIface(ifname_);
+ test_message_->setHops(6);
+ test_message_->setSecs(42);
+ test_message_->setCiaddr(IOAddress("192.0.2.1"));
+ test_message_->setSiaddr(IOAddress("192.0.2.2"));
+ test_message_->setYiaddr(IOAddress("192.0.2.3"));
+ test_message_->setGiaddr(IOAddress("192.0.2.4"));
+
+ try {
+ test_message_->pack();
+ } catch (const isc::Exception& ex) {
+ ADD_FAILURE() << "failed to create test message for PktFilterTest";
+ }
+}
+
+void
+PktFilterTest::loInit() {
+ if (if_nametoindex("lo") > 0) {
+ ifname_ = "lo";
+ ifindex_ = if_nametoindex("lo");
+
+ } else if (if_nametoindex("lo0") > 0) {
+ ifname_ = "lo0";
+ ifindex_ = if_nametoindex("lo0");
+
+ } else {
+ std::cout << "Failed to detect loopback interface. Neither "
+ << "lo nor lo0 worked. Giving up." << std::endl;
+ FAIL();
+
+ }
+}
+
+void
+PktFilterTest::sendMessage() {
+
+ // Packet will be sent over loopback interface.
+ Iface iface(ifname_, ifindex_);
+ IOAddress addr("127.0.0.1");
+
+ struct sockaddr_in addr4;
+ memset(&addr4, 0, sizeof(sockaddr));
+ addr4.sin_family = AF_INET;
+ addr4.sin_port = htons(port_ + 1);
+
+ send_msg_sock_ = socket(AF_INET, SOCK_DGRAM, 0);
+ ASSERT_GE(send_msg_sock_, 0);
+
+ ASSERT_GE(bind(send_msg_sock_, (struct sockaddr *)&addr4,
+ sizeof(addr4)), 0);
+
+ struct sockaddr_in dest_addr4;
+ memset(&dest_addr4, 0, sizeof(sockaddr));
+ dest_addr4.sin_family = AF_INET;
+ dest_addr4.sin_port = htons(port_);
+ ASSERT_EQ(sendto(send_msg_sock_, test_message_->getBuffer().getData(),
+ test_message_->getBuffer().getLength(), 0,
+ reinterpret_cast<struct sockaddr*>(&dest_addr4),
+ sizeof(sockaddr)), test_message_->getBuffer().getLength());
+ close(send_msg_sock_);
+ send_msg_sock_ = -1;
+
+}
+
+void
+PktFilterTest::testDgramSocket(const int sock) const {
+ // Check that socket has been opened.
+ ASSERT_GE(sock, 0);
+
+ // Verify that the socket belongs to AF_INET family.
+ sockaddr_in sock_address;
+ socklen_t sock_address_len = sizeof(sock_address);
+ ASSERT_EQ(0, getsockname(sock,
+ reinterpret_cast<sockaddr*>(&sock_address),
+ &sock_address_len));
+ EXPECT_EQ(AF_INET, sock_address.sin_family);
+
+ // Verify that the socket is bound the appropriate address.
+ const std::string bind_addr(inet_ntoa(sock_address.sin_addr));
+ EXPECT_EQ("127.0.0.1", bind_addr);
+
+ // Verify that the socket is bound to appropriate port.
+ EXPECT_EQ(port_, ntohs(sock_address.sin_port));
+
+ // Verify that the socket has SOCK_DGRAM type.
+ int sock_type;
+ socklen_t sock_type_len = sizeof(sock_type);
+ ASSERT_EQ(0, getsockopt(sock, SOL_SOCKET, SO_TYPE,
+ &sock_type, &sock_type_len));
+ EXPECT_EQ(SOCK_DGRAM, sock_type);
+}
+
+void
+PktFilterTest::testRcvdMessage(const Pkt4Ptr& rcvd_msg) const {
+ EXPECT_EQ(test_message_->getHops(), rcvd_msg->getHops());
+ EXPECT_EQ(test_message_->getOp(), rcvd_msg->getOp());
+ EXPECT_EQ(test_message_->getSecs(), rcvd_msg->getSecs());
+ EXPECT_EQ(test_message_->getFlags(), rcvd_msg->getFlags());
+ EXPECT_EQ(test_message_->getCiaddr(), rcvd_msg->getCiaddr());
+ EXPECT_EQ(test_message_->getSiaddr(), rcvd_msg->getSiaddr());
+ EXPECT_EQ(test_message_->getYiaddr(), rcvd_msg->getYiaddr());
+ EXPECT_EQ(test_message_->getGiaddr(), rcvd_msg->getGiaddr());
+ EXPECT_EQ(test_message_->getTransid(), rcvd_msg->getTransid());
+ EXPECT_TRUE(test_message_->getSname() == rcvd_msg->getSname());
+ EXPECT_TRUE(test_message_->getFile() == rcvd_msg->getFile());
+ EXPECT_EQ(test_message_->getHtype(), rcvd_msg->getHtype());
+ EXPECT_EQ(test_message_->getHlen(), rcvd_msg->getHlen());
+}
+
+bool
+PktFilterStub::isDirectResponseSupported() const {
+ return (true);
+}
+
+SocketInfo
+PktFilterStub::openSocket(const Iface&,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool, const bool) {
+ return (SocketInfo(addr, port, 0));
+}
+
+Pkt4Ptr
+PktFilterStub::receive(const Iface&, const SocketInfo&) {
+ return Pkt4Ptr();
+}
+
+int
+PktFilterStub::send(const Iface&, uint16_t, const Pkt4Ptr&) {
+ return (0);
+}
+
+
+} // end of isc::dhcp::test namespace
+} // end of isc::dhcp namespace
+} // end of isc namespace
diff --git a/src/lib/dhcp/tests/pkt_filter_test_utils.h b/src/lib/dhcp/tests/pkt_filter_test_utils.h
new file mode 100644
index 0000000..b2320cf
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_test_utils.h
@@ -0,0 +1,167 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PKT_FILTER_TEST_UTILS_H
+#define PKT_FILTER_TEST_UTILS_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/pkt_filter.h>
+#include <gtest/gtest.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @brief Test fixture class for testing classes derived from PktFilter class.
+///
+/// This class implements a simple algorithm checking presence of the loopback
+/// interface and initializing its index. It assumes that the loopback interface
+/// name is one of 'lo' or 'lo0'. If none of those interfaces is found, the
+/// constructor will report a failure.
+///
+/// @todo The interface detection algorithm should be more generic. This will
+/// be possible once the cross-OS interface detection is implemented.
+class PktFilterTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ ///
+ /// This constructor initializes sock_info_ structure to a default value.
+ /// The socket descriptors should be set to a negative value to indicate
+ /// that no socket has been opened. Specific tests will reinitialize this
+ /// structure with the values of the open sockets. For non-negative socket
+ /// descriptors, the class destructor will close associated sockets.
+ PktFilterTest(const uint16_t port);
+
+ /// @brief Destructor
+ ///
+ /// Closes open sockets (if any).
+ virtual ~PktFilterTest();
+
+ /// @brief Initializes DHCPv4 message used by tests.
+ void initTestMessage();
+
+ /// @brief Detect loopback interface.
+ ///
+ /// @todo this function will be removed once cross-OS interface
+ /// detection is implemented
+ void loInit();
+
+ /// @brief Sends a single DHCPv4 message to the loopback address.
+ ///
+ /// This function opens a datagram socket and binds it to the local loopback
+ /// address and client port. The client's port is assumed to be port_ + 1.
+ /// The send_msg_sock_ member holds the socket descriptor so as the socket
+ /// is closed automatically in the destructor. If the function succeeds to
+ /// send a DHCPv4 message, the socket is closed so as the function can be
+ /// called again within the same test.
+ void sendMessage();
+
+ /// @brief Test that the datagram socket is opened correctly.
+ ///
+ /// This function is used by multiple tests.
+ ///
+ /// @param sock A descriptor of the open socket.
+ void testDgramSocket(const int sock) const;
+
+ /// @brief Checks if the received message matches the test_message_.
+ ///
+ /// @param rcvd_msg An instance of the message to be tested.
+ void testRcvdMessage(const Pkt4Ptr& rcvd_msg) const;
+
+ std::string ifname_; ///< Loopback interface name
+ uint16_t ifindex_; ///< Loopback interface index.
+ uint16_t port_; ///< A port number used for the test.
+ isc::dhcp::SocketInfo sock_info_; ///< A structure holding socket info.
+ int send_msg_sock_; ///< Holds a descriptor of the socket used by
+ ///< sendMessage function.
+ Pkt4Ptr test_message_; ///< A DHCPv4 message used by tests.
+
+};
+
+/// @brief A stub implementation of the PktFilter class.
+///
+/// This class implements abstract methods of the @c isc::dhcp::PktFilter
+/// class. It is used by unit tests, which test protected methods of the
+/// @c isc::dhcp::test::PktFilter class. The implemented abstract methods are
+/// no-op.
+class PktFilterStub : public PktFilter {
+public:
+
+ /// @brief Checks if the direct DHCPv4 response is supported.
+ ///
+ /// This function checks if the direct response capability is supported,
+ /// i.e. if the server can respond to the client which doesn't have an
+ /// address yet. For this dummy class, the true is alaways returned.
+ ///
+ /// @return always true.
+ virtual bool isDirectResponseSupported() const;
+
+ /// @brief Simulate opening of the socket.
+ ///
+ /// This function simulates opening a primary socket. In reality, it doesn't
+ /// open a socket but the socket descriptor returned in the SocketInfo
+ /// structure is always set to 0.
+ ///
+ /// @param iface An interface descriptor.
+ /// @param addr Address on the interface to be used to send packets.
+ /// @param port Port number to bind socket to.
+ /// @param receive_bcast A flag which indicates if socket should be
+ /// configured to receive broadcast packets (if true).
+ /// @param send_bcast A flag which indicates if the socket should be
+ /// configured to send broadcast packets (if true).
+ ///
+ /// @note All parameters are ignored.
+ ///
+ /// @return A SocketInfo structure with the socket descriptor set to 0. The
+ /// fallback socket descriptor is set to a negative value.
+ virtual SocketInfo openSocket(const Iface& iface,
+ const isc::asiolink::IOAddress& addr,
+ const uint16_t port, const bool, const bool);
+
+ /// @brief Simulate reception of the DHCPv4 message.
+ ///
+ /// @param iface An interface to be used to receive DHCPv4 message.
+ /// @param sock_info A descriptor of the primary and fallback sockets.
+ ///
+ /// @note All parameters are ignored.
+ ///
+ /// @return always a NULL object.
+ virtual Pkt4Ptr receive(const Iface& iface, const SocketInfo& sock_info);
+
+ /// @brief Simulates sending a DHCPv4 message.
+ ///
+ /// This function does nothing.
+ ///
+ /// @param iface An interface to be used to send DHCPv4 message.
+ /// @param port A port used to send a message.
+ /// @param pkt A DHCPv4 to be sent.
+ ///
+ /// @note All parameters are ignored.
+ ///
+ /// @return 0.
+ virtual int send(const Iface& iface, uint16_t port, const Pkt4Ptr& pkt);
+
+ // Change the scope of the protected function so as they can be unit tested.
+ using PktFilter::openFallbackSocket;
+
+};
+
+
+}; // end of isc::dhcp::test namespace
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // PKT_FILTER_TEST_UTILS_H
diff --git a/src/lib/dhcp/tests/pkt_filter_unittest.cc b/src/lib/dhcp/tests/pkt_filter_unittest.cc
new file mode 100644
index 0000000..eef7fea
--- /dev/null
+++ b/src/lib/dhcp/tests/pkt_filter_unittest.cc
@@ -0,0 +1,74 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/tests/pkt_filter_test_utils.h>
+#include <gtest/gtest.h>
+
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::dhcp::test;
+
+namespace {
+
+/// Port number used by tests.
+const uint16_t PORT = 10067;
+
+class PktFilterBaseClassTest : public isc::dhcp::test::PktFilterTest {
+public:
+ /// @brief Constructor
+ ///
+ /// Does nothing but setting up the UDP port for the test.
+ PktFilterBaseClassTest() : PktFilterTest(PORT) {
+ }
+};
+
+// This test verifies that the fallback socket is successfuly opened and
+// bound using the protected function of the PktFilter class.
+TEST_F(PktFilterBaseClassTest, openFallbackSocket) {
+ // Open socket using the function under test. Note that, we don't have to
+ // close the socket on our own because the test fixture constructor
+ // will handle it.
+ PktFilterStub pkt_filter;
+ ASSERT_NO_THROW(sock_info_.fallbackfd_ =
+ pkt_filter.openFallbackSocket(IOAddress("127.0.0.1"), PORT))
+ << "Failed to open fallback socket.";
+
+ // Test that the socket has been successfully created.
+ testDgramSocket(sock_info_.fallbackfd_);
+
+ // In addition, we should check that the fallback socket is non-blocking.
+ int sock_flags = fcntl(sock_info_.fallbackfd_, F_GETFL);
+ EXPECT_EQ(O_NONBLOCK, sock_flags & O_NONBLOCK)
+ << "Fallback socket is blocking, it should be non-blocking - check"
+ " fallback socket flags (fcntl).";
+
+ // Now that we have the socket open, let's try to open another one. This
+ // should cause a binding error.
+ int another_sock = -1;
+ EXPECT_THROW(another_sock =
+ pkt_filter.openFallbackSocket(IOAddress("127.0.0.1"), PORT),
+ isc::dhcp::SocketConfigError)
+ << "it should be not allowed to open and bind two fallback sockets"
+ " to the same address and port. Surprisingly, the socket bound.";
+ // Hard to believe we managed to open another socket. But if so, we have
+ // to close it to prevent a resource leak.
+ if (another_sock >= 0) {
+ close(another_sock);
+ }
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp/tests/protocol_util_unittest.cc b/src/lib/dhcp/tests/protocol_util_unittest.cc
new file mode 100644
index 0000000..971eb7f
--- /dev/null
+++ b/src/lib/dhcp/tests/protocol_util_unittest.cc
@@ -0,0 +1,392 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <asiolink/io_address.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/hwaddr.h>
+#include <dhcp/protocol_util.h>
+#include <util/buffer.h>
+#include <gtest/gtest.h>
+// in_systm.h is required on some some BSD systems
+// complaining that n_time is undefined but used
+// in ip.h.
+#include <netinet/in_systm.h>
+#include <netinet/ip.h>
+
+using namespace isc;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+
+namespace {
+
+ /*/// @brief OptionCustomTest test class.
+class OptionCustomTest : public ::testing::Test {
+public:
+};*/
+
+/// The purpose of this test is to verify that the IP header checksum
+/// is calculated correctly.
+TEST(ProtocolUtilTest, checksum) {
+ // IPv4 header to be used to calculate checksum.
+ const uint8_t hdr[] = {
+ 0x45, // IP version and header length
+ 0x00, // TOS
+ 0x00, 0x3c, // Total length of the IP packet.
+ 0x1c, 0x46, // Identification field.
+ 0x40, 0x00, // Fragmentation.
+ 0x40, // TTL
+ 0x06, // Protocol
+ 0x00, 0x00, // Checksum (reset to 0x00).
+ 0xac, 0x10, 0x0a, 0x63, // Source IP address.
+ 0xac, 0x10, 0x0a, 0x0c // Destination IP address.
+ };
+ // Calculate size of the header array.
+ const uint32_t hdr_size = sizeof(hdr) / sizeof(hdr[0]);
+ // Get the actual checksum.
+ uint16_t chksum = ~calcChecksum(hdr, hdr_size);
+ // The 0xb1e6 value has been calculated by other means.
+ EXPECT_EQ(0xb1e6, chksum);
+ // Tested function may also take the initial value of the sum.
+ // Let's set it to 2 and see whether it is included in the
+ // calculation.
+ chksum = ~calcChecksum(hdr, hdr_size, 2);
+ // The checkum value should change.
+ EXPECT_EQ(0xb1e4, chksum);
+}
+
+// The purpose of this test is to verify that the Ethernet frame header
+// can be decoded correctly. In particular it verifies that the source
+// HW address can be extracted from it.
+TEST(ProtocolUtilTest, decodeEthernetHeader) {
+ // Source HW address, 6 bytes.
+ const uint8_t src_hw_addr[6] = {
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15
+ };
+ // Destination HW address, 6 bytes.
+ const uint8_t dest_hw_addr[6] = {
+ 0x20, 0x31, 0x42, 0x53, 0x64, 0x75
+ };
+
+ // Prepare a buffer holding Ethernet frame header and 4 bytes of
+ // dummy data.
+ OutputBuffer buf(1);
+ buf.writeData(dest_hw_addr, sizeof(dest_hw_addr));
+ buf.writeData(src_hw_addr, sizeof(src_hw_addr));
+ buf.writeUint16(ETHERNET_TYPE_IP);
+ // Append dummy data. We will later check that this data is not
+ // removed or corrupted when reading the ethernet header.
+ buf.writeUint32(0x01020304);
+
+ // Create a buffer with truncated ethernet frame header..
+ InputBuffer in_buf_truncated(buf.getData(), buf.getLength() - 6);
+ // But provide valid packet object to make sure that the function
+ // under test does not throw due to NULL pointer packet.
+ Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 0));
+ // Function should throw because header data is truncated.
+ EXPECT_THROW(decodeEthernetHeader(in_buf_truncated, pkt),
+ InvalidPacketHeader);
+
+ // Get not truncated buffer.
+ InputBuffer in_buf(buf.getData(), buf.getLength());
+ // But provide NULL packet object instead.
+ pkt.reset();
+ // It should throw again but a different exception.
+ EXPECT_THROW(decodeEthernetHeader(in_buf, pkt),
+ BadValue);
+ // Now provide, correct data.
+ pkt.reset(new Pkt4(DHCPDISCOVER, 0));
+ // It should not throw now.
+ ASSERT_NO_THROW(decodeEthernetHeader(in_buf, pkt));
+ // Verify that the destination HW address has been initialized...
+ HWAddrPtr checked_dest_hwaddr = pkt->getLocalHWAddr();
+ ASSERT_TRUE(checked_dest_hwaddr);
+ // and is correct.
+ EXPECT_EQ(HWTYPE_ETHERNET, checked_dest_hwaddr->htype_);
+ ASSERT_EQ(sizeof(dest_hw_addr), checked_dest_hwaddr->hwaddr_.size());
+ EXPECT_TRUE(std::equal(dest_hw_addr, dest_hw_addr + sizeof(dest_hw_addr),
+ checked_dest_hwaddr->hwaddr_.begin()));
+
+ // Verify that the HW address of the source has been initialized.
+ HWAddrPtr checked_src_hwaddr = pkt->getRemoteHWAddr();
+ ASSERT_TRUE(checked_src_hwaddr);
+ // And that it is correct.
+ EXPECT_EQ(HWTYPE_ETHERNET, checked_src_hwaddr->htype_);
+ ASSERT_EQ(sizeof(src_hw_addr), checked_src_hwaddr->hwaddr_.size());
+ EXPECT_TRUE(std::equal(src_hw_addr, src_hw_addr + sizeof(src_hw_addr),
+ checked_src_hwaddr->hwaddr_.begin()));
+
+ // The entire ethernet packet header should have been read. This means
+ // that the internal buffer pointer should now point to its tail.
+ ASSERT_EQ(ETHERNET_HEADER_LEN, in_buf.getPosition());
+ // And the dummy data should be still readable and correct.
+ uint32_t dummy_data = in_buf.readUint32();
+ EXPECT_EQ(0x01020304, dummy_data);
+}
+
+/// The purpose of this test is to verify that the IP and UDP header
+/// is decoded correctly and appropriate values of IP addresses and
+/// ports are assigned to a Pkt4 object.
+TEST(ProtocolUtilTest, decodeIpUdpHeader) {
+ // IPv4 header to be parsed.
+ const uint8_t hdr[] = {
+ 0x45, // IP version and header length
+ 0x00, // TOS
+ 0x00, 0x3c, // Total length of the IP packet.
+ 0x1c, 0x46, // Identification field.
+ 0x40, 0x00, // Fragmentation.
+ 0x40, // TTL
+ IPPROTO_UDP, // Protocol
+ 0x00, 0x00, // Checksum (reset to 0x00).
+ 0xc0, 0x00, 0x02, 0x63, // Source IP address.
+ 0xc0, 0x00, 0x02, 0x0c, // Destination IP address.
+ 0x27, 0x54, // Source port
+ 0x27, 0x53, // Destination port
+ 0x00, 0x08, // UDP length
+ 0x00, 0x00 // Checksum
+ };
+
+ // Write header data to the buffer.
+ OutputBuffer buf(1);
+ buf.writeData(hdr, sizeof(hdr));
+ // Append some dummy data.
+ buf.writeUint32(0x01020304);
+
+ // Create an input buffer holding truncated headers.
+ InputBuffer in_buf_truncated(buf.getData(), buf.getLength() - 10);
+ // Create non NULL Pkt4 object to make sure that the function under
+ // test does not throw due to invalid Pkt4 object.
+ Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 0));
+ // Function should throw because buffer holds truncated data.
+ EXPECT_THROW(decodeIpUdpHeader(in_buf_truncated, pkt), InvalidPacketHeader);
+
+ // Create a valid input buffer (not truncated).
+ InputBuffer in_buf(buf.getData(), buf.getLength());
+ // Set NULL Pkt4 object to verify that function under test will
+ // return exception as expected.
+ pkt.reset();
+ // And check whether it throws exception.
+ EXPECT_THROW(decodeIpUdpHeader(in_buf, pkt), BadValue);
+
+ // Now, let's provide valid arguments and make sure it doesn't throw.
+ pkt.reset(new Pkt4(DHCPDISCOVER, 0));
+ ASSERT_TRUE(pkt);
+ EXPECT_NO_THROW(decodeIpUdpHeader(in_buf, pkt));
+
+ // Verify the source address and port.
+ EXPECT_EQ("192.0.2.99", pkt->getRemoteAddr().toText());
+ EXPECT_EQ(10068, pkt->getRemotePort());
+
+ // Verify the destination address and port.
+ EXPECT_EQ("192.0.2.12", pkt->getLocalAddr().toText());
+ EXPECT_EQ(10067, pkt->getLocalPort());
+
+ // Verify that the dummy data has not been corrupted and that the
+ // internal read pointer has been moved to the tail of the UDP
+ // header.
+ ASSERT_EQ(MIN_IP_HEADER_LEN + UDP_HEADER_LEN, in_buf.getPosition());
+ EXPECT_EQ(0x01020304, in_buf.readUint32());
+}
+
+/// The purpose of this test is to verify that the ethernet
+/// header is correctly constructed from the destination and
+/// hardware addresses.
+TEST(ProtocolUtilTest, writeEthernetHeader) {
+ // Source HW address, 6 bytes.
+ const uint8_t src_hw_addr[6] = {
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15
+ };
+ // Destination HW address, 6 bytes.
+ const uint8_t dest_hw_addr[6] = {
+ 0x20, 0x31, 0x42, 0x53, 0x64, 0x75
+ };
+
+ // Create output buffer.
+ OutputBuffer buf(1);
+ Pkt4Ptr pkt(new Pkt4(DHCPDISCOVER, 0));
+
+ HWAddrPtr local_hw_addr(new HWAddr(src_hw_addr, 6, 1));
+ ASSERT_NO_THROW(pkt->setLocalHWAddr(local_hw_addr));
+
+ // Set invalid length (7) of the hw address. Fill it with
+ // values of 1.
+ std::vector<uint8_t> invalid_length_addr(7, 1);
+ HWAddrPtr remote_hw_addr(new HWAddr(invalid_length_addr, 1));
+ ASSERT_NO_THROW(pkt->setRemoteHWAddr(remote_hw_addr));
+ // HW address is too long, so it should fail.
+ EXPECT_THROW(writeEthernetHeader(pkt, buf), BadValue);
+
+ // Finally, set a valid HW address.
+ remote_hw_addr.reset(new HWAddr(dest_hw_addr, 6, 1));
+ ASSERT_NO_THROW(pkt->setRemoteHWAddr(remote_hw_addr));
+
+ // Construct the ethernet header using HW addresses stored
+ // in the pkt object.
+ writeEthernetHeader(pkt, buf);
+
+ // The resulting ethernet header consists of destination
+ // and src HW address (each 6 bytes long) and two bytes
+ // of encapsulated protocol type.
+ ASSERT_EQ(14, buf.getLength());
+
+ // Verify that first 6 bytes comprise valid destination
+ // HW address. Instead of using memory comparison functions
+ // we check bytes one-by-one. In case of mismatch we will
+ // get exact values that are mismatched. If memcmp was used
+ // the error message would not indicate the values of
+ // mismatched bytes.
+ for (int i = 0; i < 6; ++i) {
+ EXPECT_EQ(dest_hw_addr[i], buf[i]);
+ }
+ // Verify that following 6 bytes comprise the valid source
+ // HW address.
+ for (int i = 0; i < 6; ++i) {
+ EXPECT_EQ(src_hw_addr[i], buf[i + 6]);
+ }
+
+ // The last two bytes comprise the encapsulated protocol type.
+ // We expect IPv4 protocol type which is specified by 0x0800.
+ EXPECT_EQ(0x08, buf[12]);
+ EXPECT_EQ(0x0, buf[13]);
+}
+
+TEST(ProtocolUtilTest, writeIpUdpHeader) {
+ // Create DHCPv4 packet. Some values held by this object are
+ // used by the utility function under test to figure out the
+ // contents of the IP and UDP headers, e.g. source and
+ // destination IP address or port number.
+ Pkt4Ptr pkt(new Pkt4(DHCPOFFER, 0));
+ ASSERT_TRUE(pkt);
+
+ // Set local and remote address and port.
+ pkt->setLocalAddr(IOAddress("192.0.2.1"));
+ pkt->setRemoteAddr(IOAddress("192.0.2.111"));
+ pkt->setLocalPort(DHCP4_SERVER_PORT);
+ pkt->setRemotePort(DHCP4_CLIENT_PORT);
+
+ // Pack the contents of the packet.
+ ASSERT_NO_THROW(pkt->pack());
+
+ // Create output buffer. The headers will be written to it.
+ OutputBuffer buf(1);
+ // Write some dummy data to the buffer. We will check that the
+ // function under test appends to this data, not overrides it.
+ buf.writeUint16(0x0102);
+
+ // Write both IP and UDP headers.
+ writeIpUdpHeader(pkt, buf);
+
+ // The resulting size of the buffer must be 30. The 28 bytes are
+ // consumed by the IP and UDP headers. The other 2 bytes are dummy
+ // data at the beginning of the buffer.
+ ASSERT_EQ(30, buf.getLength());
+
+ // Make sure that the existing data in the buffer was not corrupted
+ // by the function under test.
+ EXPECT_EQ(0x01, buf[0]);
+ EXPECT_EQ(0x02, buf[1]);
+
+ // Copy the contents of the buffer to InputBuffer object. This object
+ // exposes convenient functions for reading.
+ InputBuffer in_buf(buf.getData(), buf.getLength());
+
+ // Check dummy data.
+ uint16_t dummy_data = in_buf.readUint16();
+ EXPECT_EQ(0x0102, dummy_data);
+
+ // The IP version and IHL are stored in the same octet (4 bits each).
+ uint8_t ver_len = in_buf.readUint8();
+ // The most significant bits define IP version.
+ uint8_t ip_ver = ver_len >> 4;
+ EXPECT_EQ(4, ip_ver);
+ // The least significant bits define header length (in 32-bits chunks).
+ uint8_t ip_len = ver_len & 0x0F;
+ EXPECT_EQ(5, ip_len);
+
+ // Get Differentiated Services Codepoint and Explicit Congestion
+ // Notification field.
+ uint8_t dscp_ecn = in_buf.readUint8();
+ EXPECT_EQ(IPTOS_LOWDELAY, dscp_ecn);
+
+ // Total length of IP packet. Includes UDP header and payload.
+ uint16_t total_len = in_buf.readUint16();
+ EXPECT_EQ(28 + pkt->getBuffer().getLength(), total_len);
+
+ // Identification field.
+ uint16_t ident = in_buf.readUint16();
+ EXPECT_EQ(0, ident);
+
+ // Fragmentation.
+ uint16_t fragment = in_buf.readUint16();
+ // Setting second most significant bit means no fragmentation.
+ EXPECT_EQ(0x4000, fragment);
+
+ // Get TTL
+ uint8_t ttl = in_buf.readUint8();
+ // Expect non-zero TTL.
+ EXPECT_GE(ttl, 1);
+
+ // Protocol type is UDP.
+ uint8_t proto = in_buf.readUint8();
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP), proto);
+
+ // Check that the checksum is correct. The reference checksum value
+ // has been calculated manually.
+ uint16_t ip_checksum = in_buf.readUint16();
+ EXPECT_EQ(0x755c, ip_checksum);
+
+ // Validate source address.
+ // Initializing it to IPv6 address guarantees that it is not initialized
+ // to the value that we expect to be read from a header since the value
+ // read from a header will be IPv4.
+ IOAddress src_addr("::1");
+ // Read src address as an array of bytes because it is easely convertible
+ // to IOAddress object.
+ uint8_t src_addr_data[4];
+ ASSERT_NO_THROW(
+ in_buf.readData(src_addr_data, 4);
+ src_addr = IOAddress::fromBytes(AF_INET, src_addr_data);
+ );
+ EXPECT_EQ(IOAddress("192.0.2.1"), src_addr);
+
+ // Validate destination address.
+ IOAddress dest_addr("::1");
+ uint8_t dest_addr_data[4];
+ ASSERT_NO_THROW(
+ in_buf.readData(dest_addr_data, 4);
+ dest_addr = IOAddress::fromBytes(AF_INET, dest_addr_data);
+ );
+ EXPECT_EQ(IOAddress("192.0.2.111"), dest_addr);
+
+ // UDP header starts here.
+
+ // Check source port.
+ uint16_t src_port = in_buf.readUint16();
+ EXPECT_EQ(pkt->getLocalPort(), src_port);
+
+ // Check destination port.
+ uint16_t dest_port = in_buf.readUint16();
+ EXPECT_EQ(pkt->getRemotePort(), dest_port);
+
+ // UDP header and data length.
+ uint16_t udp_len = in_buf.readUint16();
+ EXPECT_EQ(8 + pkt->getBuffer().getLength(), udp_len);
+
+ // Verify UDP checksum. The reference checksum has been calculated manually.
+ uint16_t udp_checksum = in_buf.readUint16();
+ EXPECT_EQ(0x8817, udp_checksum);
+}
+
+} // anonymous namespace
diff --git a/src/lib/dhcp_ddns/.gitignore b/src/lib/dhcp_ddns/.gitignore
new file mode 100644
index 0000000..c632c2e
--- /dev/null
+++ b/src/lib/dhcp_ddns/.gitignore
@@ -0,0 +1,3 @@
+/dhcp_ddns_messages.cc
+/dhcp_ddns_messages.h
+/s-messages
diff --git a/src/lib/dhcp_ddns/Makefile.am b/src/lib/dhcp_ddns/Makefile.am
new file mode 100644
index 0000000..552f6d9
--- /dev/null
+++ b/src/lib/dhcp_ddns/Makefile.am
@@ -0,0 +1,60 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(BOTAN_INCLUDES)
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+
+# Define rule to build logging source files from message file
+dhcp_ddns_messages.h dhcp_ddns_messages.cc: s-messages
+
+s-messages: dhcp_ddns_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/dhcp_ddns/dhcp_ddns_messages.mes
+ touch $@
+
+# Tell automake that the message files are built as part of the build process
+# (so that they are built before the main library is built).
+BUILT_SOURCES = dhcp_ddns_messages.h dhcp_ddns_messages.cc
+
+# Ensure that the message file is included in the distribution
+EXTRA_DIST = dhcp_ddns_messages.mes libdhcp_ddns.dox
+
+# Get rid of generated message files on a clean
+CLEANFILES = *.gcno *.gcda dhcp_ddns_messages.h dhcp_ddns_messages.cc s-messages
+
+lib_LTLIBRARIES = libb10-dhcp_ddns.la
+libb10_dhcp_ddns_la_SOURCES =
+libb10_dhcp_ddns_la_SOURCES += dhcp_ddns_log.cc dhcp_ddns_log.h
+libb10_dhcp_ddns_la_SOURCES += ncr_io.cc ncr_io.h
+libb10_dhcp_ddns_la_SOURCES += ncr_msg.cc ncr_msg.h
+libb10_dhcp_ddns_la_SOURCES += ncr_udp.cc ncr_udp.h
+
+nodist_libb10_dhcp_ddns_la_SOURCES = dhcp_ddns_messages.cc dhcp_ddns_messages.h
+
+libb10_dhcp_ddns_la_CXXFLAGS = $(AM_CXXFLAGS)
+libb10_dhcp_ddns_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libb10_dhcp_ddns_la_LDFLAGS = $(AM_LDFLAGS)
+libb10_dhcp_ddns_la_LDFLAGS += ${BOTAN_LDFLAGS}
+libb10_dhcp_ddns_la_LIBADD =
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/cryptolink/libb10-cryptolink.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/util/libb10-util.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
+libb10_dhcp_ddns_la_LIBADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libb10_dhcp_ddns_la_LIBADD += ${BOTAN_LIBS} ${BOTAN_RPATH}
+
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+libb10_dhcp_ddns_la_CXXFLAGS += -Wno-unused-parameter
+endif
+
diff --git a/src/lib/dhcp_ddns/dhcp_ddns_log.cc b/src/lib/dhcp_ddns/dhcp_ddns_log.cc
new file mode 100644
index 0000000..4411b41
--- /dev/null
+++ b/src/lib/dhcp_ddns/dhcp_ddns_log.cc
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// Defines the logger used by the top-level component of b10-dhcp_ddns.
+
+#include <dhcp_ddns/dhcp_ddns_log.h>
+
+namespace isc {
+namespace dhcp_ddns {
+
+/// @brief Defines the logger used within lib dhcp_ddns.
+isc::log::Logger dhcp_ddns_logger("libdhcp-ddns");
+
+} // namespace dhcp_ddns
+} // namespace isc
+
diff --git a/src/lib/dhcp_ddns/dhcp_ddns_log.h b/src/lib/dhcp_ddns/dhcp_ddns_log.h
new file mode 100644
index 0000000..951f61e
--- /dev/null
+++ b/src/lib/dhcp_ddns/dhcp_ddns_log.h
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DHCP_DDNS_LOG_H
+#define DHCP_DDNS_LOG_H
+
+#include <log/logger_support.h>
+#include <log/macros.h>
+#include <dhcp_ddns/dhcp_ddns_messages.h>
+
+namespace isc {
+namespace dhcp_ddns {
+
+/// Define the logger for the "dhcp_ddns" logging.
+extern isc::log::Logger dhcp_ddns_logger;
+
+} // namespace dhcp_ddns
+} // namespace isc
+
+#endif // DHCP_DDNS_LOG_H
diff --git a/src/lib/dhcp_ddns/dhcp_ddns_messages.mes b/src/lib/dhcp_ddns/dhcp_ddns_messages.mes
new file mode 100644
index 0000000..51688ad
--- /dev/null
+++ b/src/lib/dhcp_ddns/dhcp_ddns_messages.mes
@@ -0,0 +1,76 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+$NAMESPACE isc::dhcp_ddns
+
+% DHCP_DDNS_INVALID_NCR application received an invalid DNS update request: %1
+This is an error message that indicates that an invalid request to update
+a DNS entry was received by the application. Either the format or the content
+of the request is incorrect. The request will be ignored.
+
+% DHCP_DDNS_NCR_LISTEN_CLOSE_ERROR application encountered an error while closing the listener used to receive NameChangeRequests : %1
+This is an error message that indicates the application was unable to close the
+listener connection used to receive NameChangeRequests. Closure may occur
+during the course of error recovery or during normal shutdown procedure. In
+either case the error is unlikely to impair the application's ability to
+process requests but it should be reported for analysis.
+
+% DHCP_DDNS_NCR_RECV_NEXT_ERROR application could not initiate the next read following a request receive.
+This is a error message indicating that NameChangeRequest listener could not
+start another read after receiving a request. While possible, this is highly
+unlikely and is probably a programmatic error. The application should recover
+on its own.
+
+% DHCP_DDNS_NCR_SEND_CLOSE_ERROR DHCP-DDNS client encountered an error while closing the sender connection used to send NameChangeRequests : %1
+This is an error message that indicates the DHCP-DDNS client was unable to
+close the connection used to send NameChangeRequests. Closure may occur during
+the course of error recovery or during normal shutdown procedure. In either
+case the error is unlikely to impair the client's ability to send requests but
+it should be reported for analysis.
+
+% DHCP_DDNS_NCR_SEND_NEXT_ERROR DHCP-DDNS client could not initiate the next request send following send completion.
+This is a error message indicating that NameChangeRequest sender could not
+start another send after completing the send of the previous request. While
+possible, this is highly unlikely and is probably a programmatic error. The
+application should recover on its own.
+
+% DHCP_DDNS_NCR_UDP_RECV_CANCELED UDP socket receive was canceled while listening for DNS Update requests: %1
+This is an informational message indicating that the listening over a UDP socket for DNS update requests has been canceled. This is a normal part of suspending listening operations.
+
+% DHCP_DDNS_NCR_UDP_RECV_ERROR UDP socket receive error while listening for DNS Update requests: %1
+This is an error message indicating that an IO error occurred while listening
+over a UDP socket for DNS update requests. This could indicate a network
+connectivity or system resource issue.
+
+% DHCP_DDNS_NCR_UDP_SEND_CANCELED UDP socket send was canceled while sending a DNS Update request to DHCP_DDNS: %1
+This is an informational message indicating that sending requests via UDP
+socket to DHCP_DDNS has been interrupted. This is a normal part of suspending
+send operations.
+
+% DHCP_DDNS_NCR_UDP_SEND_ERROR UDP socket send error while sending a DNS Update request: %1
+This is an error message indicating that an IO error occurred while sending a
+DNS update request to DHCP_DDNS over a UDP socket. This could indicate a
+network connectivity or system resource issue.
+
+% DHCP_DDNS_UNCAUGHT_NCR_RECV_HANDLER_ERROR unexpected exception thrown from the application receive completion handler: %1
+This is an error message that indicates that an exception was thrown but not
+caught in the application's request receive completion handler. This is a
+programmatic error that needs to be reported. Dependent upon the nature of
+the error the application may or may not continue operating normally.
+
+% DHCP_DDNS_UNCAUGHT_NCR_SEND_HANDLER_ERROR unexpected exception thrown from the DHCP-DDNS client send completion handler: %1
+This is an error message that indicates that an exception was thrown but not
+caught in the application's send completion handler. This is a programmatic
+error that needs to be reported. Dependent upon the nature of the error the
+client may or may not continue operating normally.
diff --git a/src/lib/dhcp_ddns/libdhcp_ddns.dox b/src/lib/dhcp_ddns/libdhcp_ddns.dox
new file mode 100644
index 0000000..af7c943
--- /dev/null
+++ b/src/lib/dhcp_ddns/libdhcp_ddns.dox
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ at page libdhcp_ddns DHCP_DDNS Request IO Library
+
+ at section libdhcp_ddnsIntro Libdhcp_ddns Library Introduction
+
+libdhcp_ddns is a library of classes for sending and receiving requests used
+by ISC's DHCP-DDNS service for carrying out DHCP-driven DDNS. These requests
+are implemented in this library by the class, NameChangeRequest.
+
+This class provides services for constructing the requests as well as
+marshalling them to and from various transport formats. Currently, the only
+format supported is JSON, however the design of the classes in this library is such that supporting additional formats will be easy to add.
+
+For sending and receiving NameChangeRequests, this library supplies an abstract
+pair of classes, NameChangeSender and NameChangeListener. NameChangeSender
+defines the public interface that DHCP_DDNS clients, such as DHCP servers use
+for sending requests to DHCP_DDNS. NameChangeListener is used by request
+consumers, primarily the DHCP_DDNS service, for receiving the requests.
+
+By providing abstract interfaces, the implementation isolates the senders and
+listeners from any underlying details of request transportation. This was done
+to allow support for a variety of transportation mechanisms. Currently, the
+only transport supported is via UDP Sockets, though support for TCP/IP sockets
+is forthcoming. There may eventually be an implementation which is driven
+through an RDBMS.
+
+The UDP implementation is provided by NameChangeUDPSender and NameChangeUPDListener.
+The implementation is strictly unidirectional. This means that there is no explicit
+acknowledgement of receipt of a request, and as it is UDP there is no guarantee of
+delivery. Once provided, transport via TCP/IP sockets will provide a more reliable
+mechanism.
+
+*/
diff --git a/src/lib/dhcp_ddns/ncr_io.cc b/src/lib/dhcp_ddns/ncr_io.cc
new file mode 100644
index 0000000..7e7174a
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_io.cc
@@ -0,0 +1,322 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp_ddns/dhcp_ddns_log.h>
+#include <dhcp_ddns/ncr_io.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace isc {
+namespace dhcp_ddns {
+
+NameChangeProtocol stringToNcrProtocol(const std::string& protocol_str) {
+ if (boost::iequals(protocol_str, "UDP")) {
+ return (NCR_UDP);
+ }
+
+ if (boost::iequals(protocol_str, "TCP")) {
+ return (NCR_TCP);
+ }
+
+ isc_throw(BadValue, "Invalid NameChangeRequest protocol:" << protocol_str);
+}
+
+std::string ncrProtocolToString(NameChangeProtocol protocol) {
+ switch (protocol) {
+ case NCR_UDP:
+ return ("UDP");
+ case NCR_TCP:
+ return ("TCP");
+ default:
+ break;
+ }
+
+ std::ostringstream stream;
+ stream << "UNKNOWN(" << protocol << ")";
+ return (stream.str());
+}
+
+
+//************************** NameChangeListener ***************************
+
+NameChangeListener::NameChangeListener(RequestReceiveHandler&
+ recv_handler)
+ : listening_(false), io_pending_(false), recv_handler_(recv_handler) {
+};
+
+
+void
+NameChangeListener::startListening(isc::asiolink::IOService& io_service) {
+ if (amListening()) {
+ // This amounts to a programmatic error.
+ isc_throw(NcrListenerError, "NameChangeListener is already listening");
+ }
+
+ // Call implementation dependent open.
+ try {
+ open(io_service);
+ } catch (const isc::Exception& ex) {
+ stopListening();
+ isc_throw(NcrListenerOpenError, "Open failed:" << ex.what());
+ }
+
+ // Set our status to listening.
+ setListening(true);
+
+ // Start the first asynchronous receive.
+ try {
+ receiveNext();
+ } catch (const isc::Exception& ex) {
+ stopListening();
+ isc_throw(NcrListenerReceiveError, "doReceive failed:" << ex.what());
+ }
+}
+
+void
+NameChangeListener::receiveNext() {
+ io_pending_ = true;
+ doReceive();
+}
+
+void
+NameChangeListener::stopListening() {
+ try {
+ // Call implementation dependent close.
+ close();
+ } catch (const isc::Exception &ex) {
+ // Swallow exceptions. If we have some sort of error we'll log
+ // it but we won't propagate the throw.
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_LISTEN_CLOSE_ERROR)
+ .arg(ex.what());
+ }
+
+ // Set it false, no matter what. This allows us to at least try to
+ // re-open via startListening().
+ setListening(false);
+}
+
+void
+NameChangeListener::invokeRecvHandler(const Result result,
+ NameChangeRequestPtr& ncr) {
+ // Call the registered application layer handler.
+ // Surround the invocation with a try-catch. The invoked handler is
+ // not supposed to throw, but in the event it does we will at least
+ // report it.
+ try {
+ io_pending_ = false;
+ recv_handler_(result, ncr);
+ } catch (const std::exception& ex) {
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_UNCAUGHT_NCR_RECV_HANDLER_ERROR)
+ .arg(ex.what());
+ }
+
+ // Start the next IO layer asynchronous receive.
+ // In the event the handler above intervened and decided to stop listening
+ // we need to check that first.
+ if (amListening()) {
+ try {
+ receiveNext();
+ } catch (const isc::Exception& ex) {
+ // It is possible though unlikely, for doReceive to fail without
+ // scheduling the read. While, unlikely, it does mean the callback
+ // will not get called with a failure. A throw here would surface
+ // at the IOService::run (or run variant) invocation. So we will
+ // close the window by invoking the application handler with
+ // a failed result, and let the application layer sort it out.
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_RECV_NEXT_ERROR)
+ .arg(ex.what());
+
+ // Call the registered application layer handler.
+ // Surround the invocation with a try-catch. The invoked handler is
+ // not supposed to throw, but in the event it does we will at least
+ // report it.
+ NameChangeRequestPtr empty;
+ try {
+ io_pending_ = false;
+ recv_handler_(ERROR, empty);
+ } catch (const std::exception& ex) {
+ LOG_ERROR(dhcp_ddns_logger,
+ DHCP_DDNS_UNCAUGHT_NCR_RECV_HANDLER_ERROR)
+ .arg(ex.what());
+ }
+ }
+ }
+}
+
+//************************* NameChangeSender ******************************
+
+NameChangeSender::NameChangeSender(RequestSendHandler& send_handler,
+ size_t send_queue_max)
+ : sending_(false), send_handler_(send_handler),
+ send_queue_max_(send_queue_max) {
+
+ // Queue size must be big enough to hold at least 1 entry.
+ if (send_queue_max == 0) {
+ isc_throw(NcrSenderError, "NameChangeSender constructor"
+ " queue size must be greater than zero");
+ }
+}
+
+void
+NameChangeSender::startSending(isc::asiolink::IOService& io_service) {
+ if (amSending()) {
+ // This amounts to a programmatic error.
+ isc_throw(NcrSenderError, "NameChangeSender is already sending");
+ }
+
+ // Clear send marker.
+ ncr_to_send_.reset();
+
+ // Call implementation dependent open.
+ try {
+ open(io_service);
+ } catch (const isc::Exception& ex) {
+ stopSending();
+ isc_throw(NcrSenderOpenError, "Open failed: " << ex.what());
+ }
+
+ // Set our status to sending.
+ setSending(true);
+}
+
+void
+NameChangeSender::stopSending() {
+ try {
+ // Call implementation dependent close.
+ close();
+ } catch (const isc::Exception &ex) {
+ // Swallow exceptions. If we have some sort of error we'll log
+ // it but we won't propagate the throw.
+ LOG_ERROR(dhcp_ddns_logger,
+ DHCP_DDNS_NCR_SEND_CLOSE_ERROR).arg(ex.what());
+ }
+
+ // Set it false, no matter what. This allows us to at least try to
+ // re-open via startSending().
+ setSending(false);
+}
+
+void
+NameChangeSender::sendRequest(NameChangeRequestPtr& ncr) {
+ if (!amSending()) {
+ isc_throw(NcrSenderError, "sender is not ready to send");
+ }
+
+ if (!ncr) {
+ isc_throw(NcrSenderError, "request to send is empty");
+ }
+
+ if (send_queue_.size() >= send_queue_max_) {
+ isc_throw(NcrSenderQueueFull, "send queue has reached maximum capacity:"
+ << send_queue_max_ );
+ }
+
+ // Put it on the queue.
+ send_queue_.push_back(ncr);
+
+ // Call sendNext to schedule the next one to go.
+ sendNext();
+}
+
+void
+NameChangeSender::sendNext() {
+ if (ncr_to_send_) {
+ // @todo Not sure if there is any risk of getting stuck here but
+ // an interval timer to defend would be good.
+ // In reality, the derivation should ensure they timeout themselves
+ return;
+ }
+
+ // If queue isn't empty, then get one from the front. Note we leave
+ // it on the front of the queue until we successfully send it.
+ if (!send_queue_.empty()) {
+ ncr_to_send_ = send_queue_.front();
+
+ // @todo start defense timer
+ // If a send were to hang and we timed it out, then timeout
+ // handler need to cycle thru open/close ?
+
+ // Call implementation dependent send.
+ doSend(ncr_to_send_);
+ }
+}
+
+void
+NameChangeSender::invokeSendHandler(const NameChangeSender::Result result) {
+ // @todo reset defense timer
+ if (result == SUCCESS) {
+ // It shipped so pull it off the queue.
+ send_queue_.pop_front();
+ }
+
+ // Invoke the completion handler passing in the result and a pointer
+ // the request involved.
+ // Surround the invocation with a try-catch. The invoked handler is
+ // not supposed to throw, but in the event it does we will at least
+ // report it.
+ try {
+ send_handler_(result, ncr_to_send_);
+ } catch (const std::exception& ex) {
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_UNCAUGHT_NCR_SEND_HANDLER_ERROR)
+ .arg(ex.what());
+ }
+
+ // Clear the pending ncr pointer.
+ ncr_to_send_.reset();
+
+ // Set up the next send
+ try {
+ sendNext();
+ } catch (const isc::Exception& ex) {
+ // It is possible though unlikely, for sendNext to fail without
+ // scheduling the send. While, unlikely, it does mean the callback
+ // will not get called with a failure. A throw here would surface
+ // at the IOService::run (or run variant) invocation. So we will
+ // close the window by invoking the application handler with
+ // a failed result, and let the application layer sort it out.
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_SEND_NEXT_ERROR)
+ .arg(ex.what());
+
+ // Invoke the completion handler passing in failed result.
+ // Surround the invocation with a try-catch. The invoked handler is
+ // not supposed to throw, but in the event it does we will at least
+ // report it.
+ try {
+ send_handler_(ERROR, ncr_to_send_);
+ } catch (const std::exception& ex) {
+ LOG_ERROR(dhcp_ddns_logger,
+ DHCP_DDNS_UNCAUGHT_NCR_SEND_HANDLER_ERROR).arg(ex.what());
+ }
+ }
+}
+
+void
+NameChangeSender::skipNext() {
+ if (!send_queue_.empty()) {
+ // Discards the request at the front of the queue.
+ send_queue_.pop_front();
+ }
+}
+
+void
+NameChangeSender::clearSendQueue() {
+ if (amSending()) {
+ isc_throw(NcrSenderError, "Cannot clear queue while sending");
+ }
+
+ send_queue_.clear();
+}
+
+} // namespace isc::dhcp_ddns
+} // namespace isc
diff --git a/src/lib/dhcp_ddns/ncr_io.h b/src/lib/dhcp_ddns/ncr_io.h
new file mode 100644
index 0000000..a5e513a
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_io.h
@@ -0,0 +1,700 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NCR_IO_H
+#define NCR_IO_H
+
+/// @file ncr_io.h
+/// @brief This file defines abstract classes for exchanging NameChangeRequests.
+///
+/// These classes are used for sending and receiving requests to update DNS
+/// information for FQDNs embodied as NameChangeRequests (aka NCRs). Ultimately,
+/// NCRs must move through the following three layers in order to implement
+/// DHCP-DDNS:
+///
+/// * Application layer - the business layer which needs to
+/// transport NameChangeRequests, and is unaware of the means by which
+/// they are transported.
+///
+/// * NameChangeRequest layer - This is the layer which acts as the
+/// intermediary between the Application layer and the IO layer. It must
+/// be able to move NameChangeRequests to the IO layer as raw data and move
+/// raw data from the IO layer in the Application layer as
+/// NameChangeRequests.
+///
+/// * IO layer - the low-level layer that is directly responsible for
+/// sending and receiving data asynchronously and is supplied through
+/// other libraries. This layer is largely unaware of the nature of the
+/// data being transmitted. In other words, it doesn't know beans about
+/// NCRs.
+///
+/// The abstract classes defined here implement the latter, middle layer,
+/// the NameChangeRequest layer. There are two types of participants in this
+/// middle ground:
+///
+/// * listeners - Receive NCRs from one or more sources. The DHCP-DDNS
+/// application, (aka D2), is a listener. Listeners are embodied by the
+/// class, NameChangeListener.
+///
+/// * senders - sends NCRs to a given target. DHCP servers are senders.
+/// Senders are embodied by the class, NameChangeListener.
+///
+/// These two classes present a public interface for asynchronous
+/// communications that is independent of the IO layer mechanisms. While the
+/// type and details of the IO mechanism are not relevant to either class, it
+/// is presumed to use isc::asiolink library for asynchronous event processing.
+///
+
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <dhcp_ddns/ncr_msg.h>
+#include <exceptions/exceptions.h>
+
+#include <deque>
+
+namespace isc {
+namespace dhcp_ddns {
+
+/// @brief Defines the list of socket protocols supported.
+/// Currently only UDP is implemented.
+/// @todo TCP is intended to be implemented prior 1.0 release.
+/// @todo Give some thought to an ANY protocol which might try
+/// first as UDP then as TCP, etc.
+enum NameChangeProtocol {
+ NCR_UDP,
+ NCR_TCP
+};
+
+/// @brief Function which converts labels to NameChangeProtocol enum values.
+///
+/// @param protocol_str text to convert to an enum.
+/// Valid string values: "UDP", "TCP"
+///
+/// @return NameChangeProtocol value which maps to the given string.
+///
+/// @throw isc::BadValue if given a string value which does not map to an
+/// enum value.
+extern NameChangeProtocol stringToNcrProtocol(const std::string& protocol_str);
+
+/// @brief Function which converts NameChangeProtocol enums to text labels.
+///
+/// @param protocol enum value to convert to label
+///
+/// @return std:string containing the text label if the value is valid, or
+/// "UNKNOWN" if not.
+extern std::string ncrProtocolToString(NameChangeProtocol protocol);
+
+/// @brief Exception thrown if an NcrListenerError encounters a general error.
+class NcrListenerError : public isc::Exception {
+public:
+ NcrListenerError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown if an error occurs during IO source open.
+class NcrListenerOpenError : public isc::Exception {
+public:
+ NcrListenerOpenError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown if an error occurs initiating an IO receive.
+class NcrListenerReceiveError : public isc::Exception {
+public:
+ NcrListenerReceiveError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Abstract interface for receiving NameChangeRequests.
+///
+/// NameChangeListener provides the means to:
+/// - Supply a callback to invoke upon receipt of an NCR or a listening
+/// error
+/// - Start listening using a given IOService instance to process events
+/// - Stop listening
+///
+/// It implements the high level logic flow to listen until a request arrives,
+/// invoke the implementation's handler and return to listening for the next
+/// request.
+///
+/// It provides virtual methods that allow derivations supply implementations
+/// to open the appropriate IO source, perform a listen, and close the IO
+/// source.
+///
+/// The overall design is based on a callback chain. The listener's caller (the
+/// application) supplies an "application" layer callback through which it will
+/// receive inbound NameChangeRequests. The listener derivation will supply
+/// its own callback to the IO layer to process receive events from its IO
+/// source. This is referred to as the NameChangeRequest completion handler.
+/// It is through this handler that the NameChangeRequest layer gains access
+/// to the low level IO read service results. It is expected to assemble
+/// NameChangeRequests from the inbound data and forward them to the
+/// application layer by invoking the application layer callback registered
+/// with the listener.
+///
+/// The application layer callback is structured around a nested class,
+/// RequestReceiveHandler. It consists of single, abstract operator() which
+/// accepts a result code and a pointer to a NameChangeRequest as parameters.
+/// In order to receive inbound NCRs, a caller implements a derivation of the
+/// RequestReceiveHandler and supplies an instance of this derivation to the
+/// NameChangeListener constructor. This "registers" the handler with the
+/// listener.
+///
+/// To begin listening, the caller invokes the listener's startListener()
+/// method, passing in an IOService instance. This in turn will pass the
+/// IOService into the virtual method, open(). The open method is where the
+/// listener derivation performs the steps necessary to prepare its IO source
+/// for reception (e.g. opening a socket, connecting to a database).
+///
+/// Assuming the open is successful, startListener will call receiveNext, to
+/// initiate an asynchronous receive. This method calls the virtual method,
+/// doReceive(). The listener derivation uses doReceive to instigate an IO
+/// layer asynchronous receieve passing in its IO layer callback to
+/// handle receive events from the IO source.
+///
+/// As stated earlier, the derivation's NameChangeRequest completion handler
+/// MUST invoke the application layer handler registered with the listener.
+/// This is done by passing in either a success status and a populated
+/// NameChangeRequest or an error status and an empty request into the
+/// listener's invokeRecvHandler method. This is the mechanism by which the
+/// listener's caller is handed inbound NCRs.
+class NameChangeListener {
+public:
+
+ /// @brief Defines the outcome of an asynchronous NCR receive
+ enum Result {
+ SUCCESS,
+ TIME_OUT,
+ STOPPED,
+ ERROR
+ };
+
+ /// @brief Abstract class for defining application layer receive callbacks.
+ ///
+ /// Applications which will receive NameChangeRequests must provide a
+ /// derivation of this class to the listener constructor in order to
+ /// receive NameChangeRequests.
+ class RequestReceiveHandler {
+ public:
+ /// @brief Function operator implementing a NCR receive callback.
+ ///
+ /// This method allows the application to receive the inbound
+ /// NameChangeRequests. It is intended to function as a hand off of
+ /// information and should probably not be time-consuming.
+ ///
+ /// @param result contains that receive outcome status.
+ /// @param ncr is a pointer to the newly received NameChangeRequest if
+ /// result is NameChangeListener::SUCCESS. It is indeterminate other
+ /// wise.
+ /// @throw This method MUST NOT throw.
+ virtual void operator ()(const Result result,
+ NameChangeRequestPtr& ncr) = 0;
+
+ virtual ~RequestReceiveHandler() {
+ }
+ };
+
+ /// @brief Constructor
+ ///
+ /// @param recv_handler is a pointer the application layer handler to be
+ /// invoked each time a NCR is received or a receive error occurs.
+ NameChangeListener(RequestReceiveHandler& recv_handler);
+
+ /// @brief Destructor
+ virtual ~NameChangeListener() {
+ };
+
+ /// @brief Prepares the IO for reception and initiates the first receive.
+ ///
+ /// Calls the derivation's open implementation to initialize the IO layer
+ /// source for receiving inbound requests. If successful, it starts the
+ /// first asynchronous read by receiveNext.
+ ///
+ /// @param io_service is the IOService that will handle IO event processing.
+ ///
+ /// @throw NcrListenError if the listener is already "listening" or
+ /// in the event the open or doReceive methods fail.
+ void startListening(isc::asiolink::IOService& io_service);
+
+ /// @brief Closes the IO source and stops listen logic.
+ ///
+ /// Calls the derivation's implementation of close and marks the state
+ /// as not listening.
+ void stopListening();
+
+protected:
+ /// @brief Initiates an asynchronous receive
+ ///
+ /// Sets context information to indicate that IO is in progress and invokes
+ /// the derivation's asynchronous receive method, doReceive. Note doReceive
+ /// should not be called outside this method to ensure context information
+ /// integrity.
+ ///
+ /// @throw Derivation's doReceive method may throw isc::Exception upon
+ /// error.
+ void receiveNext();
+
+ /// @brief Calls the NCR receive handler registered with the listener.
+ ///
+ /// This is the hook by which the listener's caller's NCR receive handler
+ /// is called. This method MUST be invoked by the derivation's
+ /// implementation of doReceive.
+ ///
+ /// NOTE:
+ /// The handler invoked by this method MUST NOT THROW. The handler is
+ /// at application level and should trap and handle any errors at
+ /// that level, rather than throw exceptions. If an error has occurred
+ /// prior to invoking the handler, it will be expressed in terms a failed
+ /// result being passed to the handler, not a throw. Therefore any
+ /// exceptions at the handler level are application issues and should be
+ /// dealt with at that level.
+ ///
+ /// This method does wrap the handler invocation within a try-catch
+ /// block as a fail-safe. The exception will be logged but the
+ /// receive logic will continue. What this implies is that continued
+ /// operation may or may not succeed as the application has violated
+ /// the interface contract.
+ ///
+ /// @param result contains that receive outcome status.
+ /// @param ncr is a pointer to the newly received NameChangeRequest if
+ /// result is NameChangeListener::SUCCESS. It is indeterminate other
+ /// wise.
+ void invokeRecvHandler(const Result result, NameChangeRequestPtr& ncr);
+
+ /// @brief Abstract method which opens the IO source for reception.
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// prepare the IO source to receive requests.
+ ///
+ /// @param io_service is the IOService that process IO events.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void open(isc::asiolink::IOService& io_service) = 0;
+
+ /// @brief Abstract method which closes the IO source.
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// "close" the IO source.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void close() = 0;
+
+ /// @brief Initiates an IO layer asynchronous read.
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// initiate an asynchronous read of the IO source with the
+ /// derivation's IO layer handler as the IO completion callback.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void doReceive() = 0;
+
+public:
+ /// @brief Returns true if the listener is listening, false otherwise.
+ ///
+ /// A true value indicates that the IO source has been opened successfully,
+ /// and that receive loop logic is active. This implies that closing the
+ /// IO source will interrupt that operation, resulting in a callback
+ /// invocation.
+ bool amListening() const {
+ return (listening_);
+ }
+
+ /// @brief Returns true if the listener has an IO call in progress.
+ ///
+ /// A true value indicates that the listener has an asynchronous IO in
+ /// progress which will complete at some point in the future. Completion
+ /// of the call will invoke the registered callback. It is important to
+ /// understand that the listener and its related objects should not be
+ /// deleted while there is an IO call pending. This can result in the
+ /// IO service attempting to invoke methods on objects that are no longer
+ /// valid.
+ bool isIoPending() const {
+ return (io_pending_);
+ }
+
+private:
+ /// @brief Sets the listening indicator to the given value.
+ ///
+ /// Note, this method is private as it is used the base class is solely
+ /// responsible for managing the state.
+ ///
+ /// @param value is the new value to assign to the indicator.
+ void setListening(bool value) {
+ listening_ = value;
+ }
+
+ /// @brief Indicates if the listener is in listening mode.
+ bool listening_;
+
+ /// @brief Indicates that listener has an async IO pending completion.
+ bool io_pending_;
+
+ /// @brief Application level NCR receive completion handler.
+ RequestReceiveHandler& recv_handler_;
+};
+
+/// @brief Defines a smart pointer to an instance of a listener.
+typedef boost::shared_ptr<NameChangeListener> NameChangeListenerPtr;
+
+
+/// @brief Thrown when a NameChangeSender encounters an error.
+class NcrSenderError : public isc::Exception {
+public:
+ NcrSenderError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown if an error occurs during IO source open.
+class NcrSenderOpenError : public isc::Exception {
+public:
+ NcrSenderOpenError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown if an error occurs initiating an IO send.
+class NcrSenderQueueFull : public isc::Exception {
+public:
+ NcrSenderQueueFull(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown if an error occurs initiating an IO send.
+class NcrSenderSendError : public isc::Exception {
+public:
+ NcrSenderSendError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Abstract interface for sending NameChangeRequests.
+///
+/// NameChangeSender provides the means to:
+/// - Supply a callback to invoke upon completing the delivery of an NCR or a
+/// send error
+/// - Start sending using a given IOService instance to process events
+/// - Queue NCRs for delivery
+/// - Stop sending
+///
+/// It implements the high level logic flow to queue requests for delivery,
+/// and ship them one at a time, waiting for the send to complete prior to
+/// sending the next request in the queue. If a send fails, the request
+/// will remain at the front of queue and will be the send will be retried
+/// endlessly unless the caller dequeues the request. Note, it is presumed that
+/// a send failure is some form of IO error such as loss of connectivity and
+/// not a message content error. It should not be possible to queue an invalid
+/// message.
+///
+/// It should be noted that once a request is placed onto the send queue it
+/// will remain there until one of three things occur:
+/// * It is successfully delivered
+/// * @c NameChangeSender::skipNext() is called
+/// * @c NameChangeSender::clearSendQueue() is called
+///
+/// The queue contents are preserved across start and stop listening
+/// transitions. This is to provide for error recovery without losing
+/// undelivered requests.
+
+/// It provides virtual methods so derivations may supply implementations to
+/// open the appropriate IO sink, perform a send, and close the IO sink.
+///
+/// The overall design is based on a callback chain. The sender's caller (the
+/// application) supplies an "application" layer callback through which it will
+/// be given send completion notifications. The sender derivation will employ
+/// its own callback at the IO layer to process send events from its IO sink.
+/// This callback is expected to forward the outcome of each asynchronous send
+/// to the application layer by invoking the application layer callback
+/// registered with the sender.
+///
+/// The application layer callback is structured around a nested class,
+/// RequestSendHandler. It consists of single, abstract operator() which
+/// accepts a result code and a pointer to a NameChangeRequest as parameters.
+/// In order to receive send completion notifications, a caller implements a
+/// derivation of the RequestSendHandler and supplies an instance of this
+/// derivation to the NameChangeSender constructor. This "registers" the
+/// handler with the sender.
+///
+/// To begin sending, the caller invokes the listener's startSending()
+/// method, passing in an IOService instance. This in turn will pass the
+/// IOService into the virtual method, open(). The open method is where the
+/// sender derivation performs the steps necessary to prepare its IO sink for
+/// output (e.g. opening a socket, connecting to a database). At this point,
+/// the sender is ready to send messages.
+///
+/// In order to send a request, the application layer invokes the sender
+/// method, sendRequest(), passing in the NameChangeRequest to send. This
+/// method places the request onto the back of the send queue, and then invokes
+/// the sender method, sendNext().
+///
+/// If there is already a send in progress when sendNext() is called, the method
+/// will return immediately rather than initiate the next send. This is to
+/// ensure that sends are processed sequentially.
+///
+/// If there is not a send in progress and the send queue is not empty,
+/// the sendNext method will pass the NCR at the front of the send queue into
+/// the virtual doSend() method.
+///
+/// The sender derivation uses this doSend() method to instigate an IO layer
+/// asynchronous send with its IO layer callback to handle send events from its
+/// IO sink.
+///
+/// As stated earlier, the derivation's IO layer callback MUST invoke the
+/// application layer handler registered with the sender. This is done by
+/// passing in a status indicating the outcome of the send into the sender's
+/// invokeSendHandler method. This is the mechanism by which the sender's
+/// caller is handed outbound notifications.
+
+/// After invoking the application layer handler, the invokeSendHandler method
+/// will call the sendNext() method to initiate the next send. This ensures
+/// that requests continue to dequeue and ship.
+///
+class NameChangeSender {
+public:
+
+ /// @brief Defines the type used for the request send queue.
+ typedef std::deque<NameChangeRequestPtr> SendQueue;
+
+ /// @brief Defines a default maximum number of entries in the send queue.
+ static const size_t MAX_QUEUE_DEFAULT = 1024;
+
+ /// @brief Defines the outcome of an asynchronous NCR send.
+ enum Result {
+ SUCCESS,
+ TIME_OUT,
+ STOPPED,
+ ERROR
+ };
+
+ /// @brief Abstract class for defining application layer send callbacks.
+ ///
+ /// Applications which will send NameChangeRequests must provide a
+ /// derivation of this class to the sender constructor in order to
+ /// receive send outcome notifications.
+ class RequestSendHandler {
+ public:
+ /// @brief Function operator implementing a NCR send callback.
+ ///
+ /// This method allows the application to receive the outcome of
+ /// each send. It is intended to function as a hand off of information
+ /// and should probably not be time-consuming.
+ ///
+ /// @param result contains that send outcome status.
+ /// @param ncr is a pointer to the NameChangeRequest that was
+ /// delivered (or attempted).
+ ///
+ /// @throw This method MUST NOT throw.
+ virtual void operator ()(const Result result,
+ NameChangeRequestPtr& ncr) = 0;
+
+ virtual ~RequestSendHandler() {
+ }
+ };
+
+ /// @brief Constructor
+ ///
+ /// @param send_handler is a pointer the application layer handler to be
+ /// invoked each time a NCR send attempt completes.
+ /// @param send_queue_max is the maximum number of entries allowed in the
+ /// send queue. Once the maximum number is reached, all calls to
+ /// sendRequest will fail with an exception.
+ NameChangeSender(RequestSendHandler& send_handler,
+ size_t send_queue_max = MAX_QUEUE_DEFAULT);
+
+ /// @brief Destructor
+ virtual ~NameChangeSender() {
+ }
+
+ /// @brief Prepares the IO for transmission.
+ ///
+ /// Calls the derivation's open implementation to initialize the IO layer
+ /// sink for sending outbound requests.
+ ///
+ /// @param io_service is the IOService that will handle IO event processing.
+ ///
+ /// @throw NcrSenderError if the sender is already "sending" or
+ /// NcrSenderOpenError if the open fails.
+ void startSending(isc::asiolink::IOService & io_service);
+
+ /// @brief Closes the IO sink and stops send logic.
+ ///
+ /// Calls the derivation's implementation of close and marks the state
+ /// as not sending.
+ void stopSending();
+
+ /// @brief Queues the given request to be sent.
+ ///
+ /// The given request is placed at the back of the send queue and then
+ /// sendNext is invoked.
+ ///
+ /// @param ncr is the NameChangeRequest to send.
+ ///
+ /// @throw NcrSenderError if the sender is not in sending state or
+ /// the request is empty; NcrSenderQueueFull if the send queue has reached
+ /// capacity.
+ void sendRequest(NameChangeRequestPtr& ncr);
+
+protected:
+ /// @brief Dequeues and sends the next request on the send queue.
+ ///
+ /// If there is already a send in progress just return. If there is not
+ /// a send in progress and the send queue is not empty the grab the next
+ /// message on the front of the queue and call doSend().
+ ///
+ void sendNext();
+
+ /// @brief Calls the NCR send completion handler registered with the
+ /// sender.
+ ///
+ /// This is the hook by which the sender's caller's NCR send completion
+ /// handler is called. This method MUST be invoked by the derivation's
+ /// implementation of doSend. Note that if the send was a success,
+ /// the entry at the front of the queue is removed from the queue.
+ /// If not we leave it there so we can retry it. After we invoke the
+ /// handler we clear the pending ncr value and queue up the next send.
+ ///
+ /// NOTE:
+ /// The handler invoked by this method MUST NOT THROW. The handler is
+ /// application level logic and should trap and handle any errors at
+ /// that level, rather than throw exceptions. If IO errors have occurred
+ /// prior to invoking the handler, they are expressed in terms a failed
+ /// result being passed to the handler. Therefore any exceptions at the
+ /// handler level are application issues and should be dealt with at that
+ /// level.
+ ///
+ /// This method does wrap the handler invocation within a try-catch
+ /// block as a fail-safe. The exception will be logged but the
+ /// send logic will continue. What this implies is that continued
+ /// operation may or may not succeed as the application has violated
+ /// the interface contract.
+ ///
+ /// @param result contains that send outcome status.
+ void invokeSendHandler(const NameChangeSender::Result result);
+
+ /// @brief Abstract method which opens the IO sink for transmission.
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// prepare the IO sink to send requests.
+ ///
+ /// @param io_service is the IOService that process IO events.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void open(isc::asiolink::IOService& io_service) = 0;
+
+ /// @brief Abstract method which closes the IO sink.
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// "close" the IO sink.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void close() = 0;
+
+ /// @brief Initiates an IO layer asynchronous send
+ ///
+ /// The derivation uses this method to perform the steps needed to
+ /// initiate an asynchronous send through the IO sink of the given NCR.
+ ///
+ /// @param ncr is a pointer to the NameChangeRequest to send.
+ /// derivation's IO layer handler as the IO completion callback.
+ ///
+ /// @throw If the implementation encounters an error it MUST
+ /// throw it as an isc::Exception or derivative.
+ virtual void doSend(NameChangeRequestPtr& ncr) = 0;
+
+public:
+ /// @brief Removes the request at the front of the send queue
+ ///
+ /// This method can be used to avoid further retries of a failed
+ /// send. It is provided primarily as a just-in-case measure. Since
+ /// a failed send results in the same request being retried continuously
+ /// this method makes it possible to remove that entry, causing the
+ /// subsequent entry in the queue to be attempted on the next send.
+ /// It is presumed that sends will only fail due to some sort of
+ /// communications issue. In the unlikely event that a request is
+ /// somehow tainted and causes an send failure based on its content,
+ /// this method provides a means to remove th message.
+ void skipNext();
+
+ /// @brief Flushes all entries in the send queue
+ ///
+ /// This method can be used to discard all of the NCRs currently in the
+ /// the send queue. Note it may not be called while the sender is in
+ /// the sending state.
+ /// @throw NcrSenderError if called and sender is in sending state.
+ void clearSendQueue();
+
+ /// @brief Returns true if the sender is in send mode, false otherwise.
+ ///
+ /// A true value indicates that the IO sink has been opened successfully,
+ /// and that send loop logic is active.
+ bool amSending() const {
+ return (sending_);
+ }
+
+ /// @brief Returns true when a send is in progress.
+ ///
+ /// A true value indicates that a request is actively in the process of
+ /// being delivered.
+ bool isSendInProgress() const {
+ return ((ncr_to_send_) ? true : false);
+ }
+
+ /// @brief Returns the maximum number of entries allowed in the send queue.
+ size_t getQueueMaxSize() const {
+ return (send_queue_max_);
+ }
+
+ /// @brief Returns the number of entries currently in the send queue.
+ size_t getQueueSize() const {
+ return (send_queue_.size());
+ }
+
+private:
+ /// @brief Sets the sending indicator to the given value.
+ ///
+ /// Note, this method is private as it is used the base class is solely
+ /// responsible for managing the state.
+ ///
+ /// @param value is the new value to assign to the indicator.
+ void setSending(bool value) {
+ sending_ = value;
+ }
+
+ /// @brief Boolean indicator which tracks sending status.
+ bool sending_;
+
+ /// @brief A pointer to regisetered send completion handler.
+ RequestSendHandler& send_handler_;
+
+ /// @brief Maximum number of entries permitted in the send queue.
+ size_t send_queue_max_;
+
+ /// @brief Queue of the requests waiting to be sent.
+ SendQueue send_queue_;
+
+ /// @brief Pointer to the request which is in the process of being sent.
+ NameChangeRequestPtr ncr_to_send_;
+};
+
+/// @brief Defines a smart pointer to an instance of a sender.
+typedef boost::shared_ptr<NameChangeSender> NameChangeSenderPtr;
+
+} // namespace isc::dhcp_ddns
+} // namespace isc
+
+#endif
diff --git a/src/lib/dhcp_ddns/ncr_msg.cc b/src/lib/dhcp_ddns/ncr_msg.cc
new file mode 100644
index 0000000..ae1816f
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_msg.cc
@@ -0,0 +1,641 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp_ddns/ncr_msg.h>
+#include <dns/name.h>
+#include <asiolink/io_address.h>
+#include <asiolink/io_error.h>
+#include <cryptolink/cryptolink.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <botan/sha2_32.h>
+
+#include <sstream>
+#include <limits>
+
+
+namespace isc {
+namespace dhcp_ddns {
+
+
+NameChangeFormat stringToNcrFormat(const std::string& fmt_str) {
+ if (boost::iequals(fmt_str, "JSON")) {
+ return FMT_JSON;
+ }
+
+ isc_throw(BadValue, "Invalid NameChangeRequest format:" << fmt_str);
+}
+
+
+std::string ncrFormatToString(NameChangeFormat format) {
+ if (format == FMT_JSON) {
+ return ("JSON");
+ }
+
+ std::ostringstream stream;
+ stream << "UNKNOWN(" << format << ")";
+ return (stream.str());
+}
+
+/********************************* D2Dhcid ************************************/
+
+namespace {
+
+///
+/// @name Constants which define DHCID identifier-type
+//@{
+/// DHCID created from client's HW address.
+const uint8_t DHCID_ID_HWADDR = 0x0;
+/// DHCID created from client identifier.
+const uint8_t DHCID_ID_CLIENTID = 0x1;
+/// DHCID created from DUID.
+const uint8_t DHCID_ID_DUID = 0x2;
+
+}
+
+D2Dhcid::D2Dhcid() {
+}
+
+D2Dhcid::D2Dhcid(const std::string& data) {
+ fromStr(data);
+}
+
+D2Dhcid::D2Dhcid(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromHWAddr(hwaddr, wire_fqdn);
+}
+
+D2Dhcid::D2Dhcid(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromClientId(clientid_data, wire_fqdn);
+}
+
+D2Dhcid::D2Dhcid(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn) {
+ fromDUID(duid, wire_fqdn);
+}
+
+
+void
+D2Dhcid::fromStr(const std::string& data) {
+ bytes_.clear();
+ try {
+ isc::util::encode::decodeHex(data, bytes_);
+ } catch (const isc::Exception& ex) {
+ isc_throw(NcrMessageError, "Invalid data in Dhcid:" << ex.what());
+ }
+}
+
+std::string
+D2Dhcid::toStr() const {
+ return (isc::util::encode::encodeHex(bytes_));
+}
+
+void
+D2Dhcid::fromClientId(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ createDigest(DHCID_ID_CLIENTID, clientid_data, wire_fqdn);
+}
+
+void
+D2Dhcid::fromHWAddr(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn) {
+ if (!hwaddr) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from the HW address, "
+ "NULL pointer has been specified");
+ } else if (hwaddr->hwaddr_.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "unable to compute DHCID from the HW address, "
+ "HW address is empty");
+ }
+ std::vector<uint8_t> hwaddr_data;
+ hwaddr_data.push_back(hwaddr->htype_);
+ hwaddr_data.insert(hwaddr_data.end(), hwaddr->hwaddr_.begin(),
+ hwaddr->hwaddr_.end());
+ createDigest(DHCID_ID_HWADDR, hwaddr_data, wire_fqdn);
+}
+
+
+void
+D2Dhcid::fromDUID(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn) {
+
+ createDigest(DHCID_ID_DUID, duid.getDuid(), wire_fqdn);
+}
+
+void
+D2Dhcid::createDigest(const uint8_t identifier_type,
+ const std::vector<uint8_t>& identifier_data,
+ const std::vector<uint8_t>& wire_fqdn) {
+ // We get FQDN in the wire format, so we don't know if it is
+ // valid. It is caller's responsibility to make sure it is in
+ // the valid format. Here we just make sure it is not empty.
+ if (wire_fqdn.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "empty FQDN used to create DHCID");
+ }
+
+ // It is a responsibility of the classes which encapsulate client
+ // identifiers, e.g. DUID, to validate the client identifier data.
+ // But let's be on the safe side and at least check that it is not
+ // empty.
+ if (identifier_data.empty()) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "empty DUID used to create DHCID");
+ }
+
+ // A data buffer will be used to compute the digest.
+ std::vector<uint8_t> data = identifier_data;
+
+ // Append FQDN in the wire format.
+ data.insert(data.end(), wire_fqdn.begin(), wire_fqdn.end());
+
+ // Use the DUID and FQDN to compute the digest (see RFC4701, section 3).
+
+ // The getCryptoLink is a common function to initialize the Botan library.
+ cryptolink::CryptoLink::getCryptoLink();
+ // @todo The code below, which calculates the SHA-256 may need to be moved
+ // to the cryptolink library.
+ Botan::SecureVector<Botan::byte> secure;
+ try {
+ Botan::SHA_256 sha;
+ // We have checked already that the DUID and FQDN aren't empty
+ // so it is safe to assume that the data buffer is not empty.
+ secure = sha.process(static_cast<const Botan::byte*>(&data[0]),
+ data.size());
+ } catch (const std::exception& ex) {
+ isc_throw(isc::dhcp_ddns::DhcidRdataComputeError,
+ "error while generating DHCID from DUID: "
+ << ex.what());
+ }
+
+ // The DHCID RDATA has the following structure:
+ //
+ // < identifier-type > < digest-type > < digest >
+ //
+ // where identifier type
+
+ // Let's allocate the space for the identifier-type (2 bytes) and
+ // digest-type (1 byte). This is 3 bytes all together.
+ bytes_.resize(3);
+ // Leave first byte 0 and set the second byte. Those two bytes
+ // form the identifier-type.
+ bytes_[1] = identifier_type;
+ // Third byte is always equal to 1, which specifies SHA-256 digest type.
+ bytes_[2] = 1;
+ // Now let's append the digest.
+ bytes_.insert(bytes_.end(), secure.begin(), secure.end());
+}
+
+std::ostream&
+operator<<(std::ostream& os, const D2Dhcid& dhcid) {
+ os << dhcid.toStr();
+ return (os);
+}
+
+
+
+/**************************** NameChangeRequest ******************************/
+
+NameChangeRequest::NameChangeRequest()
+ : change_type_(CHG_ADD), forward_change_(false),
+ reverse_change_(false), fqdn_(""), ip_io_address_("0.0.0.0"),
+ dhcid_(), lease_expires_on_(), lease_length_(0), status_(ST_NEW) {
+}
+
+NameChangeRequest::NameChangeRequest(const NameChangeType change_type,
+ const bool forward_change, const bool reverse_change,
+ const std::string& fqdn, const std::string& ip_address,
+ const D2Dhcid& dhcid,
+ const uint64_t lease_expires_on,
+ const uint32_t lease_length)
+ : change_type_(change_type), forward_change_(forward_change),
+ reverse_change_(reverse_change), fqdn_(fqdn), ip_io_address_("0.0.0.0"),
+ dhcid_(dhcid), lease_expires_on_(lease_expires_on),
+ lease_length_(lease_length), status_(ST_NEW) {
+
+ // User setter to validate address.
+ setIpAddress(ip_address);
+
+ // Validate the contents. This will throw a NcrMessageError if anything
+ // is invalid.
+ validateContent();
+}
+
+NameChangeRequestPtr
+NameChangeRequest::fromFormat(const NameChangeFormat format,
+ isc::util::InputBuffer& buffer) {
+ // Based on the format requested, pull the marshalled request from
+ // InputBuffer and pass it into the appropriate format-specific factory.
+ NameChangeRequestPtr ncr;
+ switch (format) {
+ case FMT_JSON: {
+ try {
+ // Get the length of the JSON text.
+ size_t len = buffer.readUint16();
+
+ // Read the text from the buffer into a vector.
+ std::vector<uint8_t> vec;
+ buffer.readVector(vec, len);
+
+ // Turn the vector into a string.
+ std::string string_data(vec.begin(), vec.end());
+
+ // Pass the string of JSON text into JSON factory to create the
+ // NameChangeRequest instance. Note the factory may throw
+ // NcrMessageError.
+ ncr = NameChangeRequest::fromJSON(string_data);
+ } catch (isc::util::InvalidBufferPosition& ex) {
+ // Read error accessing data in InputBuffer.
+ isc_throw(NcrMessageError, "fromFormat: buffer read error: "
+ << ex.what());
+ }
+
+ break;
+ }
+ default:
+ // Programmatic error, shouldn't happen.
+ isc_throw(NcrMessageError, "fromFormat - invalid format");
+ break;
+ }
+
+ return (ncr);
+}
+
+void
+NameChangeRequest::toFormat(const NameChangeFormat format,
+ isc::util::OutputBuffer& buffer) const {
+ // Based on the format requested, invoke the appropriate format handler
+ // which will marshal this request's contents into the OutputBuffer.
+ switch (format) {
+ case FMT_JSON: {
+ // Invoke toJSON to create a JSON text of this request's contents.
+ std::string json = toJSON();
+ uint16_t length = json.size();
+
+ // Write the length of the JSON text to the OutputBuffer first, then
+ // write the JSON text itself.
+ buffer.writeUint16(length);
+ buffer.writeData(json.c_str(), length);
+ break;
+ }
+ default:
+ // Programmatic error, shouldn't happen.
+ isc_throw(NcrMessageError, "toFormat - invalid format");
+ break;
+ }
+}
+
+NameChangeRequestPtr
+NameChangeRequest::fromJSON(const std::string& json) {
+ // This method leverages the existing JSON parsing provided by isc::data
+ // library. Should this prove to be a performance issue, it may be that
+ // lighter weight solution would be appropriate.
+
+ // Turn the string of JSON text into an Element set.
+ isc::data::ElementPtr elements;
+ try {
+ elements = isc::data::Element::fromJSON(json);
+ } catch (isc::data::JSONError& ex) {
+ isc_throw(NcrMessageError,
+ "Malformed NameChangeRequest JSON: " << ex.what());
+ }
+
+ // Get a map of the Elements, keyed by element name.
+ ElementMap element_map = elements->mapValue();
+ isc::data::ConstElementPtr element;
+
+ // Use default constructor to create a "blank" NameChangeRequest.
+ NameChangeRequestPtr ncr(new NameChangeRequest());
+
+ // For each member of NameChangeRequest, find its element in the map and
+ // call the appropriate Element-based setter. These setters may throw
+ // NcrMessageError if the given Element is the wrong type or its data
+ // content is lexically invalid. If the element is NOT found in the
+ // map, getElement will throw NcrMessageError indicating the missing
+ // member. Currently there are no optional values.
+ element = ncr->getElement("change_type", element_map);
+ ncr->setChangeType(element);
+
+ element = ncr->getElement("forward_change", element_map);
+ ncr->setForwardChange(element);
+
+ element = ncr->getElement("reverse_change", element_map);
+ ncr->setReverseChange(element);
+
+ element = ncr->getElement("fqdn", element_map);
+ ncr->setFqdn(element);
+
+ element = ncr->getElement("ip_address", element_map);
+ ncr->setIpAddress(element);
+
+ element = ncr->getElement("dhcid", element_map);
+ ncr->setDhcid(element);
+
+ element = ncr->getElement("lease_expires_on", element_map);
+ ncr->setLeaseExpiresOn(element);
+
+ element = ncr->getElement("lease_length", element_map);
+ ncr->setLeaseLength(element);
+
+ // All members were in the Element set and were correct lexically. Now
+ // validate the overall content semantically. This will throw an
+ // NcrMessageError if anything is amiss.
+ ncr->validateContent();
+
+ // Everything is valid, return the new instance.
+ return (ncr);
+}
+
+std::string
+NameChangeRequest::toJSON() const {
+ // Create a JSON string of this request's contents. Note that this method
+ // does NOT use the isc::data library as generating the output is straight
+ // forward.
+ std::ostringstream stream;
+
+ stream << "{\"change_type\":" << getChangeType() << ","
+ << "\"forward_change\":"
+ << (isForwardChange() ? "true" : "false") << ","
+ << "\"reverse_change\":"
+ << (isReverseChange() ? "true" : "false") << ","
+ << "\"fqdn\":\"" << getFqdn() << "\","
+ << "\"ip_address\":\"" << getIpAddress() << "\","
+ << "\"dhcid\":\"" << getDhcid().toStr() << "\","
+ << "\"lease_expires_on\":\"" << getLeaseExpiresOnStr() << "\","
+ << "\"lease_length\":" << getLeaseLength() << "}";
+
+ return (stream.str());
+}
+
+
+void
+NameChangeRequest::validateContent() {
+ //@todo This is an initial implementation which provides a minimal amount
+ // of validation. FQDN and DHCID members are all currently
+ // strings, these may be replaced with richer classes.
+ if (fqdn_ == "") {
+ isc_throw(NcrMessageError, "FQDN cannot be blank");
+ }
+
+ // Validate the DHCID.
+ if (dhcid_.getBytes().size() == 0) {
+ isc_throw(NcrMessageError, "DHCID cannot be blank");
+ }
+
+ // Ensure the request specifies at least one direction to update.
+ if (!forward_change_ && !reverse_change_) {
+ isc_throw(NcrMessageError,
+ "Invalid Request, forward and reverse flags are both false");
+ }
+}
+
+isc::data::ConstElementPtr
+NameChangeRequest::getElement(const std::string& name,
+ const ElementMap& element_map) const {
+ // Look for "name" in the element map.
+ ElementMap::const_iterator it = element_map.find(name);
+ if (it == element_map.end()) {
+ // Didn't find the element, so throw.
+ isc_throw(NcrMessageError,
+ "NameChangeRequest value missing for: " << name );
+ }
+
+ // Found the element, return it.
+ return (it->second);
+}
+
+void
+NameChangeRequest::setChangeType(const NameChangeType value) {
+ change_type_ = value;
+}
+
+
+void
+NameChangeRequest::setChangeType(isc::data::ConstElementPtr element) {
+ long raw_value = -1;
+ try {
+ // Get the element's integer value.
+ raw_value = element->intValue();
+ } catch (isc::data::TypeError& ex) {
+ // We expect a integer Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for change_type: " << ex.what());
+ }
+
+ if ((raw_value != CHG_ADD) && (raw_value != CHG_REMOVE)) {
+ // Value is not a valid change type.
+ isc_throw(NcrMessageError,
+ "Invalid data value for change_type: " << raw_value);
+ }
+
+ // Good to go, make the assignment.
+ setChangeType(static_cast<NameChangeType>(raw_value));
+}
+
+void
+NameChangeRequest::setForwardChange(const bool value) {
+ forward_change_ = value;
+}
+
+void
+NameChangeRequest::setForwardChange(isc::data::ConstElementPtr element) {
+ bool value;
+ try {
+ // Get the element's boolean value.
+ value = element->boolValue();
+ } catch (isc::data::TypeError& ex) {
+ // We expect a boolean Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for forward_change :" << ex.what());
+ }
+
+ // Good to go, make the assignment.
+ setForwardChange(value);
+}
+
+void
+NameChangeRequest::setReverseChange(const bool value) {
+ reverse_change_ = value;
+}
+
+void
+NameChangeRequest::setReverseChange(isc::data::ConstElementPtr element) {
+ bool value;
+ try {
+ // Get the element's boolean value.
+ value = element->boolValue();
+ } catch (isc::data::TypeError& ex) {
+ // We expect a boolean Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for reverse_change :" << ex.what());
+ }
+
+ // Good to go, make the assignment.
+ setReverseChange(value);
+}
+
+
+void
+NameChangeRequest::setFqdn(isc::data::ConstElementPtr element) {
+ setFqdn(element->stringValue());
+}
+
+void
+NameChangeRequest::setFqdn(const std::string& value) {
+ try {
+ dns::Name tmp(value);
+ fqdn_ = tmp.toText();
+ } catch (const std::exception& ex) {
+ isc_throw(NcrMessageError,
+ "Invalid FQDN value: " << value << ", reason:" << ex.what());
+ }
+}
+
+void
+NameChangeRequest::setIpAddress(const std::string& value) {
+ // Validate IP Address.
+ try {
+ ip_io_address_ = isc::asiolink::IOAddress(value);
+ } catch (const isc::asiolink::IOError& ex) {
+ isc_throw(NcrMessageError,
+ "Invalid ip address string for ip_address: " << value);
+ }
+}
+
+void
+NameChangeRequest::setIpAddress(isc::data::ConstElementPtr element) {
+ setIpAddress(element->stringValue());
+}
+
+
+void
+NameChangeRequest::setDhcid(const std::string& value) {
+ dhcid_.fromStr(value);
+}
+
+void
+NameChangeRequest::setDhcid(isc::data::ConstElementPtr element) {
+ setDhcid(element->stringValue());
+}
+
+std::string
+NameChangeRequest::getLeaseExpiresOnStr() const {
+ return (isc::util::timeToText64(lease_expires_on_));
+}
+
+void
+NameChangeRequest::setLeaseExpiresOn(const std::string& value) {
+ try {
+ lease_expires_on_ = isc::util::timeFromText64(value);
+ } catch(...) {
+ // We were given an invalid string, so throw.
+ isc_throw(NcrMessageError,
+ "Invalid date-time string: [" << value << "]");
+ }
+
+}
+
+void NameChangeRequest::setLeaseExpiresOn(isc::data::ConstElementPtr element) {
+ // Pull out the string value and pass it into the string setter.
+ setLeaseExpiresOn(element->stringValue());
+}
+
+void
+NameChangeRequest::setLeaseLength(const uint32_t value) {
+ lease_length_ = value;
+}
+
+void
+NameChangeRequest::setLeaseLength(isc::data::ConstElementPtr element) {
+ long value = -1;
+ try {
+ // Get the element's integer value.
+ value = element->intValue();
+ } catch (isc::data::TypeError& ex) {
+ // We expect a integer Element type, don't have one.
+ isc_throw(NcrMessageError,
+ "Wrong data type for lease_length: " << ex.what());
+ }
+
+ // Make sure we the range is correct and value is positive.
+ if (value > std::numeric_limits<uint32_t>::max()) {
+ isc_throw(NcrMessageError, "lease_length value " << value <<
+ "is too large for unsigned 32-bit integer.");
+ }
+ if (value < 0) {
+ isc_throw(NcrMessageError, "lease_length value " << value <<
+ "is negative. It must greater than or equal to zero ");
+ }
+
+ // Good to go, make the assignment.
+ setLeaseLength(static_cast<uint32_t>(value));
+}
+
+void
+NameChangeRequest::setStatus(const NameChangeStatus value) {
+ status_ = value;
+}
+
+std::string
+NameChangeRequest::toText() const {
+ std::ostringstream stream;
+
+ stream << "Type: " << static_cast<int>(change_type_) << " (";
+ switch (change_type_) {
+ case CHG_ADD:
+ stream << "CHG_ADD)\n";
+ break;
+ case CHG_REMOVE:
+ stream << "CHG_REMOVE)\n";
+ break;
+ default:
+ // Shouldn't be possible.
+ stream << "Invalid Value\n";
+ }
+
+ stream << "Forward Change: " << (forward_change_ ? "yes" : "no")
+ << std::endl
+ << "Reverse Change: " << (reverse_change_ ? "yes" : "no")
+ << std::endl
+ << "FQDN: [" << fqdn_ << "]" << std::endl
+ << "IP Address: [" << ip_io_address_ << "]" << std::endl
+ << "DHCID: [" << dhcid_.toStr() << "]" << std::endl
+ << "Lease Expires On: " << getLeaseExpiresOnStr() << std::endl
+ << "Lease Length: " << lease_length_ << std::endl;
+
+ return (stream.str());
+}
+
+bool
+NameChangeRequest::operator == (const NameChangeRequest& other) {
+ return ((change_type_ == other.change_type_) &&
+ (forward_change_ == other.forward_change_) &&
+ (reverse_change_ == other.reverse_change_) &&
+ (fqdn_ == other.fqdn_) &&
+ (ip_io_address_ == other.ip_io_address_) &&
+ (dhcid_ == other.dhcid_) &&
+ (lease_expires_on_ == other.lease_expires_on_) &&
+ (lease_length_ == other.lease_length_));
+}
+
+bool
+NameChangeRequest::operator != (const NameChangeRequest& other) {
+ return (!(*this == other));
+}
+
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
diff --git a/src/lib/dhcp_ddns/ncr_msg.h b/src/lib/dhcp_ddns/ncr_msg.h
new file mode 100644
index 0000000..5dab9e8
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_msg.h
@@ -0,0 +1,638 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NCR_MSG_H
+#define NCR_MSG_H
+
+/// @file ncr_msg.h
+/// @brief This file provides the classes needed to embody, compose, and
+/// decompose DNS update requests that are sent by DHCP-DDNS clients to
+/// DHCP-DDNS. These requests are referred to as NameChangeRequests.
+
+#include <cc/data.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <dns/name.h>
+#include <exceptions/exceptions.h>
+#include <util/buffer.h>
+#include <util/encode/hex.h>
+#include <util/time_utilities.h>
+
+#include <time.h>
+#include <string>
+
+namespace isc {
+namespace dhcp_ddns {
+
+/// @brief Exception thrown when NameChangeRequest marshalling error occurs.
+class NcrMessageError : public isc::Exception {
+public:
+ NcrMessageError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+/// @brief Exception thrown when there is an error occured during computation
+/// of the DHCID.
+class DhcidRdataComputeError : public isc::Exception {
+public:
+ DhcidRdataComputeError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+
+/// @brief Defines the types of DNS updates that can be requested.
+enum NameChangeType {
+ CHG_ADD,
+ CHG_REMOVE
+};
+
+/// @brief Defines the runtime processing status values for requests.
+enum NameChangeStatus {
+ ST_NEW,
+ ST_PENDING,
+ ST_COMPLETED,
+ ST_FAILED
+};
+
+/// @brief Defines the list of data wire formats supported.
+enum NameChangeFormat {
+ FMT_JSON
+};
+
+/// @brief Function which converts labels to NameChangeFormat enum values.
+///
+/// @param format_str text to convert to an enum.
+/// Valid string values: "JSON"
+///
+/// @return NameChangeFormat value which maps to the given string.
+///
+/// @throw isc::BadValue if given a string value which does not map to an
+/// enum value.
+extern NameChangeFormat stringToNcrFormat(const std::string& fmt_str);
+
+/// @brief Function which converts NameChangeFormat enums to text labels.
+///
+/// @param format enum value to convert to label
+///
+/// @return std:string containing the text label if the value is valid, or
+/// "UNKNOWN" if not.
+extern std::string ncrFormatToString(NameChangeFormat format);
+
+/// @brief Container class for handling the DHCID value within a
+/// NameChangeRequest. It provides conversion to and from string for JSON
+/// formatting, but stores the data internally as unsigned bytes.
+class D2Dhcid {
+public:
+ /// @brief Default constructor
+ D2Dhcid();
+
+ /// @brief Constructor - Creates a new instance, populated by converting
+ /// a given string of digits into an array of unsigned bytes.
+ ///
+ /// @param data is a string of hexadecimal digits. The format is simply
+ /// a contiguous stream of digits, with no delimiters. For example a string
+ /// containing "14A3" converts to a byte array containing: 0x14, 0xA3.
+ ///
+ /// @throw NcrMessageError if the input data contains non-digits
+ /// or there is an odd number of digits.
+ D2Dhcid(const std::string& data);
+
+ /// @brief Constructor, creates an instance of the @c D2Dhcid from the
+ /// HW address.
+ ///
+ /// @param hwaddr A pointer to the object encapsulating HW address.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ D2Dhcid(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Constructor, creates an instance of the @c D2Dhcid from the
+ /// client identifier carried in the Client Identifier option.
+ ///
+ /// @param clientid_data Holds the raw bytes representing client identifier.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ D2Dhcid(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Constructor, creates an instance of the @c D2Dhcid from the
+ /// @c isc::dhcp::DUID.
+ ///
+ /// @param duid An object representing DUID.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ D2Dhcid(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Returns the DHCID value as a string of hexadecimal digits.
+ ///
+ /// @return a string containing a contiguous stream of digits.
+ std::string toStr() const;
+
+ /// @brief Sets the DHCID value based on the given string.
+ ///
+ /// @param data is a string of hexadecimal digits. The format is simply
+ /// a contiguous stream of digits, with no delimiters. For example a string
+ /// containing "14A3" converts to a byte array containing: 0x14, 0xA3.
+ ///
+ /// @throw NcrMessageError if the input data contains non-digits
+ /// or there is an odd number of digits.
+ void fromStr(const std::string& data);
+
+ /// @brief Sets the DHCID value based on the Client Identifier.
+ ///
+ /// @param clientid_data Holds the raw bytes representing client identifier.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ void fromClientId(const std::vector<uint8_t>& clientid_data,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Sets the DHCID value based on the DUID and FQDN.
+ ///
+ /// This function requires that the FQDN conforms to the section 3.5
+ /// of the RFC4701, which says that the FQDN must be in lowercase.
+ /// This function doesn't validate if it really converted.
+ ///
+ /// @param duid A @c isc::dhcp::DUID object encapsulating DUID.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ void fromDUID(const isc::dhcp::DUID& duid,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Sets the DHCID value based on the HW address and FQDN.
+ ///
+ /// @param hwaddr A pointer to the object encapsulating HW address.
+ /// @param wire_fqdn A on-wire canonical representation of the FQDN.
+ void fromHWAddr(const isc::dhcp::HWAddrPtr& hwaddr,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Returns a reference to the DHCID byte vector.
+ ///
+ /// @return a reference to the vector.
+ const std::vector<uint8_t>& getBytes() const {
+ return (bytes_);
+ }
+
+ /// @brief Compares two D2Dhcids for equality
+ bool operator==(const D2Dhcid& other) const {
+ return (this->bytes_ == other.bytes_);
+ }
+
+ /// @brief Compares two D2Dhcids for inequality
+ bool operator!=(const D2Dhcid& other) const {
+ return (this->bytes_ != other.bytes_);
+ }
+
+ /// @brief Compares two D2Dhcids lexcially
+ bool operator<(const D2Dhcid& other) const {
+ return (this->bytes_ < other.bytes_);
+ }
+
+private:
+
+ /// @brief Creates the DHCID using specified indetifier.
+ ///
+ /// This function creates the DHCID RDATA as specified in RFC4701,
+ /// section 3.5.
+ ///
+ /// @param identifier_type is a less significant byte of the identifier-type
+ /// defined in RFC4701.
+ /// @param identifier_data A buffer holding client identifier raw data -
+ /// e.g. DUID, data carried in the Client Identifier option or client's
+ /// HW address.
+ /// @param A on-wire canonical representation of the FQDN.
+ void createDigest(const uint8_t identifier_type,
+ const std::vector<uint8_t>& identifier_data,
+ const std::vector<uint8_t>& wire_fqdn);
+
+ /// @brief Storage for the DHCID value in unsigned bytes.
+ std::vector<uint8_t> bytes_;
+};
+
+std::ostream&
+operator<<(std::ostream& os, const D2Dhcid& dhcid);
+
+class NameChangeRequest;
+/// @brief Defines a pointer to a NameChangeRequest.
+typedef boost::shared_ptr<NameChangeRequest> NameChangeRequestPtr;
+
+/// @brief Defines a map of Elements, keyed by their string name.
+typedef std::map<std::string, isc::data::ConstElementPtr> ElementMap;
+
+/// @brief Represents a DHCP-DDNS client request.
+/// This class is used by DHCP-DDNS clients (e.g. DHCP4, DHCP6) to
+/// request DNS updates. Each message contains a single DNS change (either an
+/// add/update or a remove) for a single FQDN. It provides marshalling services
+/// for moving instances to and from the wire. Currently, the only format
+/// supported is JSON, however the class provides an interface such that other
+/// formats can be readily supported.
+class NameChangeRequest {
+public:
+ /// @brief Default Constructor.
+ ///
+ /// @todo Currently, fromWire makes use of the ability to create an empty
+ /// NameChangeRequest and then builds it bit by bit. This means that it
+ /// is technically possible to create one and attempt to use in ways
+ /// other than intended and its invalid content may or may not be handled
+ /// gracefully by consuming code. It might be wise to revisit this
+ /// structuring such that we do not use a default constructor and only
+ /// allow valid instantiations.
+ NameChangeRequest();
+
+ /// @brief Constructor. Full constructor, which provides parameters for
+ /// all of the class members, except status.
+ ///
+ /// @param change_type the type of change (Add or Update)
+ /// @param forward_change indicates if this change should be sent to forward
+ /// DNS servers.
+ /// @param reverse_change indicates if this change should be sent to reverse
+ /// DNS servers.
+ /// @param fqdn the domain name whose pointer record(s) should be
+ /// updated.
+ /// @param ip_address the ip address leased to the given FQDN.
+ /// @param dhcid the lease client's unique DHCID.
+ /// @param lease_expires_on a timestamp containing the date/time the lease
+ /// expires.
+ /// @param lease_length the amount of time in seconds for which the
+ /// lease is valid (TTL).
+ NameChangeRequest(const NameChangeType change_type,
+ const bool forward_change, const bool reverse_change,
+ const std::string& fqdn, const std::string& ip_address,
+ const D2Dhcid& dhcid,
+ const uint64_t lease_expires_on,
+ const uint32_t lease_length);
+
+ /// @brief Static method for creating a NameChangeRequest from a
+ /// buffer containing a marshalled request in a given format.
+ ///
+ /// When the format is:
+ ///
+ /// JSON: The buffer is expected to contain a two byte unsigned integer
+ /// which specified the length of the JSON text; followed by the JSON
+ /// text itself. This method attempts to extract "length" characters
+ /// from the buffer. This data is used to create a character string that
+ /// is than treated as JSON which is then parsed into the data needed
+ /// to create a request instance.
+ ///
+ /// (NOTE currently only JSON is supported.)
+ ///
+ /// @param format indicates the data format to use
+ /// @param buffer is the input buffer containing the marshalled request
+ ///
+ /// @return a pointer to the new NameChangeRequest
+ ///
+ /// @throw NcrMessageError if an error occurs creating new
+ /// request.
+ static NameChangeRequestPtr fromFormat(const NameChangeFormat format,
+ isc::util::InputBuffer& buffer);
+
+ /// @brief Instance method for marshalling the contents of the request
+ /// into the given buffer in the given format.
+ ///
+ /// When the format is:
+ ///
+ /// JSON: Upon completion, the buffer will contain a two byte unsigned
+ /// integer which specifies the length of the JSON text; followed by the
+ /// JSON text itself. The JSON text contains the names and values for all
+ /// the request data needed to reassemble the request on the receiving
+ /// end. The JSON text in the buffer is NOT null-terminated.
+ ///
+ /// (NOTE currently only JSON is supported.)
+ ///
+ /// @param format indicates the data format to use
+ /// @param buffer is the output buffer to which the request should be
+ /// marshalled.
+ void toFormat(const NameChangeFormat format,
+ isc::util::OutputBuffer& buffer) const;
+
+ /// @brief Static method for creating a NameChangeRequest from a
+ /// string containing a JSON rendition of a request.
+ ///
+ /// @param json is a string containing the JSON text
+ ///
+ /// @return a pointer to the new NameChangeRequest
+ ///
+ /// @throw NcrMessageError if an error occurs creating new request.
+ static NameChangeRequestPtr fromJSON(const std::string& json);
+
+ /// @brief Instance method for marshalling the contents of the request
+ /// into a string of JSON text.
+ ///
+ /// @return a string containing the JSON rendition of the request
+ std::string toJSON() const;
+
+ /// @brief Validates the content of a populated request. This method is
+ /// used by both the full constructor and from-wire marshalling to ensure
+ /// that the request is content valid. Currently it enforces the
+ /// following rules:
+ ///
+ /// - FQDN must not be blank.
+ /// - The IP address must be a valid address.
+ /// - The DHCID must not be blank.
+ /// - The lease expiration date must be a valid date/time.
+ /// - That at least one of the two direction flags, forward change and
+ /// reverse change is true.
+ ///
+ /// @todo This is an initial implementation which provides a minimal amount
+ /// of validation. FQDN, DHCID, and IP Address members are all currently
+ /// strings, these may be replaced with richer classes.
+ ///
+ /// @throw NcrMessageError if the request content violates any
+ /// of the validation rules.
+ void validateContent();
+
+ /// @brief Fetches the request change type.
+ ///
+ /// @return the change type
+ NameChangeType getChangeType() const {
+ return (change_type_);
+ }
+
+ /// @brief Sets the change type to the given value.
+ ///
+ /// @param value is the NameChangeType value to assign to the request.
+ void setChangeType(const NameChangeType value);
+
+ /// @brief Sets the change type to the value of the given Element.
+ ///
+ /// @param element is an integer Element containing the change type value.
+ ///
+ /// @throw NcrMessageError if the element is not an integer
+ /// Element or contains an invalid value.
+ void setChangeType(isc::data::ConstElementPtr element);
+
+ /// @brief Checks forward change flag.
+ ///
+ /// @return a true if the forward change flag is true.
+ bool isForwardChange() const {
+ return (forward_change_);
+ }
+
+ /// @brief Sets the forward change flag to the given value.
+ ///
+ /// @param value contains the new value to assign to the forward change
+ /// flag
+ void setForwardChange(const bool value);
+
+ /// @brief Sets the forward change flag to the value of the given Element.
+ ///
+ /// @param element is a boolean Element containing the forward change flag
+ /// value.
+ ///
+ /// @throw NcrMessageError if the element is not a boolean
+ /// Element
+ void setForwardChange(isc::data::ConstElementPtr element);
+
+ /// @brief Checks reverse change flag.
+ ///
+ /// @return a true if the reverse change flag is true.
+ bool isReverseChange() const {
+ return (reverse_change_);
+ }
+
+ /// @brief Sets the reverse change flag to the given value.
+ ///
+ /// @param value contains the new value to assign to the reverse change
+ /// flag
+ void setReverseChange(const bool value);
+
+ /// @brief Sets the reverse change flag to the value of the given Element.
+ ///
+ /// @param element is a boolean Element containing the reverse change flag
+ /// value.
+ ///
+ /// @throw NcrMessageError if the element is not a boolean
+ /// Element
+ void setReverseChange(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request FQDN
+ ///
+ /// @return a string containing the FQDN
+ const std::string getFqdn() const {
+ return (fqdn_);
+ }
+
+ /// @brief Sets the FQDN to the given value.
+ ///
+ /// @param value contains the new value to assign to the FQDN
+ void setFqdn(const std::string& value);
+
+ /// @brief Sets the FQDN to the value of the given Element.
+ ///
+ /// @param element is a string Element containing the FQDN
+ ///
+ /// @throw NcrMessageError if the element is not a string
+ /// Element
+ void setFqdn(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request IP address string.
+ ///
+ /// @return a string containing the IP address
+ std::string getIpAddress() const {
+ return (ip_io_address_.toText());
+ }
+
+ /// @brief Fetches the request IP address as an IOAddress.
+ ///
+ /// @return a asiolink::IOAddress containing the IP address
+ const asiolink::IOAddress& getIpIoAddress() const {
+ return (ip_io_address_);
+ }
+
+ /// @brief Returns true if the lease address is a IPv4 lease.
+ ///
+ /// @return boolean true if the lease address family is AF_INET.
+ bool isV4 () const {
+ return (ip_io_address_.isV4());
+ }
+
+ /// @brief Returns true if the lease address is a IPv6 lease.
+ ///
+ /// @return boolean true if the lease address family is AF_INET6.
+ bool isV6 () const {
+ return (ip_io_address_.isV6());
+ }
+
+ /// @brief Sets the IP address to the given value.
+ ///
+ /// @param value contains the new value to assign to the IP address
+ void setIpAddress(const std::string& value);
+
+ /// @brief Sets the IP address to the value of the given Element.
+ ///
+ /// @param element is a string Element containing the IP address
+ ///
+ /// @throw NcrMessageError if the element is not a string
+ /// Element
+ void setIpAddress(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request DHCID
+ ///
+ /// @return a reference to the request's D2Dhcid
+ const D2Dhcid& getDhcid() const {
+ return (dhcid_);
+ }
+
+ /// @brief Sets the DHCID based on the given string value.
+ ///
+ /// @param value is a string of hexadecimal digits. The format is simply
+ /// a contiguous stream of digits, with no delimiters. For example a string
+ /// containing "14A3" converts to a byte array containing: 0x14, 0xA3.
+ ///
+ /// @throw NcrMessageError if the input data contains non-digits
+ /// or there is an odd number of digits.
+ void setDhcid(const std::string& value);
+
+ /// @brief Sets the DHCID based on the value of the given Element.
+ ///
+ /// @param element is a string Element containing the string of hexadecimal
+ /// digits. (See setDhcid(std::string&) above.)
+ ///
+ /// @throw NcrMessageError if the input data contains non-digits
+ /// or there is an odd number of digits.
+ void setDhcid(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request lease expiration
+ ///
+ /// @return the lease expiration as the number of seconds since
+ /// the (00:00:00 January 1, 1970)
+ uint64_t getLeaseExpiresOn() const {
+ return (lease_expires_on_);
+ }
+
+ /// @brief Fetches the request lease expiration as string.
+ ///
+ /// The format of the string returned is:
+ ///
+ /// YYYYMMDDHHmmSS
+ ///
+ /// Example: 18:54:54 June 26, 2013 would be: 20130626185455
+ /// NOTE This is always UTC time.
+ ///
+ /// @return a ISO date-time string of the lease expiration.
+ std::string getLeaseExpiresOnStr() const;
+
+ /// @brief Sets the lease expiration based on the given string.
+ ///
+ /// @param value is an date-time string from which to set the
+ /// lease expiration. The format of the input is:
+ ///
+ /// YYYYMMDDHHmmSS
+ ///
+ /// Example: 18:54:54 June 26, 2013 would be: 20130626185455
+ /// NOTE This is always UTC time.
+ ///
+ /// @throw NcrMessageError if the ISO string is invalid.
+ void setLeaseExpiresOn(const std::string& value);
+
+ /// @brief Sets the lease expiration based on the given Element.
+ ///
+ /// @param element is string Element containing a date-time string.
+ ///
+ /// @throw NcrMessageError if the element is not a string
+ /// Element, or if the element value is an invalid date-time string.
+ void setLeaseExpiresOn(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request lease length.
+ ///
+ /// @return an integer containing the lease length
+ uint32_t getLeaseLength() const {
+ return (lease_length_);
+ }
+
+ /// @brief Sets the lease length to the given value.
+ ///
+ /// @param value contains the new value to assign to the lease length
+ void setLeaseLength(const uint32_t value);
+
+ /// @brief Sets the lease length to the value of the given Element.
+ ///
+ /// @param element is a integer Element containing the lease length
+ ///
+ /// @throw NcrMessageError if the element is not a string
+ /// Element
+ void setLeaseLength(isc::data::ConstElementPtr element);
+
+ /// @brief Fetches the request status.
+ ///
+ /// @return the request status as a NameChangeStatus
+ NameChangeStatus getStatus() const {
+ return (status_);
+ }
+
+ /// @brief Sets the request status to the given value.
+ ///
+ /// @param value contains the new value to assign to request status
+ void setStatus(const NameChangeStatus value);
+
+ /// @brief Given a name, finds and returns an element from a map of
+ /// elements.
+ ///
+ /// @param name is the name of the desired element
+ /// @param element_map is the map of elements to search
+ ///
+ /// @return a pointer to the element if located
+ /// @throw NcrMessageError if the element cannot be found within
+ /// the map
+ isc::data::ConstElementPtr getElement(const std::string& name,
+ const ElementMap& element_map) const;
+
+ /// @brief Returns a text rendition of the contents of the request.
+ /// This method is primarily for logging purposes.
+ ///
+ /// @return a string containing the text.
+ std::string toText() const;
+
+ bool operator == (const NameChangeRequest& b);
+ bool operator != (const NameChangeRequest& b);
+
+private:
+ /// @brief Denotes the type of this change as either an Add or a Remove.
+ NameChangeType change_type_;
+
+ /// @brief Indicates if this change should sent to forward DNS servers.
+ bool forward_change_;
+
+ /// @brief Indicates if this change should sent to reverse DNS servers.
+ bool reverse_change_;
+
+ /// @brief The domain name whose DNS entry(ies) are to be updated.
+ /// @todo Currently, this is a std::string but may be replaced with
+ /// dns::Name which provides additional validation and domain name
+ /// manipulation.
+ std::string fqdn_;
+
+ /// @brief The ip address leased to the FQDN as an IOAddress.
+ ///
+ /// The lease address is used in many places, sometimes as a string
+ /// and sometimes as an IOAddress. To avoid converting back and forth
+ /// continually over the life span of an NCR, we do it once when the
+ /// ip address is actually set.
+ asiolink::IOAddress ip_io_address_;
+
+ /// @brief The lease client's unique DHCID.
+ /// @todo Currently, this is uses D2Dhcid it but may be replaced with
+ /// dns::DHCID which provides additional validation.
+ D2Dhcid dhcid_;
+
+ /// @brief The date-time the lease expires.
+ uint64_t lease_expires_on_;
+
+ /// @brief The amount of time in seconds for which the lease is valid (TTL).
+ uint32_t lease_length_;
+
+ /// @brief The processing status of the request. Used internally.
+ NameChangeStatus status_;
+};
+
+
+}; // end of isc::dhcp_ddns namespace
+}; // end of isc namespace
+
+#endif
diff --git a/src/lib/dhcp_ddns/ncr_udp.cc b/src/lib/dhcp_ddns/ncr_udp.cc
new file mode 100644
index 0000000..9e83f7c
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_udp.cc
@@ -0,0 +1,328 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp_ddns/dhcp_ddns_log.h>
+#include <dhcp_ddns/ncr_udp.h>
+
+#include <asio/ip/udp.hpp>
+#include <asio/error_code.hpp>
+#include <boost/bind.hpp>
+
+namespace isc {
+namespace dhcp_ddns {
+
+//*************************** UDPCallback ***********************
+UDPCallback::UDPCallback (RawBufferPtr& buffer, const size_t buf_size,
+ UDPEndpointPtr& data_source,
+ const UDPCompletionHandler& handler)
+ : handler_(handler), data_(new Data(buffer, buf_size, data_source)) {
+ if (handler.empty()) {
+ isc_throw(NcrUDPError, "UDPCallback - handler can't be null");
+ }
+
+ if (!buffer) {
+ isc_throw(NcrUDPError, "UDPCallback - buffer can't be null");
+ }
+}
+
+void
+UDPCallback::operator ()(const asio::error_code error_code,
+ const size_t bytes_transferred) {
+
+ // Save the result state and number of bytes transferred.
+ setErrorCode(error_code);
+ setBytesTransferred(bytes_transferred);
+
+ // Invoke the NameChangeRequest layer completion handler.
+ // First argument is a boolean indicating success or failure.
+ // The second is a pointer to "this" callback object. By passing
+ // ourself in, we make all of the service related data available
+ // to the completion handler.
+ handler_(!error_code, this);
+}
+
+void
+UDPCallback::putData(const uint8_t* src, size_t len) {
+ if (!src) {
+ isc_throw(NcrUDPError, "UDPCallback putData, data source is NULL");
+ }
+
+ if (len > data_->buf_size_) {
+ isc_throw(NcrUDPError, "UDPCallback putData, data length too large");
+ }
+
+ memcpy (data_->buffer_.get(), src, len);
+ data_->put_len_ = len;
+}
+
+
+//*************************** NameChangeUDPListener ***********************
+NameChangeUDPListener::
+NameChangeUDPListener(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port, const NameChangeFormat format,
+ RequestReceiveHandler& ncr_recv_handler,
+ const bool reuse_address)
+ : NameChangeListener(ncr_recv_handler), ip_address_(ip_address),
+ port_(port), format_(format), reuse_address_(reuse_address) {
+ // Instantiate the receive callback. This gets passed into each receive.
+ // Note that the callback constructor is passed an instance method
+ // pointer to our completion handler method, receiveCompletionHandler.
+ RawBufferPtr buffer(new uint8_t[RECV_BUF_MAX]);
+ UDPEndpointPtr data_source(new asiolink::UDPEndpoint());
+ recv_callback_.reset(new
+ UDPCallback(buffer, RECV_BUF_MAX, data_source,
+ boost::bind(&NameChangeUDPListener::
+ receiveCompletionHandler, this, _1, _2)));
+}
+
+NameChangeUDPListener::~NameChangeUDPListener() {
+ // Clean up.
+ stopListening();
+}
+
+void
+NameChangeUDPListener::open(isc::asiolink::IOService& io_service) {
+ // create our endpoint and bind the the low level socket to it.
+ isc::asiolink::UDPEndpoint endpoint(ip_address_.getAddress(), port_);
+
+ // Create the low level socket.
+ try {
+ asio_socket_.reset(new asio::ip::udp::
+ socket(io_service.get_io_service(),
+ (ip_address_.isV4() ? asio::ip::udp::v4() :
+ asio::ip::udp::v6())));
+
+ // Set the socket option to reuse addresses if it is enabled.
+ if (reuse_address_) {
+ asio_socket_->set_option(asio::socket_base::reuse_address(true));
+ }
+
+ // Bind the low level socket to our endpoint.
+ asio_socket_->bind(endpoint.getASIOEndpoint());
+ } catch (asio::system_error& ex) {
+ asio_socket_.reset();
+ isc_throw (NcrUDPError, ex.code().message());
+ }
+
+ // Create the asiolink socket from the low level socket.
+ socket_.reset(new NameChangeUDPSocket(*asio_socket_));
+}
+
+
+void
+NameChangeUDPListener::doReceive() {
+ // Call the socket's asychronous receiving, passing ourself in as callback.
+ RawBufferPtr recv_buffer = recv_callback_->getBuffer();
+ socket_->asyncReceive(recv_buffer.get(), recv_callback_->getBufferSize(),
+ 0, recv_callback_->getDataSource().get(),
+ *recv_callback_);
+}
+
+void
+NameChangeUDPListener::close() {
+ // Whether we think we are listening or not, make sure we aren't.
+ // Since we are managing our own socket, we need to close it ourselves.
+ // NOTE that if there is a pending receive, it will be canceled, which
+ // WILL generate an invocation of the callback with error code of
+ // "operation aborted".
+ if (asio_socket_) {
+ if (asio_socket_->is_open()) {
+ try {
+ asio_socket_->close();
+ } catch (asio::system_error& ex) {
+ // It is really unlikely that this will occur.
+ // If we do reopen later it will be with a new socket
+ // instance. Repackage exception as one that is conformant
+ // with the interface.
+ isc_throw (NcrUDPError, ex.code().message());
+ }
+ }
+
+ asio_socket_.reset();
+ }
+
+ socket_.reset();
+}
+
+void
+NameChangeUDPListener::receiveCompletionHandler(const bool successful,
+ const UDPCallback *callback) {
+ NameChangeRequestPtr ncr;
+ Result result = SUCCESS;
+
+ if (successful) {
+ // Make an InputBuffer from our internal array
+ isc::util::InputBuffer input_buffer(callback->getData(),
+ callback->getBytesTransferred());
+
+ try {
+ ncr = NameChangeRequest::fromFormat(format_, input_buffer);
+ } catch (const NcrMessageError& ex) {
+ // log it and go back to listening
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_INVALID_NCR).arg(ex.what());
+
+ // Queue up the next recieve.
+ // NOTE: We must call the base class, NEVER doReceive
+ receiveNext();
+ return;
+ }
+ } else {
+ asio::error_code error_code = callback->getErrorCode();
+ if (error_code.value() == asio::error::operation_aborted) {
+ LOG_INFO(dhcp_ddns_logger, DHCP_DDNS_NCR_UDP_RECV_CANCELED)
+ .arg(error_code.message());
+ result = STOPPED;
+ } else {
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_UDP_RECV_ERROR)
+ .arg(error_code.message());
+ result = ERROR;
+ }
+ }
+
+ // Call the application's registered request receive handler.
+ invokeRecvHandler(result, ncr);
+}
+
+
+//*************************** NameChangeUDPSender ***********************
+
+NameChangeUDPSender::
+NameChangeUDPSender(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port,
+ const isc::asiolink::IOAddress& server_address,
+ const uint32_t server_port, const NameChangeFormat format,
+ RequestSendHandler& ncr_send_handler,
+ const size_t send_que_max, const bool reuse_address)
+ : NameChangeSender(ncr_send_handler, send_que_max),
+ ip_address_(ip_address), port_(port), server_address_(server_address),
+ server_port_(server_port), format_(format),
+ reuse_address_(reuse_address) {
+ // Instantiate the send callback. This gets passed into each send.
+ // Note that the callback constructor is passed the an instance method
+ // pointer to our completion handler, sendCompletionHandler.
+ RawBufferPtr buffer(new uint8_t[SEND_BUF_MAX]);
+ UDPEndpointPtr data_source(new asiolink::UDPEndpoint());
+ send_callback_.reset(new UDPCallback(buffer, SEND_BUF_MAX, data_source,
+ boost::bind(&NameChangeUDPSender::
+ sendCompletionHandler, this,
+ _1, _2)));
+}
+
+NameChangeUDPSender::~NameChangeUDPSender() {
+ // Clean up.
+ stopSending();
+}
+
+void
+NameChangeUDPSender::open(isc::asiolink::IOService& io_service) {
+ // create our endpoint and bind the the low level socket to it.
+ isc::asiolink::UDPEndpoint endpoint(ip_address_.getAddress(), port_);
+
+ // Create the low level socket.
+ try {
+ asio_socket_.reset(new asio::ip::udp::
+ socket(io_service.get_io_service(),
+ (ip_address_.isV4() ? asio::ip::udp::v4() :
+ asio::ip::udp::v6())));
+
+ // Set the socket option to reuse addresses if it is enabled.
+ if (reuse_address_) {
+ asio_socket_->set_option(asio::socket_base::reuse_address(true));
+ }
+
+ // Bind the low leve socket to our endpoint.
+ asio_socket_->bind(endpoint.getASIOEndpoint());
+ } catch (asio::system_error& ex) {
+ isc_throw (NcrUDPError, ex.code().message());
+ }
+
+ // Create the asiolink socket from the low level socket.
+ socket_.reset(new NameChangeUDPSocket(*asio_socket_));
+
+ // Create the server endpoint
+ server_endpoint_.reset(new isc::asiolink::
+ UDPEndpoint(server_address_.getAddress(),
+ server_port_));
+
+ send_callback_->setDataSource(server_endpoint_);
+}
+
+void
+NameChangeUDPSender::close() {
+ // Whether we think we are sending or not, make sure we aren't.
+ // Since we are managing our own socket, we need to close it ourselves.
+ // NOTE that if there is a pending send, it will be canceled, which
+ // WILL generate an invocation of the callback with error code of
+ // "operation aborted".
+ if (asio_socket_) {
+ if (asio_socket_->is_open()) {
+ try {
+ asio_socket_->close();
+ } catch (asio::system_error& ex) {
+ // It is really unlikely that this will occur.
+ // If we do reopen later it will be with a new socket
+ // instance. Repackage exception as one that is conformant
+ // with the interface.
+ isc_throw (NcrUDPError, ex.code().message());
+ }
+ }
+
+ asio_socket_.reset();
+ }
+
+ socket_.reset();
+}
+
+void
+NameChangeUDPSender::doSend(NameChangeRequestPtr& ncr) {
+ // Now use the NCR to write JSON to an output buffer.
+ isc::util::OutputBuffer ncr_buffer(SEND_BUF_MAX);
+ ncr->toFormat(format_, ncr_buffer);
+
+ // Copy the wire-ized request to callback. This way we know after
+ // send completes what we sent (or attempted to send).
+ send_callback_->putData(static_cast<const uint8_t*>(ncr_buffer.getData()),
+ ncr_buffer.getLength());
+
+ // Call the socket's asychronous send, passing our callback
+ socket_->asyncSend(send_callback_->getData(), send_callback_->getPutLen(),
+ send_callback_->getDataSource().get(), *send_callback_);
+}
+
+void
+NameChangeUDPSender::sendCompletionHandler(const bool successful,
+ const UDPCallback *send_callback) {
+ Result result;
+ if (successful) {
+ result = SUCCESS;
+ }
+ else {
+ // On a failure, log the error and set the result to ERROR.
+ asio::error_code error_code = send_callback->getErrorCode();
+ if (error_code.value() == asio::error::operation_aborted) {
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_UDP_SEND_CANCELED)
+ .arg(error_code.message());
+ result = STOPPED;
+ } else {
+ LOG_ERROR(dhcp_ddns_logger, DHCP_DDNS_NCR_UDP_SEND_ERROR)
+ .arg(error_code.message());
+ result = ERROR;
+ }
+ }
+
+ // Call the application's registered request send handler.
+ invokeSendHandler(result);
+}
+}; // end of isc::dhcp_ddns namespace
+}; // end of isc namespace
diff --git a/src/lib/dhcp_ddns/ncr_udp.h b/src/lib/dhcp_ddns/ncr_udp.h
new file mode 100644
index 0000000..7648a61
--- /dev/null
+++ b/src/lib/dhcp_ddns/ncr_udp.h
@@ -0,0 +1,562 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef NCR_UDP_LISTENER_H
+#define NCR_UDP_LISTENER_H
+
+/// @file ncr_udp.h
+/// @brief This file provides UDP socket based implementation for sending and
+/// receiving NameChangeRequests
+///
+/// These classes are derived from the abstract classes, NameChangeListener
+/// and NameChangeSender (see ncr_io.h).
+///
+/// The following discussion will refer to three layers of communications:
+///
+/// * Application layer - This is the business layer which needs to
+/// transport NameChangeRequests, and is unaware of the means by which
+/// they are transported.
+///
+/// * IO layer - This is the low-level layer that is directly responsible
+/// for sending and receiving data asynchronously and is supplied through
+/// other libraries. This layer is largely unaware of the nature of the
+/// data being transmitted. In other words, it doesn't know beans about
+/// NCRs.
+///
+/// * NameChangeRequest layer - This is the layer which acts as the
+/// intermediary between the Application layer and the IO layer. It must
+/// be able to move NameChangeRequests to the IO layer as raw data and move
+/// raw data from the IO layer in the Application layer as
+/// NameChangeRequests.
+///
+/// This file defines NameChangeUDPListener class for receiving NCRs, and
+/// NameChangeUDPSender for sending NCRs.
+///
+/// Both the listener and sender implementations utilize the same underlying
+/// construct to move NCRs to and from a UDP socket. This construct consists
+/// of a set of classes centered around isc::asiolink::UDPSocket. UDPSocket
+/// is a templated class that supports asio asynchronous event processing; and
+/// which accepts as its parameter, the name of a callback class.
+///
+/// The asynchronous services provided by UDPSocket typically accept a buffer
+/// for transferring data (either in or out depending on the service direction)
+/// and an object which supplies a callback to invoke upon completion of the
+/// service.
+///
+/// The callback class must provide an operator() with the following signature:
+/// @code
+/// void operator ()(const asio::error_code error_code,
+/// const size_t bytes_transferred);
+/// @endcode
+///
+/// Upon completion of the service, the callback instance's operator() is
+/// invoked by the asio layer. It is given both a outcome result and the
+/// number of bytes either read or written, to or from the buffer supplied
+/// to the service.
+///
+/// Typically, an asiolink based implementation would simply implement the
+/// callback operator directly. However, the nature of the asiolink library
+/// is such that the callback object may be copied several times during course
+/// of a service invocation. This implies that any class being used as a
+/// callback class must be copyable. This is not always desirable. In order
+/// to separate the callback class from the NameChangeRequest, the construct
+/// defines the UDPCallback class for use as a copyable, callback object.
+///
+/// The UDPCallback class provides the asiolink layer callback operator(),
+/// which is invoked by the asiolink layer upon service completion. It
+/// contains:
+/// * a pointer to the transfer buffer
+/// * the capacity of the transfer buffer
+/// * a IO layer outcome result
+/// * the number of bytes transferred
+/// * a method pointer to a NameChangeRequest layer completion handler
+///
+/// This last item, is critical. It points to an instance method that
+/// will be invoked by the UDPCallback operator. This provides access to
+/// the outcome of the service call to the NameChangeRequest layer without
+/// that layer being used as the actual callback object.
+///
+/// The completion handler method signature is codified in the typedef,
+/// UDPCompletionHandler, and must be as follows:
+///
+/// @code
+/// void(const bool, const UDPCallback*)
+/// @endcode
+///
+/// Note that is accepts two parameters. The first is a boolean indicator
+/// which indicates if the service call completed successfully or not. The
+/// second is a pointer to the callback object invoked by the IOService upon
+/// completion of the service. The callback instance will contain all of the
+/// pertinent information about the invocation and outcome of the service.
+///
+/// Using the contents of the callback, it is the responsibility of the
+/// UDPCompletionHandler to interpret the results of the service invocation and
+/// pass the interpretation to the application layer via either
+/// NameChangeListener::invokeRecvHandler in the case of the UDP listener, or
+/// NameChangeSender::invokeSendHandler in the case of UDP sender.
+///
+#include <asio.hpp>
+#include <asiolink/io_address.h>
+#include <asiolink/io_service.h>
+#include <asiolink/udp_endpoint.h>
+#include <asiolink/udp_socket.h>
+#include <dhcp_ddns/ncr_io.h>
+#include <util/buffer.h>
+
+#include <boost/shared_array.hpp>
+
+/// responsibility of the completion handler to perform the steps necessary
+/// to interpret the raw data provided by the service outcome. The
+/// UDPCallback operator implementation is mostly a pass through.
+///
+namespace isc {
+namespace dhcp_ddns {
+
+/// @brief Thrown when a UDP level exception occurs.
+class NcrUDPError : public isc::Exception {
+public:
+ NcrUDPError(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
+class UDPCallback;
+/// @brief Defines a function pointer for NameChangeRequest completion handlers.
+typedef boost::function<void(const bool, const UDPCallback*)>
+ UDPCompletionHandler;
+
+/// @brief Defines a dynamically allocated shared array.
+typedef boost::shared_array<uint8_t> RawBufferPtr;
+
+typedef boost::shared_ptr<asiolink::UDPEndpoint> UDPEndpointPtr;
+
+/// @brief Implements the callback class passed into UDPSocket calls.
+///
+/// It serves as the link between the asiolink::UDPSocket asynchronous services
+/// and the NameChangeRequest layer. The class provides the asiolink layer
+/// callback operator(), which is invoked by the asiolink layer upon service
+/// completion. It contains all of the data pertinent to both the invocation
+/// and completion of a service, as well as a pointer to NameChangeRequest
+/// layer completion handler to invoke.
+///
+class UDPCallback {
+
+public:
+ /// @brief Container class which stores service invocation related data.
+ ///
+ /// Because the callback object may be copied numerous times during the
+ /// course of service invocation, it does not directly contain data values.
+ /// Rather it will retain a shared pointer to an instance of this structure
+ /// thus ensuring that all copies of the callback object, ultimately refer
+ /// to the same data values.
+ struct Data {
+
+ /// @brief Constructor
+ ///
+ /// @param buffer is a pointer to the data transfer buffer. This is
+ /// the buffer data will be written to on a read, or read from on a
+ /// send.
+ /// @param buf_size is the capacity of the buffer
+ /// @param data_source storage for UDP endpoint which supplied the data
+ Data(RawBufferPtr& buffer, const size_t buf_size,
+ UDPEndpointPtr& data_source)
+ : buffer_(buffer), buf_size_(buf_size), data_source_(data_source),
+ put_len_(0), error_code_(), bytes_transferred_(0) {
+ };
+
+ /// @brief A pointer to the data transfer buffer.
+ RawBufferPtr buffer_;
+
+ /// @brief Storage capacity of the buffer.
+ size_t buf_size_;
+
+ /// @brief The UDP endpoint that is the origin of the data transferred.
+ UDPEndpointPtr data_source_;
+
+ /// @brief Stores this size of the data within the buffer when written
+ /// there manually. (See UDPCallback::putData()) .
+ size_t put_len_;
+
+ /// @brief Stores the IO layer result code of the completed IO service.
+ asio::error_code error_code_;
+
+ /// @brief Stores the number of bytes transferred by completed IO
+ /// service.
+ /// For a read it is the number of bytes written into the
+ /// buffer. For a write it is the number of bytes read from the
+ /// buffer.
+ size_t bytes_transferred_;
+
+ };
+
+ /// @brief Used as the callback object for UDPSocket services.
+ ///
+ /// @param buffer is a pointer to the data transfer buffer. This is
+ /// the buffer data will be written to on a read, or read from on a
+ /// send.
+ /// @param buf_size is the capacity of the buffer
+ /// @param data_source storage for UDP endpoint which supplied the data
+ /// @param handler is a method pointer to the completion handler that
+ /// is to be called by the operator() implementation.
+ ///
+ /// @throw NcrUDPError if either the handler or buffer pointers
+ /// are invalid.
+ UDPCallback (RawBufferPtr& buffer, const size_t buf_size,
+ UDPEndpointPtr& data_source,
+ const UDPCompletionHandler& handler);
+
+ /// @brief Operator that will be invoked by the asiolink layer.
+ ///
+ /// @param error_code is the IO layer result code of the
+ /// completed IO service.
+ /// @param bytes_transferred is the number of bytes transferred by
+ /// completed IO.
+ /// For a read it is the number of bytes written into the
+ /// buffer. For a write it is the number of bytes read from the
+ /// buffer.
+ void operator ()(const asio::error_code error_code,
+ const size_t bytes_transferred);
+
+ /// @brief Returns the number of bytes transferred by the completed IO
+ /// service.
+ ///
+ /// For a read it is the number of bytes written into the
+ /// buffer. For a write it is the number of bytes read from the
+ /// buffer.
+ size_t getBytesTransferred() const {
+ return (data_->bytes_transferred_);
+ }
+
+ /// @brief Sets the number of bytes transferred.
+ ///
+ /// @param value is the new value to assign to bytes transferred.
+ void setBytesTransferred(const size_t value) {
+ data_->bytes_transferred_ = value;
+ }
+
+ /// @brief Returns the completed IO layer service outcome status.
+ asio::error_code getErrorCode() const {
+ return (data_->error_code_);
+ }
+
+ /// @brief Sets the completed IO layer service outcome status.
+ ///
+ /// @param value is the new value to assign to outcome status.
+ void setErrorCode(const asio::error_code value) {
+ data_->error_code_ = value;
+ }
+
+ /// @brief Returns the data transfer buffer.
+ RawBufferPtr getBuffer() const {
+ return (data_->buffer_);
+ }
+
+ /// @brief Returns the data transfer buffer capacity.
+ size_t getBufferSize() const {
+ return (data_->buf_size_);
+ }
+
+ /// @brief Returns a pointer the data transfer buffer content.
+ const uint8_t* getData() const {
+ return (data_->buffer_.get());
+ }
+
+ /// @brief Copies data into the data transfer buffer.
+ ///
+ /// Copies the given number of bytes from the given source buffer
+ /// into the data transfer buffer, and updates the value of put length.
+ /// This method may be used when performing sends to make a copy of
+ /// the "raw data" that was shipped (or attempted) accessible to the
+ /// upstream callback.
+ ///
+ /// @param src is a pointer to the data source from which to copy
+ /// @param len is the number of bytes to copy
+ ///
+ /// @throw NcrUDPError if the number of bytes to copy exceeds
+ /// the buffer capacity or if the source pointer is invalid.
+ void putData(const uint8_t* src, size_t len);
+
+ /// @brief Returns the number of bytes manually written into the
+ /// transfer buffer.
+ size_t getPutLen() const {
+ return (data_->put_len_);
+ }
+
+ /// @brief Sets the data source to the given endpoint.
+ ///
+ /// @param endpoint is the new value to assign to data source.
+ void setDataSource(UDPEndpointPtr& endpoint) {
+ data_->data_source_ = endpoint;
+ }
+
+ /// @brief Returns the UDP endpoint that provided the transferred data.
+ const UDPEndpointPtr& getDataSource() {
+ return (data_->data_source_);
+ }
+
+ private:
+ /// @brief NameChangeRequest layer completion handler to invoke.
+ UDPCompletionHandler handler_;
+
+ /// @brief Shared pointer to the service data container.
+ boost::shared_ptr<Data> data_;
+};
+
+/// @brief Convenience type for UDP socket based listener
+typedef isc::asiolink::UDPSocket<UDPCallback> NameChangeUDPSocket;
+
+/// @brief Provides the ability to receive NameChangeRequests via UDP socket
+///
+/// This class is a derivation of the NameChangeListener which is capable of
+/// receiving NameChangeRequests through a UDP socket. The caller need only
+/// supply network addressing and a RequestReceiveHandler instance to receive
+/// NameChangeRequests asynchronously.
+class NameChangeUDPListener : public NameChangeListener {
+public:
+ /// @brief Defines the maximum size packet that can be received.
+ static const size_t RECV_BUF_MAX = isc::asiolink::
+ UDPSocket<UDPCallback>::MIN_SIZE;
+
+ /// @brief Constructor
+ ///
+ /// @param ip_address is the network address on which to listen
+ /// @param port is the UDP port on which to listen
+ /// @param format is the wire format of the inbound requests. Currently
+ /// only JSON is supported
+ /// @param ncr_recv_handler the receive handler object to notify when
+ /// a receive completes.
+ /// @param reuse_address enables IP address sharing when true
+ /// It defaults to false.
+ ///
+ /// @throw base class throws NcrListenerError if handler is invalid.
+ NameChangeUDPListener(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port,
+ const NameChangeFormat format,
+ RequestReceiveHandler& ncr_recv_handler,
+ const bool reuse_address = false);
+
+ /// @brief Destructor.
+ virtual ~NameChangeUDPListener();
+
+ /// @brief Opens a UDP socket using the given IOService.
+ ///
+ /// Creates a NameChangeUDPSocket bound to the listener's ip address
+ /// and port, that is monitored by the given IOService instance.
+ ///
+ /// @param io_service the IOService which will monitor the socket.
+ ///
+ /// @throw NcrUDPError if the open fails.
+ virtual void open(isc::asiolink::IOService& io_service);
+
+ /// @brief Closes the UDPSocket.
+ ///
+ /// It first invokes the socket's cancel method which should stop any
+ /// pending read and remove the socket callback from the IOService. It
+ /// then calls the socket's close method to actually close the socket.
+ ///
+ /// @throw NcrUDPError if the open fails.
+ virtual void close();
+
+ /// @brief Initiates an asynchronous read on the socket.
+ ///
+ /// Invokes the asyncReceive() method on the socket passing in the
+ /// recv_callback_ member's transfer buffer as the receive buffer, and
+ /// recv_callback_ itself as the callback object.
+ ///
+ /// @throw NcrUDPError if the open fails.
+ void doReceive();
+
+ /// @brief Implements the NameChangeRequest level receive completion
+ /// handler.
+ ///
+ /// This method is invoked by the UPDCallback operator() implementation,
+ /// passing in the boolean success indicator and pointer to itself.
+ ///
+ /// If the indicator denotes success, then the method will attempt to
+ /// to construct a NameChangeRequest from the received data. If the
+ /// construction was successful, it will send the new NCR to the
+ /// application layer by calling invokeRecvHandler() with a success
+ /// status and a pointer to the new NCR.
+ ///
+ /// If the buffer contains invalid data such that construction fails,
+ /// the method will log the failure and then call doReceive() to start a
+ /// initiate the next receive.
+ ///
+ /// If the indicator denotes failure the method will log the failure and
+ /// notify the application layer by calling invokeRecvHandler() with
+ /// an error status and an empty pointer.
+ ///
+ /// @param successful boolean indicator that should be true if the
+ /// socket receive completed without error, false otherwise.
+ /// @param recv_callback pointer to the callback instance which handled
+ /// the socket receive completion.
+ void receiveCompletionHandler(const bool successful,
+ const UDPCallback* recv_callback);
+private:
+ /// @brief IP address on which to listen for requests.
+ isc::asiolink::IOAddress ip_address_;
+
+ /// @brief Port number on which to listen for requests.
+ uint32_t port_;
+
+ /// @brief Wire format of the inbound requests.
+ NameChangeFormat format_;
+
+ /// @brief Low level socket underneath the listening socket
+ boost::shared_ptr<asio::ip::udp::socket> asio_socket_;
+
+ /// @brief NameChangeUDPSocket listening socket
+ boost::shared_ptr<NameChangeUDPSocket> socket_;
+
+ /// @brief Pointer to the receive callback
+ boost::shared_ptr<UDPCallback> recv_callback_;
+
+ /// @brief Flag which enables the reuse address socket option if true.
+ bool reuse_address_;
+
+ ///
+ /// @name Copy and constructor assignment operator
+ ///
+ /// The copy constructor and assignment operator are private to avoid
+ /// potential issues with multiple listeners attempting to share sockets
+ /// and callbacks.
+private:
+ NameChangeUDPListener(const NameChangeUDPListener& source);
+ NameChangeUDPListener& operator=(const NameChangeUDPListener& source);
+ //@}
+};
+
+
+/// @brief Provides the ability to send NameChangeRequests via UDP socket
+///
+/// This class is a derivation of the NameChangeSender which is capable of
+/// sending NameChangeRequests through a UDP socket. The caller need only
+/// supply network addressing and a RequestSendHandler instance to send
+/// NameChangeRequests asynchronously.
+class NameChangeUDPSender : public NameChangeSender {
+public:
+
+ /// @brief Defines the maximum size packet that can be sent.
+ static const size_t SEND_BUF_MAX = NameChangeUDPListener::RECV_BUF_MAX;
+
+ /// @brief Constructor
+ ///
+ /// @param ip_address the IP address from which to send
+ /// @param port the port from which to send
+ /// @param server_address the IP address of the target listener
+ /// @param server_port is the IP port of the target listener
+ /// @param format is the wire format of the outbound requests.
+ /// @param ncr_send_handler the send handler object to notify when
+ /// when a send completes.
+ /// @param send_que_max sets the maximum number of entries allowed in
+ /// the send queue.
+ /// It defaults to NameChangeSender::MAX_QUEUE_DEFAULT
+ /// @param reuse_address enables IP address sharing when true
+ /// It defaults to false.
+ ///
+ NameChangeUDPSender(const isc::asiolink::IOAddress& ip_address,
+ const uint32_t port, const isc::asiolink::IOAddress& server_address,
+ const uint32_t server_port, const NameChangeFormat format,
+ RequestSendHandler& ncr_send_handler,
+ const size_t send_que_max = NameChangeSender::MAX_QUEUE_DEFAULT,
+ const bool reuse_address = false);
+
+ /// @brief Destructor
+ virtual ~NameChangeUDPSender();
+
+
+ /// @brief Opens a UDP socket using the given IOService.
+ ///
+ /// Creates a NameChangeUDPSocket bound to the sender's IP address
+ /// and port, that is monitored by the given IOService instance.
+ ///
+ /// @param io_service the IOService which will monitor the socket.
+ ///
+ /// @throw NcrUDPError if the open fails.
+ virtual void open(isc::asiolink::IOService& io_service);
+
+
+ /// @brief Closes the UDPSocket.
+ ///
+ /// It first invokes the socket's cancel method which should stop any
+ /// pending send and remove the socket callback from the IOService. It
+ /// then calls the socket's close method to actually close the socket.
+ ///
+ /// @throw NcrUDPError if the open fails.
+ virtual void close();
+
+ /// @brief Sends a given request asynchronously over the socket
+ ///
+ /// The given NameChangeRequest is converted to wire format and copied
+ /// into the send callback's transfer buffer. Then the socket's
+ /// asyncSend() method is called, passing in send_callback_ member's
+ /// transfer buffer as the send buffer and the send_callback_ itself
+ /// as the callback object.
+ virtual void doSend(NameChangeRequestPtr& ncr);
+
+ /// @brief Implements the NameChangeRequest level send completion handler.
+ ///
+ /// This method is invoked by the UDPCallback operator() implementation,
+ /// passing in the boolean success indicator and pointer to itself.
+ ///
+ /// If the indicator denotes success, then the method will notify the
+ /// application layer by calling invokeSendHandler() with a success
+ /// status.
+ ///
+ /// If the indicator denotes failure the method will log the failure and
+ /// notify the application layer by calling invokeRecvHandler() with
+ /// an error status.
+ ///
+ /// @param successful boolean indicator that should be true if the
+ /// socket send completed without error, false otherwise.
+ /// @param send_callback pointer to the callback instance which handled
+ /// the socket receive completion.
+ void sendCompletionHandler(const bool successful,
+ const UDPCallback* send_callback);
+
+private:
+ /// @brief IP address from which to send.
+ isc::asiolink::IOAddress ip_address_;
+
+ /// @brief Port from which to send.
+ uint32_t port_;
+
+ /// @brief IP address of the target listener.
+ isc::asiolink::IOAddress server_address_;
+
+ /// @brief Port of the target listener.
+ uint32_t server_port_;
+
+ /// @brief Wire format of the outbound requests.
+ NameChangeFormat format_;
+
+ /// @brief Low level socket underneath the sending socket.
+ boost::shared_ptr<asio::ip::udp::socket> asio_socket_;
+
+ /// @brief NameChangeUDPSocket sending socket.
+ boost::shared_ptr<NameChangeUDPSocket> socket_;
+
+ /// @brief Endpoint of the target listener.
+ boost::shared_ptr<isc::asiolink::UDPEndpoint> server_endpoint_;
+
+ /// @brief Pointer to the send callback
+ boost::shared_ptr<UDPCallback> send_callback_;
+
+ /// @brief Flag which enables the reuse address socket option if true.
+ bool reuse_address_;
+};
+
+} // namespace isc::dhcp_ddns
+} // namespace isc
+
+#endif
diff --git a/src/lib/dhcp_ddns/tests/.gitignore b/src/lib/dhcp_ddns/tests/.gitignore
new file mode 100644
index 0000000..f022996
--- /dev/null
+++ b/src/lib/dhcp_ddns/tests/.gitignore
@@ -0,0 +1 @@
+/libdhcp_ddns_unittests
diff --git a/src/lib/dhcp_ddns/tests/Makefile.am b/src/lib/dhcp_ddns/tests/Makefile.am
new file mode 100644
index 0000000..78effc7
--- /dev/null
+++ b/src/lib/dhcp_ddns/tests/Makefile.am
@@ -0,0 +1,58 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(BOTAN_INCLUDES)
+AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcp_ddns/tests\"
+AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += libdhcp_ddns_unittests
+
+libdhcp_ddns_unittests_SOURCES = run_unittests.cc
+libdhcp_ddns_unittests_SOURCES += ncr_unittests.cc
+libdhcp_ddns_unittests_SOURCES += ncr_udp_unittests.cc
+
+libdhcp_ddns_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
+
+libdhcp_ddns_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+
+libdhcp_ddns_unittests_CXXFLAGS = $(AM_CXXFLAGS)
+if USE_CLANGPP
+# This is to workaround unused variables tcout and tcerr in
+# log4cplus's streams.h and unused parameters from some of the
+# Boost headers.
+libdhcp_ddns_unittests_CXXFLAGS += -Wno-unused-parameter
+endif
+
+libdhcp_ddns_unittests_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/cryptolink/libb10-cryptolink.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
+libdhcp_ddns_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libdhcp_ddns_unittests_LDADD += ${BOTAN_LIBS} ${BOTAN_RPATH}
+libdhcp_ddns_unittests_LDADD += $(GTEST_LDADD)
+endif
+
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc b/src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc
new file mode 100644
index 0000000..997ad4f
--- /dev/null
+++ b/src/lib/dhcp_ddns/tests/ncr_udp_unittests.cc
@@ -0,0 +1,518 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <asiolink/interval_timer.h>
+#include <dhcp_ddns/ncr_io.h>
+#include <dhcp_ddns/ncr_udp.h>
+#include <util/time_utilities.h>
+
+#include <asio/ip/udp.hpp>
+#include <boost/function.hpp>
+#include <boost/bind.hpp>
+#include <gtest/gtest.h>
+#include <algorithm>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp_ddns;
+
+namespace {
+
+/// @brief Defines a list of valid JSON NameChangeRequest test messages.
+const char *valid_msgs[] =
+{
+ // Valid Add.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Remove.
+ "{"
+ " \"change_type\" : 1 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Add with IPv6 address
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"fe80::2acf:e9ff:fe12:e56f\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}"
+};
+
+const char* TEST_ADDRESS = "127.0.0.1";
+const uint32_t LISTENER_PORT = 5301;
+const uint32_t SENDER_PORT = LISTENER_PORT+1;
+const long TEST_TIMEOUT = 5 * 1000;
+
+/// @brief A NOP derivation for constructor test purposes.
+class SimpleListenHandler : public NameChangeListener::RequestReceiveHandler {
+public:
+ virtual void operator ()(const NameChangeListener::Result,
+ NameChangeRequestPtr&) {
+ }
+};
+
+/// @brief Tests the NameChangeUDPListener constructors.
+/// This test verifies that:
+/// 1. Given valid parameters, the listener constructor works
+TEST(NameChangeUDPListenerBasicTest, constructionTests) {
+ // Verify the default constructor works.
+ isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
+ uint32_t port = LISTENER_PORT;
+ isc::asiolink::IOService io_service;
+ SimpleListenHandler ncr_handler;
+ // Verify that valid constructor works.
+ EXPECT_NO_THROW(NameChangeUDPListener(ip_address, port, FMT_JSON,
+ ncr_handler));
+}
+
+/// @brief Tests NameChangeUDPListener starting and stopping listening .
+/// This test verifies that the listener will:
+/// 1. Enter listening state
+/// 2. If in the listening state, does not allow calls to start listening
+/// 3. Exist the listening state
+/// 4. Return to the listening state after stopping
+TEST(NameChangeUDPListenerBasicTest, basicListenTests) {
+ // Verify the default constructor works.
+ isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
+ uint32_t port = LISTENER_PORT;
+ isc::asiolink::IOService io_service;
+ SimpleListenHandler ncr_handler;
+
+ NameChangeListenerPtr listener;
+ ASSERT_NO_THROW(listener.reset(
+ new NameChangeUDPListener(ip_address, port, FMT_JSON, ncr_handler)));
+
+ // Verify that we can start listening.
+ EXPECT_NO_THROW(listener->startListening(io_service));
+ // Verify that we are in listening mode.
+ EXPECT_TRUE(listener->amListening());
+ // Verify that a read is in progress.
+ EXPECT_TRUE(listener->isIoPending());
+
+ // Verify that attempting to listen when we already are is an error.
+ EXPECT_THROW(listener->startListening(io_service), NcrListenerError);
+
+ // Verify that we can stop listening.
+ EXPECT_NO_THROW(listener->stopListening());
+ EXPECT_FALSE(listener->amListening());
+
+ // Verify that IO pending is still true, as IO cancel event has not yet
+ // occurred.
+ EXPECT_TRUE(listener->isIoPending());
+
+ // Verify that IO pending is false, after cancel event occurs.
+ EXPECT_NO_THROW(io_service.run_one());
+ EXPECT_FALSE(listener->isIoPending());
+
+ // Verify that attempting to stop listening when we are not is ok.
+ EXPECT_NO_THROW(listener->stopListening());
+
+ // Verify that we can re-enter listening.
+ EXPECT_NO_THROW(listener->startListening(io_service));
+ EXPECT_TRUE(listener->amListening());
+}
+
+/// @brief Compares two NameChangeRequests for equality.
+bool checkSendVsReceived(NameChangeRequestPtr sent_ncr,
+ NameChangeRequestPtr received_ncr) {
+ return ((sent_ncr && received_ncr) &&
+ (*sent_ncr == *received_ncr));
+}
+
+/// @brief Text fixture for testing NameChangeUDPListener
+class NameChangeUDPListenerTest : public virtual ::testing::Test,
+ NameChangeListener::RequestReceiveHandler {
+public:
+ isc::asiolink::IOService io_service_;
+ NameChangeListener::Result result_;
+ NameChangeRequestPtr sent_ncr_;
+ NameChangeRequestPtr received_ncr_;
+ NameChangeListenerPtr listener_;
+ isc::asiolink::IntervalTimer test_timer_;
+
+ /// @brief Constructor
+ //
+ // Instantiates the listener member and the test timer. The timer is used
+ // to ensure a test doesn't go awry and hang forever.
+ NameChangeUDPListenerTest()
+ : io_service_(), result_(NameChangeListener::SUCCESS),
+ test_timer_(io_service_) {
+ isc::asiolink::IOAddress addr(TEST_ADDRESS);
+ listener_.reset(new NameChangeUDPListener(addr, LISTENER_PORT,
+ FMT_JSON, *this, true));
+
+ // Set the test timeout to break any running tasks if they hang.
+ test_timer_.setup(boost::bind(&NameChangeUDPListenerTest::
+ testTimeoutHandler, this),
+ TEST_TIMEOUT);
+ }
+
+ virtual ~NameChangeUDPListenerTest(){
+ }
+
+
+ /// @brief Converts JSON string into an NCR and sends it to the listener.
+ ///
+ void sendNcr(const std::string& msg) {
+ // Build an NCR from json string. This verifies that the
+ // test string is valid.
+ ASSERT_NO_THROW(sent_ncr_ = NameChangeRequest::fromJSON(msg));
+
+ // Now use the NCR to write JSON to an output buffer.
+ isc::util::OutputBuffer ncr_buffer(1024);
+ ASSERT_NO_THROW(sent_ncr_->toFormat(FMT_JSON, ncr_buffer));
+
+ // Create a UDP socket through which our "sender" will send the NCR.
+ asio::ip::udp::socket
+ udp_socket(io_service_.get_io_service(), asio::ip::udp::v4());
+
+ // Create an endpoint pointed at the listener.
+ asio::ip::udp::endpoint
+ listener_endpoint(asio::ip::address::from_string(TEST_ADDRESS),
+ LISTENER_PORT);
+
+ // A response message is now ready to send. Send it!
+ // Note this uses a synchronous send so it ships immediately.
+ // If listener isn't in listening mode, it will get missed.
+ udp_socket.send_to(asio::buffer(ncr_buffer.getData(),
+ ncr_buffer.getLength()),
+ listener_endpoint);
+ }
+
+ /// @brief RequestReceiveHandler operator implementation for receiving NCRs.
+ ///
+ /// The fixture acts as the "application" layer. It derives from
+ /// RequestReceiveHandler and as such implements operator() in order to
+ /// receive NCRs.
+ virtual void operator ()(const NameChangeListener::Result result,
+ NameChangeRequestPtr& ncr) {
+ // save the result and the NCR we received
+ result_ = result;
+ received_ncr_ = ncr;
+ }
+ // @brief Handler invoked when test timeout is hit.
+ //
+ // This callback stops all running (hanging) tasks on IO service.
+ void testTimeoutHandler() {
+ io_service_.stop();
+ FAIL() << "Test timeout hit.";
+ }
+};
+
+/// @brief Tests NameChangeUDPListener ability to receive NCRs.
+/// This test verifies that a listener can enter listening mode and
+/// receive NCRs in wire format on its UDP socket; reconstruct the
+/// NCRs and delivery them to the "application" layer.
+TEST_F(NameChangeUDPListenerTest, basicReceivetest) {
+ // Verify we can enter listening mode.
+ ASSERT_FALSE(listener_->amListening());
+ ASSERT_NO_THROW(listener_->startListening(io_service_));
+ ASSERT_TRUE(listener_->amListening());
+ ASSERT_TRUE(listener_->isIoPending());
+
+ // Iterate over a series of requests, sending and receiving one
+ /// at time.
+ int num_msgs = sizeof(valid_msgs)/sizeof(char*);
+ for (int i = 0; i < num_msgs; i++) {
+ // We are not verifying ability to send, so if we can't test is over.
+ ASSERT_NO_THROW(sendNcr(valid_msgs[i]));
+
+ // Execute no more then one event, which should be receive complete.
+ EXPECT_NO_THROW(io_service_.run_one());
+
+ // Verify the "application" status value for a successful complete.
+ EXPECT_EQ(NameChangeListener::SUCCESS, result_);
+
+ // Verify the received request matches the sent request.
+ EXPECT_TRUE(checkSendVsReceived(sent_ncr_, received_ncr_));
+ }
+
+ // Verify we can gracefully stop listening.
+ EXPECT_NO_THROW(listener_->stopListening());
+ EXPECT_FALSE(listener_->amListening());
+
+ // Verify that IO pending is false, after cancel event occurs.
+ EXPECT_NO_THROW(io_service_.run_one());
+ EXPECT_FALSE(listener_->isIoPending());
+}
+
+/// @brief A NOP derivation for constructor test purposes.
+class SimpleSendHandler : public NameChangeSender::RequestSendHandler {
+public:
+ virtual void operator ()(const NameChangeSender::Result,
+ NameChangeRequestPtr&) {
+ }
+};
+
+/// @brief Tests the NameChangeUDPSender constructors.
+/// This test verifies that:
+/// 1. Constructing with a max queue size of 0 is not allowed
+/// 2. Given valid parameters, the sender constructor works
+/// 3. Default construction provides default max queue size
+/// 4. Construction with a custom max queue size works
+TEST(NameChangeUDPSenderBasicTest, constructionTests) {
+ isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
+ uint32_t port = SENDER_PORT;
+ isc::asiolink::IOService io_service;
+ SimpleSendHandler ncr_handler;
+
+ // Verify that constructing with an queue size of zero is not allowed.
+ EXPECT_THROW(NameChangeUDPSender(ip_address, port,
+ ip_address, port, FMT_JSON, ncr_handler, 0), NcrSenderError);
+
+ NameChangeSenderPtr sender;
+ // Verify that valid constructor works.
+ EXPECT_NO_THROW(sender.reset(
+ new NameChangeUDPSender(ip_address, port, ip_address, port,
+ FMT_JSON, ncr_handler)));
+
+ // Verify that send queue default max is correct.
+ size_t expected = NameChangeSender::MAX_QUEUE_DEFAULT;
+ EXPECT_EQ(expected, sender->getQueueMaxSize());
+
+ // Verify that constructor with a valid custom queue size works.
+ EXPECT_NO_THROW(sender.reset(
+ new NameChangeUDPSender(ip_address, port, ip_address, port,
+ FMT_JSON, ncr_handler, 100)));
+
+ EXPECT_EQ(100, sender->getQueueMaxSize());
+}
+
+/// @brief Tests NameChangeUDPSender basic send functionality
+/// This test verifies that:
+TEST(NameChangeUDPSenderBasicTest, basicSendTests) {
+ isc::asiolink::IOAddress ip_address(TEST_ADDRESS);
+ uint32_t port = SENDER_PORT;
+ isc::asiolink::IOService io_service;
+ SimpleSendHandler ncr_handler;
+
+ // Tests are based on a list of messages, get the count now.
+ int num_msgs = sizeof(valid_msgs)/sizeof(char*);
+
+ // Create the sender, setting the queue max equal to the number of
+ // messages we will have in the list.
+ NameChangeUDPSender sender(ip_address, port, ip_address, port,
+ FMT_JSON, ncr_handler, num_msgs);
+
+ // Verify that we can start sending.
+ EXPECT_NO_THROW(sender.startSending(io_service));
+ EXPECT_TRUE(sender.amSending());
+
+ // Verify that attempting to send when we already are is an error.
+ EXPECT_THROW(sender.startSending(io_service), NcrSenderError);
+
+ // Verify that we can stop sending.
+ EXPECT_NO_THROW(sender.stopSending());
+ EXPECT_FALSE(sender.amSending());
+
+ // Verify that attempting to stop sending when we are not is ok.
+ EXPECT_NO_THROW(sender.stopSending());
+
+ // Verify that we can re-enter sending after stopping.
+ EXPECT_NO_THROW(sender.startSending(io_service));
+ EXPECT_TRUE(sender.amSending());
+
+ // Iterate over a series of messages, sending each one. Since we
+ // do not invoke IOService::run, then the messages should accumulate
+ // in the queue.
+ NameChangeRequestPtr ncr;
+ for (int i = 0; i < num_msgs; i++) {
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ EXPECT_NO_THROW(sender.sendRequest(ncr));
+ // Verify that the queue count increments in step with each send.
+ EXPECT_EQ(i+1, sender.getQueueSize());
+ }
+
+ // Verify that attempting to send an additional message results in a
+ // queue full exception.
+ EXPECT_THROW(sender.sendRequest(ncr), NcrSenderQueueFull);
+
+ // Loop for the number of valid messages and invoke IOService::run_one.
+ // This should send exactly one message and the queue count should
+ // decrement accordingly.
+ for (int i = num_msgs; i > 0; i--) {
+ io_service.run_one();
+ // Verify that the queue count decrements in step with each run.
+ EXPECT_EQ(i-1, sender.getQueueSize());
+ }
+
+ // Verify that the queue is empty.
+ EXPECT_EQ(0, sender.getQueueSize());
+
+ // Verify that we can add back to the queue
+ EXPECT_NO_THROW(sender.sendRequest(ncr));
+ EXPECT_EQ(1, sender.getQueueSize());
+
+ // Verify that we can remove the current entry at the front of the queue.
+ EXPECT_NO_THROW(sender.skipNext());
+ EXPECT_EQ(0, sender.getQueueSize());
+
+ // Verify that flushing the queue is not allowed in sending state.
+ EXPECT_THROW(sender.clearSendQueue(), NcrSenderError);
+
+ // Put a message on the queue.
+ EXPECT_NO_THROW(sender.sendRequest(ncr));
+ EXPECT_EQ(1, sender.getQueueSize());
+
+ // Verify that we can gracefully stop sending.
+ EXPECT_NO_THROW(sender.stopSending());
+ EXPECT_FALSE(sender.amSending());
+
+ // Verify that the queue is preserved after leaving sending state.
+ EXPECT_EQ(1, sender.getQueueSize());
+
+ // Verify that flushing the queue works when not sending.
+ EXPECT_NO_THROW(sender.clearSendQueue());
+ EXPECT_EQ(0, sender.getQueueSize());
+}
+
+/// @brief Text fixture that allows testing a listener and sender together
+/// It derives from both the receive and send handler classes and contains
+/// and instance of UDP listener and UDP sender.
+class NameChangeUDPTest : public virtual ::testing::Test,
+ NameChangeListener::RequestReceiveHandler,
+ NameChangeSender::RequestSendHandler {
+public:
+ isc::asiolink::IOService io_service_;
+ NameChangeListener::Result recv_result_;
+ NameChangeSender::Result send_result_;
+ NameChangeListenerPtr listener_;
+ NameChangeSenderPtr sender_;
+ isc::asiolink::IntervalTimer test_timer_;
+
+ std::vector<NameChangeRequestPtr> sent_ncrs_;
+ std::vector<NameChangeRequestPtr> received_ncrs_;
+
+ NameChangeUDPTest()
+ : io_service_(), recv_result_(NameChangeListener::SUCCESS),
+ send_result_(NameChangeSender::SUCCESS), test_timer_(io_service_) {
+ isc::asiolink::IOAddress addr(TEST_ADDRESS);
+ // Create our listener instance. Note that reuse_address is true.
+ listener_.reset(
+ new NameChangeUDPListener(addr, LISTENER_PORT, FMT_JSON,
+ *this, true));
+
+ // Create our sender instance. Note that reuse_address is true.
+ sender_.reset(
+ new NameChangeUDPSender(addr, SENDER_PORT, addr, LISTENER_PORT,
+ FMT_JSON, *this, 100, true));
+
+ // Set the test timeout to break any running tasks if they hang.
+ test_timer_.setup(boost::bind(&NameChangeUDPTest::testTimeoutHandler,
+ this),
+ TEST_TIMEOUT);
+ }
+
+ void reset_results() {
+ sent_ncrs_.clear();
+ received_ncrs_.clear();
+ }
+
+ /// @brief Implements the receive completion handler.
+ virtual void operator ()(const NameChangeListener::Result result,
+ NameChangeRequestPtr& ncr) {
+ // save the result and the NCR received.
+ recv_result_ = result;
+ received_ncrs_.push_back(ncr);
+ }
+
+ /// @brief Implements the send completion handler.
+ virtual void operator ()(const NameChangeSender::Result result,
+ NameChangeRequestPtr& ncr) {
+ // save the result and the NCR sent.
+ send_result_ = result;
+ sent_ncrs_.push_back(ncr);
+ }
+
+ // @brief Handler invoked when test timeout is hit.
+ //
+ // This callback stops all running (hanging) tasks on IO service.
+ void testTimeoutHandler() {
+ io_service_.stop();
+ FAIL() << "Test timeout hit.";
+ }
+};
+
+/// @brief Uses a sender and listener to test UDP-based NCR delivery
+/// Conducts a "round-trip" test using a sender to transmit a set of valid
+/// NCRs to a listener. The test verifies that what was sent matches what
+/// was received both in quantity and in content.
+TEST_F (NameChangeUDPTest, roundTripTest) {
+ // Place the listener into listening state.
+ ASSERT_NO_THROW(listener_->startListening(io_service_));
+ EXPECT_TRUE(listener_->amListening());
+
+ // Get the number of messages in the list of test messages.
+ int num_msgs = sizeof(valid_msgs)/sizeof(char*);
+
+ // Place the sender into sending state.
+ ASSERT_NO_THROW(sender_->startSending(io_service_));
+ EXPECT_TRUE(sender_->amSending());
+
+ for (int i = 0; i < num_msgs; i++) {
+ NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(valid_msgs[i]));
+ sender_->sendRequest(ncr);
+ EXPECT_EQ(i+1, sender_->getQueueSize());
+ }
+
+ // Execute callbacks until we have sent and received all of messages.
+ while (sender_->getQueueSize() > 0 || (received_ncrs_.size() < num_msgs)) {
+ EXPECT_NO_THROW(io_service_.run_one());
+ }
+
+ // Send queue should be empty.
+ EXPECT_EQ(0, sender_->getQueueSize());
+
+ // We should have the same number of sends and receives as we do messages.
+ ASSERT_EQ(num_msgs, sent_ncrs_.size());
+ ASSERT_EQ(num_msgs, received_ncrs_.size());
+
+ // Verify that what we sent matches what we received.
+ for (int i = 0; i < num_msgs; i++) {
+ EXPECT_TRUE (checkSendVsReceived(sent_ncrs_[i], received_ncrs_[i]));
+ }
+
+ // Verify that we can gracefully stop listening.
+ EXPECT_NO_THROW(listener_->stopListening());
+ EXPECT_FALSE(listener_->amListening());
+
+ // Verify that IO pending is false, after cancel event occurs.
+ EXPECT_NO_THROW(io_service_.run_one());
+ EXPECT_FALSE(listener_->isIoPending());
+
+ // Verify that we can gracefully stop sending.
+ EXPECT_NO_THROW(sender_->stopSending());
+ EXPECT_FALSE(sender_->amSending());
+}
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcp_ddns/tests/ncr_unittests.cc b/src/lib/dhcp_ddns/tests/ncr_unittests.cc
new file mode 100644
index 0000000..c66b891
--- /dev/null
+++ b/src/lib/dhcp_ddns/tests/ncr_unittests.cc
@@ -0,0 +1,633 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp_ddns/ncr_io.h>
+#include <dhcp/duid.h>
+#include <dhcp/hwaddr.h>
+#include <util/time_utilities.h>
+
+#include <gtest/gtest.h>
+#include <algorithm>
+
+using namespace std;
+using namespace isc;
+using namespace isc::dhcp_ddns;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Defines a list of valid JSON NameChangeRequest renditions.
+/// They are used as input to test conversion from JSON.
+const char *valid_msgs[] =
+{
+ // Valid Add.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Remove.
+ "{"
+ " \"change_type\" : 1 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Valid Add with IPv6 address
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"fe80::2acf:e9ff:fe12:e56f\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}"
+};
+
+/// @brief Defines a list of invalid JSON NameChangeRequest renditions.
+/// They are used as input to test conversion from JSON.
+const char *invalid_msgs[] =
+{
+ // Invalid change type.
+ "{"
+ " \"change_type\" : 7 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Invalid forward change.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : \"bogus\" , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Invalid reverse change.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : 500 , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Forward and reverse change both false.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : false , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Blank FQDN
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Malformed FQDN
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \".bad_name\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Bad IP address
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"xxxxxx\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Blank DHCID
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Odd number of digits in DHCID
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Text in DHCID
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"THIS IS BOGUS!!!\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Invalid lease expiration string
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"Wed Jun 26 13:46:46 EDT 2013\" , "
+ " \"lease_length\" : 1300 "
+ "}",
+ // Non-integer for lease length.
+ "{"
+ " \"change_type\" : 0 , "
+ " \"forward_change\" : true , "
+ " \"reverse_change\" : false , "
+ " \"fqdn\" : \"walah.walah.com\" , "
+ " \"ip_address\" : \"192.168.2.1\" , "
+ " \"dhcid\" : \"010203040A7F8E3D\" , "
+ " \"lease_expires_on\" : \"20130121132405\" , "
+ " \"lease_length\" : \"BOGUS\" "
+ "}"
+
+};
+
+/// @brief Tests the NameChangeRequest constructors.
+/// This test verifies that:
+/// 1. Default constructor works.
+/// 2. "Full" constructor, when given valid parameter values, works.
+/// 3. "Full" constructor, given a blank FQDN fails
+/// 4. "Full" constructor, given an invalid IP Address FQDN fails
+/// 5. "Full" constructor, given a blank DHCID fails
+/// 6. "Full" constructor, given false for both forward and reverse fails
+TEST(NameChangeRequestTest, constructionTests) {
+ // Verify the default constructor works.
+ NameChangeRequestPtr ncr;
+ EXPECT_NO_THROW(ncr.reset(new NameChangeRequest()));
+ EXPECT_TRUE(ncr);
+
+ // Verify that full constructor works.
+ uint64_t expiry = isc::util::detail::gettimeWrapper();
+ D2Dhcid dhcid("010203040A7F8E3D");
+
+ EXPECT_NO_THROW(ncr.reset(new NameChangeRequest(
+ CHG_ADD, true, true, "walah.walah.com",
+ "192.168.1.101", dhcid, expiry, 1300)));
+ EXPECT_TRUE(ncr);
+ ncr.reset();
+
+ // Verify blank FQDN is detected.
+ EXPECT_THROW(NameChangeRequest(CHG_ADD, true, true, "",
+ "192.168.1.101", dhcid, expiry, 1300), NcrMessageError);
+
+ // Verify that an invalid IP address is detected.
+ EXPECT_THROW(NameChangeRequest(CHG_ADD, true, true, "valid.fqdn",
+ "xxx.168.1.101", dhcid, expiry, 1300), NcrMessageError);
+
+ // Verify that a blank DHCID is detected.
+ D2Dhcid blank_dhcid;
+ EXPECT_THROW(NameChangeRequest(CHG_ADD, true, true, "walah.walah.com",
+ "192.168.1.101", blank_dhcid, expiry, 1300), NcrMessageError);
+
+ // Verify that one or both of direction flags must be true.
+ EXPECT_THROW(NameChangeRequest(CHG_ADD, false, false, "valid.fqdn",
+ "192.168.1.101", dhcid, expiry, 1300), NcrMessageError);
+
+}
+
+/// @brief Tests the basic workings of D2Dhcid to and from string conversions.
+/// It verifies that:
+/// 1. DHCID input strings must contain an even number of characters
+/// 2. DHCID input strings must contain only hexadecimal character digits
+/// 3. A valid DHCID string converts correctly.
+/// 4. Converting a D2Dhcid to a string works correctly.
+/// 5. Equality, inequality, and less-than-equal operators work.
+TEST(NameChangeRequestTest, dhcidTest) {
+ D2Dhcid dhcid;
+
+ // Odd number of digits should be rejected.
+ std::string test_str = "010203040A7F8E3";
+ EXPECT_THROW(dhcid.fromStr(test_str), NcrMessageError);
+
+ // Non digit content should be rejected.
+ test_str = "0102BOGUSA7F8E3D";
+ EXPECT_THROW(dhcid.fromStr(test_str), NcrMessageError);
+
+ // Verify that valid input converts into a proper byte array.
+ test_str = "010203040A7F8E3D";
+ ASSERT_NO_THROW(dhcid.fromStr(test_str));
+
+ // Create a test vector of expected byte contents.
+ const uint8_t bytes[] = { 0x1, 0x2, 0x3, 0x4, 0xA, 0x7F, 0x8E, 0x3D };
+ std::vector<uint8_t> expected_bytes(bytes, bytes + sizeof(bytes));
+
+ // Fetch the byte vector from the dhcid and verify if equals the expected
+ // content.
+ const std::vector<uint8_t>& converted_bytes = dhcid.getBytes();
+ EXPECT_EQ(expected_bytes.size(), converted_bytes.size());
+ EXPECT_TRUE (std::equal(expected_bytes.begin(),
+ expected_bytes.begin()+expected_bytes.size(),
+ converted_bytes.begin()));
+
+ // Convert the new dhcid back to string and verify it matches the original
+ // DHCID input string.
+ std::string next_str = dhcid.toStr();
+ EXPECT_EQ(test_str, next_str);
+
+ // Test equality, inequality, and less-than-equal operators
+ test_str="AABBCCDD";
+ EXPECT_NO_THROW(dhcid.fromStr(test_str));
+
+ D2Dhcid other_dhcid;
+ EXPECT_NO_THROW(other_dhcid.fromStr(test_str));
+
+ EXPECT_TRUE(dhcid == other_dhcid);
+ EXPECT_FALSE(dhcid != other_dhcid);
+
+ EXPECT_NO_THROW(other_dhcid.fromStr("BBCCDDEE"));
+ EXPECT_TRUE(dhcid < other_dhcid);
+
+}
+
+/// @brief Test fixture class for testing DHCID creation.
+class DhcidTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ DhcidTest() {
+ const uint8_t fqdn_data[] = {
+ 6, 109, 121, 104, 111, 115, 116, // myhost.
+ 7, 101, 120, 97, 109, 112, 108, 101, // example.
+ 3, 99, 111, 109, 0 // com.
+ };
+ wire_fqdn_.assign(fqdn_data, fqdn_data + sizeof(fqdn_data));
+ }
+
+ /// @brief Destructor
+ virtual ~DhcidTest() {
+ }
+
+ std::vector<uint8_t> wire_fqdn_;
+};
+
+/// Tests that DHCID is correctly created from a DUID and FQDN. The final format
+/// of the DHCID is as follows:
+/// <identifier-type> <digest-type-code> <digest>
+/// where:
+/// - identifier-type (2 octets) is 0x0002.
+/// - digest-type-code (1 octet) indicates SHA-256 hashing and is equal 0x1.
+/// - digest = SHA-256(<DUID> <FQDN>)
+/// Note: FQDN is given in the on-wire canonical format.
+TEST_F(DhcidTest, fromDUID) {
+ D2Dhcid dhcid;
+
+ // Create DUID.
+ uint8_t duid_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+ DUID duid(duid_data, sizeof(duid_data));
+
+ // Create DHCID.
+ ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
+
+ // The reference DHCID (represented as string of hexadecimal digits)
+ // has been calculated using one of the online calculators.
+ std::string dhcid_ref = "0002012191B7B21AF97E0E656DF887C5E2D"
+ "EF30E7758A207EDF4CCB2DE8CA37066021C";
+
+ // Make sure that the DHCID is valid.
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+// Test that DHCID is correctly created when the DUID has minimal length (1).
+TEST_F(DhcidTest, fromMinDUID) {
+ D2Dhcid dhcid;
+
+ // Create DUID.
+ uint8_t duid_data[] = { 1 };
+ DUID duid(duid_data, sizeof(duid_data));
+
+ // Create DHCID.
+ ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
+
+ // The reference DHCID (represented as string of hexadecimal digits)
+ // has been calculated using one of the online calculators.
+ std::string dhcid_ref = "000201F89004F73E60CAEDFF514E11CB91D"
+ "1F45C8F0A55D4BC4C688484A819F8EA4074";
+
+ // Make sure that the DHCID is valid.
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+// Test that DHCID is correctly created when the DUID has maximal length (128).
+TEST_F(DhcidTest, fromMaxDUID) {
+ D2Dhcid dhcid;
+
+ // Create DUID.
+ std::vector<uint8_t> duid_data(128, 1);
+ DUID duid(&duid_data[0], duid_data.size());
+
+ // Create DHCID.
+ ASSERT_NO_THROW(dhcid.fromDUID(duid, wire_fqdn_));
+
+ // The reference DHCID (represented as string of hexadecimal digits)
+ // has been calculated using one of the online calculators.
+ std::string dhcid_ref = "00020137D8FBDC0585B44DFA03FAD2E36C6"
+ "159737D545A12EFB40B0D88D110A5748234";
+
+ // Make sure that the DHCID is valid.
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+}
+
+// This test verifies that DHCID is properly computed from a buffer holding
+// client identifier data.
+TEST_F(DhcidTest, fromClientId) {
+ D2Dhcid dhcid;
+
+ // Create a buffer holding client id..
+ uint8_t clientid_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+ std::vector<uint8_t> clientid(clientid_data,
+ clientid_data + sizeof(clientid_data));
+
+ // Create DHCID.
+ ASSERT_NO_THROW(dhcid.fromClientId(clientid, wire_fqdn_));
+
+ // The reference DHCID (represented as string of hexadecimal digits)
+ // has been calculated using one of the online calculators.
+ std::string dhcid_ref = "0001012191B7B21AF97E0E656DF887C5E2D"
+ "EF30E7758A207EDF4CCB2DE8CA37066021C";
+
+ // Make sure that the DHCID is valid.
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+
+ // Make sure that the empty FQDN is not accepted.
+ std::vector<uint8_t> empty_wire_fqdn;
+ EXPECT_THROW(dhcid.fromClientId(clientid, empty_wire_fqdn),
+ isc::dhcp_ddns::DhcidRdataComputeError);
+
+ // Make sure that the empty client identifier is not accepted.
+ clientid.clear();
+ EXPECT_THROW(dhcid.fromClientId(clientid, wire_fqdn_),
+ isc::dhcp_ddns::DhcidRdataComputeError);
+
+
+}
+
+// This test verifies that DHCID is properly computed from a HW address.
+TEST_F(DhcidTest, fromHWAddr) {
+ D2Dhcid dhcid;
+
+ // Create a buffer holding client id..
+ uint8_t hwaddr_data[] = { 0, 1, 2, 3, 4, 5, 6 };
+ HWAddrPtr hwaddr(new HWAddr(hwaddr_data, sizeof(hwaddr_data),
+ HTYPE_ETHER));
+
+ // Create DHCID.
+ ASSERT_NO_THROW(dhcid.fromHWAddr(hwaddr, wire_fqdn_));
+
+ // The reference DHCID (represented as string of hexadecimal digits)
+ // has been calculated using one of the online calculators.
+ std::string dhcid_ref = "0000012247F6DC4423C3E8627434A9D686860"
+ "9D88948F78018B215EDCAA30C0C135035";
+
+ // Make sure that the DHCID is valid.
+ EXPECT_EQ(dhcid_ref, dhcid.toStr());
+
+ // Make sure that the empty FQDN is not accepted.
+ std::vector<uint8_t> empty_wire_fqdn;
+ EXPECT_THROW(dhcid.fromHWAddr(hwaddr, empty_wire_fqdn),
+ isc::dhcp_ddns::DhcidRdataComputeError);
+
+ // Make sure that the NULL HW address is not accepted.
+ hwaddr.reset();
+ EXPECT_THROW(dhcid.fromHWAddr(hwaddr, wire_fqdn_),
+ isc::dhcp_ddns::DhcidRdataComputeError);
+}
+
+// test operator<< on D2Dhcid
+TEST(NameChangeRequestTest, leftShiftOperation) {
+ const D2Dhcid dhcid("010203040A7F8E3D");
+
+ ostringstream oss;
+ oss << dhcid;
+ EXPECT_EQ(dhcid.toStr(), oss.str());
+}
+
+/// @brief Verifies the fundamentals of converting from and to JSON.
+/// It verifies that:
+/// 1. A NameChangeRequest can be created from a valid JSON string.
+/// 2. A valid JSON string can be created from a NameChangeRequest
+TEST(NameChangeRequestTest, basicJsonTest) {
+ // Define valid JSON rendition of a request.
+ std::string msg_str = "{"
+ "\"change_type\":1,"
+ "\"forward_change\":true,"
+ "\"reverse_change\":false,"
+ "\"fqdn\":\"walah.walah.com.\","
+ "\"ip_address\":\"192.168.2.1\","
+ "\"dhcid\":\"010203040A7F8E3D\","
+ "\"lease_expires_on\":\"20130121132405\","
+ "\"lease_length\":1300"
+ "}";
+
+ // Verify that a NameChangeRequests can be instantiated from the
+ // a valid JSON rendition.
+ NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(msg_str));
+ ASSERT_TRUE(ncr);
+
+ // Verify that the JSON string created by the new request equals the
+ // original input string.
+ std::string json_str = ncr->toJSON();
+ EXPECT_EQ(msg_str, json_str);
+}
+
+/// @brief Tests a variety of invalid JSON message strings.
+/// This test iterates over a list of JSON messages, each containing a single
+/// content error. The list of messages is defined by the global array,
+/// invalid_messages. Currently that list contains the following invalid
+/// conditions:
+/// 1. Invalid change type
+/// 2. Invalid forward change
+/// 3. Invalid reverse change
+/// 4. Forward and reverse change both false
+/// 5. Invalid forward change
+/// 6. Blank FQDN
+/// 7. Bad IP address
+/// 8. Blank DHCID
+/// 9. Odd number of digits in DHCID
+/// 10. Text in DHCID
+/// 11. Invalid lease expiration string
+/// 12. Non-integer for lease length.
+/// If more permutations arise they can easily be added to the list.
+TEST(NameChangeRequestTest, invalidMsgChecks) {
+ // Iterate over the list of JSON strings, attempting to create a
+ // NameChangeRequest. The attempt should throw a NcrMessageError.
+ int num_msgs = sizeof(invalid_msgs)/sizeof(char*);
+ for (int i = 0; i < num_msgs; i++) {
+ EXPECT_THROW(NameChangeRequest::fromJSON(invalid_msgs[i]),
+ NcrMessageError) << "Invalid message not caught idx: "
+ << i << std::endl << " text:[" << invalid_msgs[i] << "]"
+ << std::endl;
+ }
+}
+
+/// @brief Tests a variety of valid JSON message strings.
+/// This test iterates over a list of JSON messages, each containing a single
+/// valid request rendition. The list of messages is defined by the global
+/// array, valid_messages. Currently that list contains the following valid
+/// messages:
+/// 1. Valid, IPv4 Add
+/// 2. Valid, IPv4 Remove
+/// 3. Valid, IPv6 Add
+/// If more permutations arise they can easily be added to the list.
+TEST(NameChangeRequestTest, validMsgChecks) {
+ // Iterate over the list of JSON strings, attempting to create a
+ // NameChangeRequest. The attempt should succeed.
+ int num_msgs = sizeof(valid_msgs)/sizeof(char*);
+ for (int i = 0; i < num_msgs; i++) {
+ EXPECT_NO_THROW(NameChangeRequest::fromJSON(valid_msgs[i]))
+ << "Valid message failed, message idx: " << i
+ << std::endl << " text:[" << valid_msgs[i] << "]"
+ << std::endl;
+ }
+}
+
+/// @brief Tests converting to and from JSON via isc::util buffer classes.
+/// This test verifies that:
+/// 1. A NameChangeRequest can be rendered in JSON written to an OutputBuffer
+/// 2. A InputBuffer containing a valid JSON request rendition can be used
+/// to create a NameChangeRequest.
+TEST(NameChangeRequestTest, toFromBufferTest) {
+ // Define a string containing a valid JSON NameChangeRequest rendition.
+ std::string msg_str = "{"
+ "\"change_type\":1,"
+ "\"forward_change\":true,"
+ "\"reverse_change\":false,"
+ "\"fqdn\":\"walah.walah.com.\","
+ "\"ip_address\":\"192.168.2.1\","
+ "\"dhcid\":\"010203040A7F8E3D\","
+ "\"lease_expires_on\":\"20130121132405\","
+ "\"lease_length\":1300"
+ "}";
+
+ // Create a request from JSON directly.
+ NameChangeRequestPtr ncr;
+ ASSERT_NO_THROW(ncr = NameChangeRequest::fromJSON(msg_str));
+
+ // Verify that we output the request as JSON text to a buffer
+ // without error.
+ isc::util::OutputBuffer output_buffer(1024);
+ ASSERT_NO_THROW(ncr->toFormat(FMT_JSON, output_buffer));
+
+ // Make an InputBuffer from the OutputBuffer.
+ isc::util::InputBuffer input_buffer(output_buffer.getData(),
+ output_buffer.getLength());
+
+ // Verify that we can create a new request from the InputBuffer.
+ NameChangeRequestPtr ncr2;
+ ASSERT_NO_THROW(ncr2 =
+ NameChangeRequest::fromFormat(FMT_JSON, input_buffer));
+
+ // Convert the new request to JSON directly.
+ std::string final_str = ncr2->toJSON();
+
+ // Verify that the final string matches the original.
+ ASSERT_EQ(final_str, msg_str);
+}
+
+/// @brief Tests ip address modification and validation
+TEST(NameChangeRequestTest, ipAddresses) {
+ NameChangeRequest ncr;
+
+ // Verify that a valid IPv4 address works.
+ ASSERT_NO_THROW(ncr.setIpAddress("192.168.1.1"));
+ const asiolink::IOAddress& io_addr4 = ncr.getIpIoAddress();
+ EXPECT_EQ(ncr.getIpAddress(), io_addr4.toText());
+ EXPECT_TRUE(ncr.isV4());
+ EXPECT_FALSE(ncr.isV6());
+
+ // Verify that a valid IPv6 address works.
+ ASSERT_NO_THROW(ncr.setIpAddress("2001:1::f3"));
+ const asiolink::IOAddress& io_addr6 = ncr.getIpIoAddress();
+ EXPECT_EQ(ncr.getIpAddress(), io_addr6.toText());
+ EXPECT_FALSE(ncr.isV4());
+ EXPECT_TRUE(ncr.isV6());
+
+ // Verify that an invalid address fails.
+ ASSERT_THROW(ncr.setIpAddress("x001:1::f3"),NcrMessageError);
+}
+
+/// @brief Tests conversion of NameChangeFormat between enum and strings.
+TEST(NameChangeFormatTest, formatEnumConversion){
+ ASSERT_EQ(stringToNcrFormat("JSON"), dhcp_ddns::FMT_JSON);
+ ASSERT_EQ(stringToNcrFormat("jSoN"), dhcp_ddns::FMT_JSON);
+ ASSERT_THROW(stringToNcrFormat("bogus"), isc::BadValue);
+
+ ASSERT_EQ(ncrFormatToString(dhcp_ddns::FMT_JSON), "JSON");
+}
+
+/// @brief Tests conversion of NameChangeProtocol between enum and strings.
+TEST(NameChangeProtocolTest, protocolEnumConversion){
+ ASSERT_EQ(stringToNcrProtocol("UDP"), dhcp_ddns::NCR_UDP);
+ ASSERT_EQ(stringToNcrProtocol("udP"), dhcp_ddns::NCR_UDP);
+ ASSERT_EQ(stringToNcrProtocol("TCP"), dhcp_ddns::NCR_TCP);
+ ASSERT_EQ(stringToNcrProtocol("Tcp"), dhcp_ddns::NCR_TCP);
+ ASSERT_THROW(stringToNcrProtocol("bogus"), isc::BadValue);
+
+ ASSERT_EQ(ncrProtocolToString(dhcp_ddns::NCR_UDP), "UDP");
+ ASSERT_EQ(ncrProtocolToString(dhcp_ddns::NCR_TCP), "TCP");
+}
+
+} // end of anonymous namespace
+
diff --git a/src/lib/dhcp_ddns/tests/run_unittests.cc b/src/lib/dhcp_ddns/tests/run_unittests.cc
new file mode 100644
index 0000000..bd32c4d
--- /dev/null
+++ b/src/lib/dhcp_ddns/tests/run_unittests.cc
@@ -0,0 +1,27 @@
+// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/logger_support.h>
+
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+ ::testing::InitGoogleTest(&argc, argv);
+ isc::log::initLogger();
+
+ int result = RUN_ALL_TESTS();
+
+ return (result);
+}
diff --git a/src/lib/dhcpsrv/.gitignore b/src/lib/dhcpsrv/.gitignore
index 0b02c01..1f08538 100644
--- a/src/lib/dhcpsrv/.gitignore
+++ b/src/lib/dhcpsrv/.gitignore
@@ -1,2 +1,3 @@
/dhcpsrv_messages.cc
/dhcpsrv_messages.h
+/s-messages
diff --git a/src/lib/dhcpsrv/Makefile.am b/src/lib/dhcpsrv/Makefile.am
index f646dd6..69b14b2 100644
--- a/src/lib/dhcpsrv/Makefile.am
+++ b/src/lib/dhcpsrv/Makefile.am
@@ -11,8 +11,11 @@ endif
AM_CXXFLAGS = $(B10_CXXFLAGS)
# Define rule to build logging source files from message file
-dhcpsrv_messages.h dhcpsrv_messages.cc: dhcpsrv_messages.mes
+dhcpsrv_messages.h dhcpsrv_messages.cc: s-messages
+
+s-messages: dhcpsrv_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/dhcpsrv/dhcpsrv_messages.mes
+ touch $@
# Tell Automake that the dhcpsrv_messages.{cc,h} source files are created in the
# build process, so it must create these before doing anything else. Although
@@ -29,17 +32,21 @@ BUILT_SOURCES = dhcpsrv_messages.h dhcpsrv_messages.cc
AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
# Make sure the generated files are deleted in a "clean" operation
-CLEANFILES = *.gcno *.gcda dhcpsrv_messages.h dhcpsrv_messages.cc
+CLEANFILES = *.gcno *.gcda dhcpsrv_messages.h dhcpsrv_messages.cc s-messages
lib_LTLIBRARIES = libb10-dhcpsrv.la
libb10_dhcpsrv_la_SOURCES =
libb10_dhcpsrv_la_SOURCES += addr_utilities.cc addr_utilities.h
libb10_dhcpsrv_la_SOURCES += alloc_engine.cc alloc_engine.h
+libb10_dhcpsrv_la_SOURCES += callout_handle_store.h
+libb10_dhcpsrv_la_SOURCES += d2_client.cc d2_client.h
libb10_dhcpsrv_la_SOURCES += dbaccess_parser.cc dbaccess_parser.h
libb10_dhcpsrv_la_SOURCES += dhcpsrv_log.cc dhcpsrv_log.h
libb10_dhcpsrv_la_SOURCES += cfgmgr.cc cfgmgr.h
libb10_dhcpsrv_la_SOURCES += dhcp_config_parser.h
+libb10_dhcpsrv_la_SOURCES += dhcp_parsers.cc dhcp_parsers.h
libb10_dhcpsrv_la_SOURCES += key_from_key.h
+libb10_dhcpsrv_la_SOURCES += lease.cc lease.h
libb10_dhcpsrv_la_SOURCES += lease_mgr.cc lease_mgr.h
libb10_dhcpsrv_la_SOURCES += lease_mgr_factory.cc lease_mgr_factory.h
libb10_dhcpsrv_la_SOURCES += memfile_lease_mgr.cc memfile_lease_mgr.h
@@ -58,7 +65,13 @@ libb10_dhcpsrv_la_CXXFLAGS = $(AM_CXXFLAGS)
libb10_dhcpsrv_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
libb10_dhcpsrv_la_LIBADD = $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
+libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/util/libb10-util.la
+libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/cc/libb10-cc.la
+libb10_dhcpsrv_la_LIBADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
+
libb10_dhcpsrv_la_LDFLAGS = -no-undefined -version-info 3:0:0
if HAVE_MYSQL
libb10_dhcpsrv_la_LDFLAGS += $(MYSQL_LIBS)
diff --git a/src/lib/dhcpsrv/alloc_engine.cc b/src/lib/dhcpsrv/alloc_engine.cc
index 9c5bdeb..37f2fdf 100644
--- a/src/lib/dhcpsrv/alloc_engine.cc
+++ b/src/lib/dhcpsrv/alloc_engine.cc
@@ -16,17 +16,45 @@
#include <dhcpsrv/dhcpsrv_log.h>
#include <dhcpsrv/lease_mgr_factory.h>
+#include <hooks/server_hooks.h>
+#include <hooks/hooks_manager.h>
+
#include <cstring>
#include <vector>
#include <string.h>
using namespace isc::asiolink;
+using namespace isc::hooks;
+
+namespace {
+
+/// Structure that holds registered hook indexes
+struct AllocEngineHooks {
+ int hook_index_lease4_select_; ///< index for "lease4_receive" hook point
+ int hook_index_lease4_renew_; ///< index for "lease4_renew" hook point
+ int hook_index_lease6_select_; ///< index for "lease6_receive" hook point
+
+ /// Constructor that registers hook points for AllocationEngine
+ AllocEngineHooks() {
+ hook_index_lease4_select_ = HooksManager::registerHook("lease4_select");
+ hook_index_lease4_renew_ = HooksManager::registerHook("lease4_renew");
+ hook_index_lease6_select_ = HooksManager::registerHook("lease6_select");
+ }
+};
+
+// Declare a Hooks object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+AllocEngineHooks Hooks;
+
+}; // anonymous namespace
namespace isc {
namespace dhcp {
-AllocEngine::IterativeAllocator::IterativeAllocator()
- :Allocator() {
+AllocEngine::IterativeAllocator::IterativeAllocator(Lease::Type lease_type)
+ :Allocator(lease_type) {
}
isc::asiolink::IOAddress
@@ -57,18 +85,77 @@ AllocEngine::IterativeAllocator::increaseAddress(const isc::asiolink::IOAddress&
return (IOAddress::fromBytes(addr.getFamily(), packed));
}
+isc::asiolink::IOAddress
+AllocEngine::IterativeAllocator::increasePrefix(const isc::asiolink::IOAddress& prefix,
+ const uint8_t prefix_len) {
+ if (!prefix.isV6()) {
+ isc_throw(BadValue, "Prefix operations are for IPv6 only (attempted to "
+ "increase prefix " << prefix << ")");
+ }
+
+ // Get a buffer holding an address.
+ const std::vector<uint8_t>& vec = prefix.toBytes();
+
+ if (prefix_len < 1 || prefix_len > 128) {
+ isc_throw(BadValue, "Cannot increase prefix: invalid prefix length: "
+ << prefix_len);
+ }
+
+ // Brief explanation what happens here:
+ // http://www.youtube.com/watch?v=NFQCYpIHLNQ
+
+ uint8_t n_bytes = (prefix_len - 1)/8;
+ uint8_t n_bits = 8 - (prefix_len - n_bytes*8);
+ uint8_t mask = 1 << n_bits;
+
+ // Longer explanation: n_bytes specifies number of full bytes that are
+ // in-prefix. They can also be used as an offset for the first byte that
+ // is not in prefix. n_bits specifies number of bits on the last byte that
+ // is (often partially) in prefix. For example for a /125 prefix, the values
+ // are 15 and 3, respectively. Mask is a bitmask that has the least
+ // significant bit from the prefix set.
+
+ uint8_t packed[V6ADDRESS_LEN];
+
+ // Copy the address. It must be V6, but we already checked that.
+ std::memcpy(packed, &vec[0], V6ADDRESS_LEN);
+
+ // Can we safely increase only the last byte in prefix without overflow?
+ if (packed[n_bytes] + uint16_t(mask) < 256u) {
+ packed[n_bytes] += mask;
+ return (IOAddress::fromBytes(AF_INET6, packed));
+ }
+
+ // Overflow (done on uint8_t, but the sum is greater than 255)
+ packed[n_bytes] += mask;
+
+ // Deal with the overflow. Start increasing the least significant byte
+ for (int i = n_bytes - 1; i >= 0; --i) {
+ ++packed[i];
+ // If we haven't overflowed (0xff->0x0) the next byte, then we are done
+ if (packed[i] != 0) {
+ break;
+ }
+ }
+
+ return (IOAddress::fromBytes(AF_INET6, packed));
+}
+
isc::asiolink::IOAddress
AllocEngine::IterativeAllocator::pickAddress(const SubnetPtr& subnet,
const DuidPtr&,
const IOAddress&) {
+ // Is this prefix allocation?
+ bool prefix = pool_type_ == Lease::TYPE_PD;
+
// Let's get the last allocated address. It is usually set correctly,
// but there are times when it won't be (like after removing a pool or
- // perhaps restaring the server).
- IOAddress last = subnet->getLastAllocated();
+ // perhaps restarting the server).
+ IOAddress last = subnet->getLastAllocated(pool_type_);
- const PoolCollection& pools = subnet->getPools();
+ const PoolCollection& pools = subnet->getPools(pool_type_);
if (pools.empty()) {
isc_throw(AllocFailed, "No pools defined in selected subnet");
@@ -89,16 +176,28 @@ AllocEngine::IterativeAllocator::pickAddress(const SubnetPtr& subnet,
if (it == pools.end()) {
// ok to access first element directly. We checked that pools is non-empty
IOAddress next = pools[0]->getFirstAddress();
- subnet->setLastAllocated(next);
+ subnet->setLastAllocated(pool_type_, next);
return (next);
}
// Ok, we have a pool that the last address belonged to, let's use it.
- IOAddress next = increaseAddress(last); // basically addr++
+ IOAddress next("::");
+ if (!prefix) {
+ next = increaseAddress(last); // basically addr++
+ } else {
+ Pool6Ptr pool6 = boost::dynamic_pointer_cast<Pool6>(*it);
+ if (!pool6) {
+ // Something is gravely wrong here
+ isc_throw(Unexpected, "Wrong type of pool: " << (*it)->toText()
+ << " is not Pool6");
+ }
+ // Get the next prefix
+ next = increasePrefix(last, pool6->getLength());
+ }
if ((*it)->inRange(next)) {
// the next one is in the pool as well, so we haven't hit pool boundary yet
- subnet->setLastAllocated(next);
+ subnet->setLastAllocated(pool_type_, next);
return (next);
}
@@ -108,18 +207,18 @@ AllocEngine::IterativeAllocator::pickAddress(const SubnetPtr& subnet,
// Really out of luck today. That was the last pool. Let's rewind
// to the beginning.
next = pools[0]->getFirstAddress();
- subnet->setLastAllocated(next);
+ subnet->setLastAllocated(pool_type_, next);
return (next);
}
- // there is a next pool, let's try first adddress from it
+ // there is a next pool, let's try first address from it
next = (*it)->getFirstAddress();
- subnet->setLastAllocated(next);
+ subnet->setLastAllocated(pool_type_, next);
return (next);
}
-AllocEngine::HashedAllocator::HashedAllocator()
- :Allocator() {
+AllocEngine::HashedAllocator::HashedAllocator(Lease::Type lease_type)
+ :Allocator(lease_type) {
isc_throw(NotImplemented, "Hashed allocator is not implemented");
}
@@ -131,8 +230,8 @@ AllocEngine::HashedAllocator::pickAddress(const SubnetPtr&,
isc_throw(NotImplemented, "Hashed allocator is not implemented");
}
-AllocEngine::RandomAllocator::RandomAllocator()
- :Allocator() {
+AllocEngine::RandomAllocator::RandomAllocator(Lease::Type lease_type)
+ :Allocator(lease_type) {
isc_throw(NotImplemented, "Random allocator is not implemented");
}
@@ -145,36 +244,68 @@ AllocEngine::RandomAllocator::pickAddress(const SubnetPtr&,
}
-AllocEngine::AllocEngine(AllocType engine_type, unsigned int attempts)
+AllocEngine::AllocEngine(AllocType engine_type, unsigned int attempts,
+ bool ipv6)
:attempts_(attempts) {
+
+ // Choose the basic (normal address) lease type
+ Lease::Type basic_type = ipv6 ? Lease::TYPE_NA : Lease::TYPE_V4;
+
+ // Initalize normal address allocators
switch (engine_type) {
case ALLOC_ITERATIVE:
- allocator_ = boost::shared_ptr<Allocator>(new IterativeAllocator());
+ allocators_[basic_type] = AllocatorPtr(new IterativeAllocator(basic_type));
break;
case ALLOC_HASHED:
- allocator_ = boost::shared_ptr<Allocator>(new HashedAllocator());
+ allocators_[basic_type] = AllocatorPtr(new HashedAllocator(basic_type));
break;
case ALLOC_RANDOM:
- allocator_ = boost::shared_ptr<Allocator>(new RandomAllocator());
+ allocators_[basic_type] = AllocatorPtr(new RandomAllocator(basic_type));
break;
-
default:
isc_throw(BadValue, "Invalid/unsupported allocation algorithm");
}
+
+ // If this is IPv6 allocation engine, initalize also temporary addrs
+ // and prefixes
+ if (ipv6) {
+ switch (engine_type) {
+ case ALLOC_ITERATIVE:
+ allocators_[Lease::TYPE_TA] = AllocatorPtr(new IterativeAllocator(Lease::TYPE_TA));
+ allocators_[Lease::TYPE_PD] = AllocatorPtr(new IterativeAllocator(Lease::TYPE_PD));
+ break;
+ case ALLOC_HASHED:
+ allocators_[Lease::TYPE_TA] = AllocatorPtr(new HashedAllocator(Lease::TYPE_TA));
+ allocators_[Lease::TYPE_PD] = AllocatorPtr(new HashedAllocator(Lease::TYPE_PD));
+ break;
+ case ALLOC_RANDOM:
+ allocators_[Lease::TYPE_TA] = AllocatorPtr(new RandomAllocator(Lease::TYPE_TA));
+ allocators_[Lease::TYPE_PD] = AllocatorPtr(new RandomAllocator(Lease::TYPE_PD));
+ break;
+ default:
+ isc_throw(BadValue, "Invalid/unsupported allocation algorithm");
+ }
+ }
+
+ // Register hook points
+ hook_index_lease4_select_ = Hooks.hook_index_lease4_select_;
+ hook_index_lease6_select_ = Hooks.hook_index_lease6_select_;
}
-Lease6Ptr
-AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
- const DuidPtr& duid,
- uint32_t iaid,
- const IOAddress& hint,
- bool fake_allocation /* = false */ ) {
+Lease6Collection
+AllocEngine::allocateLeases6(const Subnet6Ptr& subnet, const DuidPtr& duid,
+ uint32_t iaid, const IOAddress& hint,
+ Lease::Type type, const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname, bool fake_allocation,
+ const isc::hooks::CalloutHandlePtr& callout_handle) {
try {
- // That check is not necessary. We create allocator in AllocEngine
- // constructor
- if (!allocator_) {
- isc_throw(InvalidOperation, "No allocator selected");
+ AllocatorPtr allocator = getAllocator(type);
+
+ if (!allocator) {
+ isc_throw(InvalidOperation, "No allocator specified for "
+ << Lease6::typeToText(type));
}
if (!subnet) {
@@ -186,33 +317,54 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
}
// check if there's existing lease for that subnet/duid/iaid combination.
- Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(*duid, iaid, subnet->getID());
- if (existing) {
- // we have a lease already. This is a returning client, probably after
- // his reboot.
+ /// @todo: Make this generic (cover temp. addrs and prefixes)
+ Lease6Collection existing = LeaseMgrFactory::instance().getLeases6(type,
+ *duid, iaid, subnet->getID());
+
+ if (!existing.empty()) {
+ // we have at least one lease already. This is a returning client,
+ // probably after his reboot.
return (existing);
}
// check if the hint is in pool and is available
- if (subnet->inPool(hint)) {
- existing = LeaseMgrFactory::instance().getLease6(hint);
- if (!existing) {
+ // This is equivalent of subnet->inPool(hint), but returns the pool
+ Pool6Ptr pool = boost::dynamic_pointer_cast<Pool6>(subnet->getPool(type, hint, false));
+
+ if (pool) {
+ /// @todo: We support only one hint for now
+ Lease6Ptr lease = LeaseMgrFactory::instance().getLease6(type, hint);
+ if (!lease) {
/// @todo: check if the hint is reserved once we have host support
/// implemented
// the hint is valid and not currently used, let's create a lease for it
- Lease6Ptr lease = createLease6(subnet, duid, iaid, hint, fake_allocation);
+ lease = createLease6(subnet, duid, iaid, hint, pool->getLength(),
+ type, fwd_dns_update, rev_dns_update,
+ hostname, callout_handle, fake_allocation);
// It can happen that the lease allocation failed (we could have lost
// the race condition. That means that the hint is lo longer usable and
// we need to continue the regular allocation path.
if (lease) {
- return (lease);
+ /// @todo: We support only one lease per ia for now
+ Lease6Collection collection;
+ collection.push_back(lease);
+ return (collection);
}
} else {
- if (existing->expired()) {
- return (reuseExpiredLease(existing, subnet, duid, iaid,
- fake_allocation));
+ if (lease->expired()) {
+ /// We found a lease and it is expired, so we can reuse it
+ lease = reuseExpiredLease(lease, subnet, duid, iaid,
+ pool->getLength(),
+ fwd_dns_update, rev_dns_update,
+ hostname, callout_handle,
+ fake_allocation);
+
+ /// @todo: We support only one lease per ia for now
+ Lease6Collection collection;
+ collection.push_back(lease);
+ return (collection);
}
}
@@ -236,19 +388,35 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
unsigned int i = attempts_;
do {
- IOAddress candidate = allocator_->pickAddress(subnet, duid, hint);
+ IOAddress candidate = allocator->pickAddress(subnet, duid, hint);
/// @todo: check if the address is reserved once we have host support
/// implemented
- Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(candidate);
+ // The first step is to find out prefix length. It is 128 for
+ // non-PD leases.
+ uint8_t prefix_len = 128;
+ if (type == Lease::TYPE_PD) {
+ Pool6Ptr pool = boost::dynamic_pointer_cast<Pool6>(
+ subnet->getPool(type, candidate, false));
+ prefix_len = pool->getLength();
+ }
+
+ Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(type,
+ candidate);
if (!existing) {
+
// there's no existing lease for selected candidate, so it is
// free. Let's allocate it.
+
Lease6Ptr lease = createLease6(subnet, duid, iaid, candidate,
- fake_allocation);
+ prefix_len, type, fwd_dns_update,
+ rev_dns_update, hostname,
+ callout_handle, fake_allocation);
if (lease) {
- return (lease);
+ Lease6Collection collection;
+ collection.push_back(lease);
+ return (collection);
}
// Although the address was free just microseconds ago, it may have
@@ -256,8 +424,13 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
// allocation attempts.
} else {
if (existing->expired()) {
- return (reuseExpiredLease(existing, subnet, duid, iaid,
- fake_allocation));
+ existing = reuseExpiredLease(existing, subnet, duid, iaid,
+ prefix_len, fwd_dns_update,
+ rev_dns_update, hostname,
+ callout_handle, fake_allocation);
+ Lease6Collection collection;
+ collection.push_back(existing);
+ return (collection);
}
}
@@ -275,20 +448,29 @@ AllocEngine::allocateAddress6(const Subnet6Ptr& subnet,
LOG_ERROR(dhcpsrv_logger, DHCPSRV_ADDRESS6_ALLOC_ERROR).arg(e.what());
}
- return (Lease6Ptr());
+ return (Lease6Collection());
}
Lease4Ptr
-AllocEngine::allocateAddress4(const SubnetPtr& subnet,
- const ClientIdPtr& clientid,
- const HWAddrPtr& hwaddr,
- const IOAddress& hint,
- bool fake_allocation /* = false */ ) {
+AllocEngine::allocateLease4(const SubnetPtr& subnet, const ClientIdPtr& clientid,
+ const HWAddrPtr& hwaddr, const IOAddress& hint,
+ const bool fwd_dns_update, const bool rev_dns_update,
+ const std::string& hostname, bool fake_allocation,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
+ Lease4Ptr& old_lease) {
+
+ // The NULL pointer indicates that the old lease didn't exist. It may
+ // be later set to non NULL value if existing lease is found in the
+ // database.
+ old_lease.reset();
try {
+
+ AllocatorPtr allocator = getAllocator(Lease::TYPE_V4);
+
// Allocator is always created in AllocEngine constructor and there is
// currently no other way to set it, so that check is not really necessary.
- if (!allocator_) {
+ if (!allocator) {
isc_throw(InvalidOperation, "No allocator selected");
}
@@ -303,9 +485,13 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
// Check if there's existing lease for that subnet/clientid/hwaddr combination.
Lease4Ptr existing = LeaseMgrFactory::instance().getLease4(*hwaddr, subnet->getID());
if (existing) {
+ // Save the old lease, before renewal.
+ old_lease.reset(new Lease4(*existing));
// We have a lease already. This is a returning client, probably after
// its reboot.
- existing = renewLease4(subnet, clientid, hwaddr, existing, fake_allocation);
+ existing = renewLease4(subnet, clientid, hwaddr,
+ fwd_dns_update, rev_dns_update, hostname,
+ existing, callout_handle, fake_allocation);
if (existing) {
return (existing);
}
@@ -317,9 +503,14 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
if (clientid) {
existing = LeaseMgrFactory::instance().getLease4(*clientid, subnet->getID());
if (existing) {
+ // Save the old lease before renewal.
+ old_lease.reset(new Lease4(*existing));
// we have a lease already. This is a returning client, probably after
// its reboot.
- existing = renewLease4(subnet, clientid, hwaddr, existing, fake_allocation);
+ existing = renewLease4(subnet, clientid, hwaddr,
+ fwd_dns_update, rev_dns_update,
+ hostname, existing, callout_handle,
+ fake_allocation);
// @todo: produce a warning. We haven't found him using MAC address, but
// we found him using client-id
if (existing) {
@@ -329,14 +520,17 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
}
// check if the hint is in pool and is available
- if (subnet->inPool(hint)) {
+ if (subnet->inPool(Lease::TYPE_V4, hint)) {
existing = LeaseMgrFactory::instance().getLease4(hint);
if (!existing) {
/// @todo: Check if the hint is reserved once we have host support
/// implemented
// The hint is valid and not currently used, let's create a lease for it
- Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, hint, fake_allocation);
+ Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, hint,
+ fwd_dns_update, rev_dns_update,
+ hostname, callout_handle,
+ fake_allocation);
// It can happen that the lease allocation failed (we could have lost
// the race condition. That means that the hint is lo longer usable and
@@ -346,7 +540,11 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
}
} else {
if (existing->expired()) {
+ // Save the old lease, before reusing it.
+ old_lease.reset(new Lease4(*existing));
return (reuseExpiredLease(existing, subnet, clientid, hwaddr,
+ fwd_dns_update, rev_dns_update,
+ hostname, callout_handle,
fake_allocation));
}
@@ -371,7 +569,7 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
unsigned int i = attempts_;
do {
- IOAddress candidate = allocator_->pickAddress(subnet, clientid, hint);
+ IOAddress candidate = allocator->pickAddress(subnet, clientid, hint);
/// @todo: check if the address is reserved once we have host support
/// implemented
@@ -380,8 +578,10 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
if (!existing) {
// there's no existing lease for selected candidate, so it is
// free. Let's allocate it.
- Lease4Ptr lease = createLease4(subnet, clientid, hwaddr, candidate,
- fake_allocation);
+ Lease4Ptr lease = createLease4(subnet, clientid, hwaddr,
+ candidate, fwd_dns_update,
+ rev_dns_update, hostname,
+ callout_handle, fake_allocation);
if (lease) {
return (lease);
}
@@ -391,7 +591,11 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
// allocation attempts.
} else {
if (existing->expired()) {
+ // Save old lease before reusing it.
+ old_lease.reset(new Lease4(*existing));
return (reuseExpiredLease(existing, subnet, clientid, hwaddr,
+ fwd_dns_update, rev_dns_update,
+ hostname, callout_handle,
fake_allocation));
}
}
@@ -415,9 +619,23 @@ AllocEngine::allocateAddress4(const SubnetPtr& subnet,
Lease4Ptr AllocEngine::renewLease4(const SubnetPtr& subnet,
const ClientIdPtr& clientid,
const HWAddrPtr& hwaddr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
const Lease4Ptr& lease,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /* = false */) {
+ if (!lease) {
+ isc_throw(InvalidOperation, "Lease4 must be specified");
+ }
+
+ // Let's keep the old data. This is essential if we are using memfile
+ // (the lease returned points directly to the lease4 object in the database)
+ // We'll need it if we want to skip update (i.e. roll back renewal)
+ /// @todo: remove this once #3083 is implemented
+ Lease4 old_values = *lease;
+
lease->subnet_id_ = subnet->getID();
lease->hwaddr_ = hwaddr->hwaddr_;
lease->client_id_ = clientid;
@@ -425,11 +643,52 @@ Lease4Ptr AllocEngine::renewLease4(const SubnetPtr& subnet,
lease->t1_ = subnet->getT1();
lease->t2_ = subnet->getT2();
lease->valid_lft_ = subnet->getValid();
+ lease->fqdn_fwd_ = fwd_dns_update;
+ lease->fqdn_rev_ = rev_dns_update;
+ lease->hostname_ = hostname;
+
+ bool skip = false;
+ // Execute all callouts registered for packet6_send
+ if (HooksManager::getHooksManager().calloutsPresent(Hooks.hook_index_lease4_renew_)) {
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Subnet from which we do the allocation. Convert the general subnet
+ // pointer to a pointer to a Subnet4. Note that because we are using
+ // boost smart pointers here, we need to do the cast using the boost
+ // version of dynamic_pointer_cast.
+ Subnet4Ptr subnet4 = boost::dynamic_pointer_cast<Subnet4>(subnet);
+
+ // Pass the parameters
+ callout_handle->setArgument("subnet4", subnet4);
+ callout_handle->setArgument("clientid", clientid);
+ callout_handle->setArgument("hwaddr", hwaddr);
+
+ // Pass the lease to be updated
+ callout_handle->setArgument("lease4", lease);
+
+ // Call all installed callouts
+ HooksManager::callCallouts(Hooks.hook_index_lease4_renew_, *callout_handle);
+
+ // Callouts decided to skip the next processing step. The next
+ // processing step would to actually renew the lease, so skip at this
+ // stage means "keep the old lease as it is".
+ if (callout_handle->getSkip()) {
+ skip = true;
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE4_RENEW_SKIP);
+ }
+ }
- if (!fake_allocation) {
+ if (!fake_allocation && !skip) {
// for REQUEST we do update the lease
LeaseMgrFactory::instance().updateLease4(lease);
}
+ if (skip) {
+ // Rollback changes (really useful only for memfile)
+ /// @todo: remove this once #3083 is implemented
+ *lease = old_values;
+ }
return (lease);
}
@@ -437,13 +696,22 @@ Lease4Ptr AllocEngine::renewLease4(const SubnetPtr& subnet,
Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
const Subnet6Ptr& subnet,
const DuidPtr& duid,
- uint32_t iaid,
+ const uint32_t iaid,
+ uint8_t prefix_len,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /*= false */ ) {
if (!expired->expired()) {
isc_throw(BadValue, "Attempt to recycle lease that is still valid");
}
+ if (expired->type_ != Lease::TYPE_PD) {
+ prefix_len = 128; // non-PD lease types must be always /128
+ }
+
// address, lease type and prefixlen (0) stay the same
expired->iaid_ = iaid;
expired->duid_ = duid;
@@ -454,13 +722,47 @@ Lease6Ptr AllocEngine::reuseExpiredLease(Lease6Ptr& expired,
expired->cltt_ = time(NULL);
expired->subnet_id_ = subnet->getID();
expired->fixed_ = false;
- expired->hostname_ = std::string("");
- expired->fqdn_fwd_ = false;
- expired->fqdn_rev_ = false;
+ expired->hostname_ = hostname;
+ expired->fqdn_fwd_ = fwd_dns_update;
+ expired->fqdn_rev_ = rev_dns_update;
+ expired->prefixlen_ = prefix_len;
/// @todo: log here that the lease was reused (there's ticket #2524 for
/// logging in libdhcpsrv)
+ // Let's execute all callouts registered for lease6_select
+ if (callout_handle &&
+ HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass necessary arguments
+ // Subnet from which we do the allocation
+ callout_handle->setArgument("subnet6", subnet);
+
+ // Is this solicit (fake = true) or request (fake = false)
+ callout_handle->setArgument("fake_allocation", fake_allocation);
+
+ // The lease that will be assigned to a client
+ callout_handle->setArgument("lease6", expired);
+
+ // Call the callouts
+ HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+ // Callouts decided to skip the action. This means that the lease is not
+ // assigned, so the client will get NoAddrAvail as a result. The lease
+ // won't be inserted into the database.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_SELECT_SKIP);
+ return (Lease6Ptr());
+ }
+
+ // Let's use whatever callout returned. Hopefully it is the same lease
+ // we handled to it.
+ callout_handle->getArgument("lease6", expired);
+ }
+
if (!fake_allocation) {
// for REQUEST we do update the lease
LeaseMgrFactory::instance().updateLease6(expired);
@@ -478,6 +780,10 @@ Lease4Ptr AllocEngine::reuseExpiredLease(Lease4Ptr& expired,
const SubnetPtr& subnet,
const ClientIdPtr& clientid,
const HWAddrPtr& hwaddr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /*= false */ ) {
if (!expired->expired()) {
@@ -493,13 +799,51 @@ Lease4Ptr AllocEngine::reuseExpiredLease(Lease4Ptr& expired,
expired->cltt_ = time(NULL);
expired->subnet_id_ = subnet->getID();
expired->fixed_ = false;
- expired->hostname_ = std::string("");
- expired->fqdn_fwd_ = false;
- expired->fqdn_rev_ = false;
+ expired->hostname_ = hostname;
+ expired->fqdn_fwd_ = fwd_dns_update;
+ expired->fqdn_rev_ = rev_dns_update;
/// @todo: log here that the lease was reused (there's ticket #2524 for
/// logging in libdhcpsrv)
+ // Let's execute all callouts registered for lease4_select
+ if (callout_handle &&
+ HooksManager::getHooksManager().calloutsPresent(hook_index_lease4_select_)) {
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass necessary arguments
+
+ // Subnet from which we do the allocation. Convert the general subnet
+ // pointer to a pointer to a Subnet4. Note that because we are using
+ // boost smart pointers here, we need to do the cast using the boost
+ // version of dynamic_pointer_cast.
+ Subnet4Ptr subnet4 = boost::dynamic_pointer_cast<Subnet4>(subnet);
+ callout_handle->setArgument("subnet4", subnet4);
+
+ // Is this solicit (fake = true) or request (fake = false)
+ callout_handle->setArgument("fake_allocation", fake_allocation);
+
+ // The lease that will be assigned to a client
+ callout_handle->setArgument("lease4", expired);
+
+ // Call the callouts
+ HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+ // Callouts decided to skip the action. This means that the lease is not
+ // assigned, so the client will get NoAddrAvail as a result. The lease
+ // won't be inserted into the database.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE4_SELECT_SKIP);
+ return (Lease4Ptr());
+ }
+
+ // Let's use whatever callout returned. Hopefully it is the same lease
+ // we handled to it.
+ callout_handle->getArgument("lease4", expired);
+ }
+
if (!fake_allocation) {
// for REQUEST we do update the lease
LeaseMgrFactory::instance().updateLease4(expired);
@@ -515,13 +859,60 @@ Lease4Ptr AllocEngine::reuseExpiredLease(Lease4Ptr& expired,
Lease6Ptr AllocEngine::createLease6(const Subnet6Ptr& subnet,
const DuidPtr& duid,
- uint32_t iaid,
+ const uint32_t iaid,
const IOAddress& addr,
+ uint8_t prefix_len,
+ const Lease::Type type,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /*= false */ ) {
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, duid, iaid,
+ if (type != Lease::TYPE_PD) {
+ prefix_len = 128; // non-PD lease types must be always /128
+ }
+
+ Lease6Ptr lease(new Lease6(type, addr, duid, iaid,
subnet->getPreferred(), subnet->getValid(),
- subnet->getT1(), subnet->getT2(), subnet->getID()));
+ subnet->getT1(), subnet->getT2(), subnet->getID(),
+ prefix_len));
+
+ lease->fqdn_fwd_ = fwd_dns_update;
+ lease->fqdn_rev_ = rev_dns_update;
+ lease->hostname_ = hostname;
+
+ // Let's execute all callouts registered for lease6_select
+ if (callout_handle &&
+ HooksManager::getHooksManager().calloutsPresent(hook_index_lease6_select_)) {
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass necessary arguments
+
+ // Subnet from which we do the allocation
+ callout_handle->setArgument("subnet6", subnet);
+
+ // Is this solicit (fake = true) or request (fake = false)
+ callout_handle->setArgument("fake_allocation", fake_allocation);
+ callout_handle->setArgument("lease6", lease);
+
+ // This is the first callout, so no need to clear any arguments
+ HooksManager::callCallouts(hook_index_lease6_select_, *callout_handle);
+
+ // Callouts decided to skip the action. This means that the lease is not
+ // assigned, so the client will get NoAddrAvail as a result. The lease
+ // won't be inserted into the database.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE6_SELECT_SKIP);
+ return (Lease6Ptr());
+ }
+
+ // Let's use whatever callout returned. Hopefully it is the same lease
+ // we handled to it.
+ callout_handle->getArgument("lease6", lease);
+ }
if (!fake_allocation) {
// That is a real (REQUEST) allocation
@@ -542,7 +933,8 @@ Lease6Ptr AllocEngine::createLease6(const Subnet6Ptr& subnet,
// It is for advertise only. We should not insert the lease into LeaseMgr,
// but rather check that we could have inserted it.
- Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(addr);
+ Lease6Ptr existing = LeaseMgrFactory::instance().getLease6(
+ Lease::TYPE_NA, addr);
if (!existing) {
return (lease);
} else {
@@ -555,6 +947,10 @@ Lease4Ptr AllocEngine::createLease4(const SubnetPtr& subnet,
const DuidPtr& clientid,
const HWAddrPtr& hwaddr,
const IOAddress& addr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /*= false */ ) {
if (!hwaddr) {
isc_throw(BadValue, "Can't create a lease with NULL HW address");
@@ -572,6 +968,49 @@ Lease4Ptr AllocEngine::createLease4(const SubnetPtr& subnet,
subnet->getT1(), subnet->getT2(), now,
subnet->getID()));
+ // Set FQDN specific lease parameters.
+ lease->fqdn_fwd_ = fwd_dns_update;
+ lease->fqdn_rev_ = rev_dns_update;
+ lease->hostname_ = hostname;
+
+ // Let's execute all callouts registered for lease4_select
+ if (callout_handle &&
+ HooksManager::getHooksManager().calloutsPresent(hook_index_lease4_select_)) {
+
+ // Delete all previous arguments
+ callout_handle->deleteAllArguments();
+
+ // Pass necessary arguments
+
+ // Subnet from which we do the allocation (That's as far as we can go
+ // with using SubnetPtr to point to Subnet4 object. Users should not
+ // be confused with dynamic_pointer_casts. They should get a concrete
+ // pointer (Subnet4Ptr) pointing to a Subnet4 object.
+ Subnet4Ptr subnet4 = boost::dynamic_pointer_cast<Subnet4>(subnet);
+ callout_handle->setArgument("subnet4", subnet4);
+
+ // Is this solicit (fake = true) or request (fake = false)
+ callout_handle->setArgument("fake_allocation", fake_allocation);
+
+ // Pass the intended lease as well
+ callout_handle->setArgument("lease4", lease);
+
+ // This is the first callout, so no need to clear any arguments
+ HooksManager::callCallouts(hook_index_lease4_select_, *callout_handle);
+
+ // Callouts decided to skip the action. This means that the lease is not
+ // assigned, so the client will get NoAddrAvail as a result. The lease
+ // won't be inserted into the database.
+ if (callout_handle->getSkip()) {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_HOOKS, DHCPSRV_HOOK_LEASE4_SELECT_SKIP);
+ return (Lease4Ptr());
+ }
+
+ // Let's use whatever callout returned. Hopefully it is the same lease
+ // we handled to it.
+ callout_handle->getArgument("lease4", lease);
+ }
+
if (!fake_allocation) {
// That is a real (REQUEST) allocation
bool status = LeaseMgrFactory::instance().addLease(lease);
@@ -598,6 +1037,16 @@ Lease4Ptr AllocEngine::createLease4(const SubnetPtr& subnet,
}
}
+AllocEngine::AllocatorPtr AllocEngine::getAllocator(Lease::Type type) {
+ std::map<Lease::Type, AllocatorPtr>::const_iterator alloc = allocators_.find(type);
+
+ if (alloc == allocators_.end()) {
+ isc_throw(BadValue, "No allocator initialized for pool type "
+ << Lease::typeToText(type));
+ }
+ return (alloc->second);
+}
+
AllocEngine::~AllocEngine() {
// no need to delete allocator. smart_ptr will do the trick for us
}
diff --git a/src/lib/dhcpsrv/alloc_engine.h b/src/lib/dhcpsrv/alloc_engine.h
index 7e3d136..8299bb8 100644
--- a/src/lib/dhcpsrv/alloc_engine.h
+++ b/src/lib/dhcpsrv/alloc_engine.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -20,10 +20,13 @@
#include <dhcp/hwaddr.h>
#include <dhcpsrv/subnet.h>
#include <dhcpsrv/lease_mgr.h>
+#include <hooks/callout_handle.h>
#include <boost/shared_ptr.hpp>
#include <boost/noncopyable.hpp>
+#include <map>
+
namespace isc {
namespace dhcp {
@@ -67,6 +70,14 @@ protected:
/// again if necessary. The number of times this method is called will
/// increase as the number of available leases will decrease.
///
+ /// This method can also be used to pick a prefix. We should not rename
+ /// it to pickLease(), because at this early stage there is no concept
+ /// of a lease yet. Here it is a matter of selecting one address or
+ /// prefix from the defined pool, without going into details who it is
+ /// for or who uses it. I thought that pickAddress() is less confusing
+ /// than pickResource(), because nobody would immediately know what the
+ /// resource means in this context.
+ ///
/// @param subnet next address will be returned from pool of that subnet
/// @param duid Client's DUID
/// @param hint client's hint
@@ -76,12 +87,26 @@ protected:
pickAddress(const SubnetPtr& subnet, const DuidPtr& duid,
const isc::asiolink::IOAddress& hint) = 0;
+ /// @brief Default constructor.
+ ///
+ /// Specifies which type of leases this allocator will assign
+ /// @param pool_type specifies pool type (addresses, temp. addr or prefixes)
+ Allocator(Lease::Type pool_type)
+ :pool_type_(pool_type) {
+ }
+
/// @brief virtual destructor
virtual ~Allocator() {
}
protected:
+
+ /// @brief defines pool type allocation
+ Lease::Type pool_type_;
};
+ /// defines a pointer to allocator
+ typedef boost::shared_ptr<Allocator> AllocatorPtr;
+
/// @brief Address/prefix allocator that iterates over all addresses
///
/// This class implements iterative algorithm that returns all addresses in
@@ -94,7 +119,8 @@ protected:
/// @brief default constructor
///
/// Does not do anything
- IterativeAllocator();
+ /// @param type - specifies allocation type
+ IterativeAllocator(Lease::Type type);
/// @brief returns the next address from pools in a subnet
///
@@ -106,13 +132,31 @@ protected:
pickAddress(const SubnetPtr& subnet,
const DuidPtr& duid,
const isc::asiolink::IOAddress& hint);
- private:
+ protected:
- /// @brief returns an address by one
+ /// @brief Returns an address increased by one
+ ///
+ /// This method works for both IPv4 and IPv6 addresses. For example,
+ /// increase 192.0.2.255 will become 192.0.3.0.
+ ///
/// @param addr address to be increased
/// @return address increased by one
- isc::asiolink::IOAddress increaseAddress(const isc::asiolink::IOAddress& addr);
+ static isc::asiolink::IOAddress
+ increaseAddress(const isc::asiolink::IOAddress& addr);
+ /// @brief Returns the next prefix
+ ///
+ /// This method works for IPv6 addresses only. It increases
+ /// specified prefix by a given prefix_len. For example, 2001:db8::
+ /// increased by prefix length /32 will become 2001:db9::. This method
+ /// is used to iterate over IPv6 prefix pools
+ ///
+ /// @param prefix prefix to be increased
+ /// @param prefix_len length of the prefix to be increased
+ /// @return result prefix
+ static isc::asiolink::IOAddress
+ increasePrefix(const isc::asiolink::IOAddress& prefix,
+ const uint8_t prefix_len);
};
/// @brief Address/prefix allocator that gets an address based on a hash
@@ -122,7 +166,8 @@ protected:
public:
/// @brief default constructor (does nothing)
- HashedAllocator();
+ /// @param type - specifies allocation type
+ HashedAllocator(Lease::Type type);
/// @brief returns an address based on hash calculated from client's DUID.
///
@@ -144,7 +189,8 @@ protected:
public:
/// @brief default constructor (does nothing)
- RandomAllocator();
+ /// @param type - specifies allocation type
+ RandomAllocator(Lease::Type type);
/// @brief returns an random address from pool of specified subnet
///
@@ -179,27 +225,63 @@ protected:
/// @param engine_type selects allocation algorithm
/// @param attempts number of attempts for each lease allocation before
/// we give up (0 means unlimited)
- AllocEngine(AllocType engine_type, unsigned int attempts);
+ /// @param ipv6 specifies if the engine should work for IPv4 or IPv6
+ AllocEngine(AllocType engine_type, unsigned int attempts, bool ipv6 = true);
- /// @brief Allocates an IPv4 lease
+ /// @brief Returns IPv4 lease.
///
- /// This method uses currently selected allocator to pick an address from
- /// specified subnet, creates a lease for that address and then inserts
- /// it into LeaseMgr (if this allocation is not fake).
+ /// This method finds the appropriate lease for the client using the
+ /// following algorithm:
+ /// - If lease exists for the combination of the HW address, client id and
+ /// subnet, try to renew a lease and return it.
+ /// - If lease exists for the combination of the client id and subnet, try
+ /// to renew the lease and return it.
+ /// - If client supplied an address hint and this address is available,
+ /// allocate the new lease with this address.
+ /// - If client supplied an address hint and the lease for this address
+ /// exists in the database, return this lease if it is expired.
+ /// - Pick new address from the pool and try to allocate it for the client,
+ /// if expired lease exists for the picked address, try to reuse this lease.
+ ///
+ /// When a server should do DNS updates, it is required that allocation
+ /// returns the information how the lease was obtained by the allocation
+ /// engine. In particular, the DHCP server should be able to check whether
+ /// existing lease was returned, or new lease was allocated. When existing
+ /// lease was returned, server should check whether the FQDN has changed
+ /// between the allocation of the old and new lease. If so, server should
+ /// perform appropriate DNS update. If not, server may choose to not
+ /// perform the update. The information about the old lease is returned via
+ /// @c old_lease parameter. If NULL value is returned, it is an indication
+ /// that new lease was allocated for the client. If non-NULL value is
+ /// returned, it is an indication that allocation engine reused/renewed an
+ /// existing lease.
///
/// @param subnet subnet the allocation should come from
/// @param clientid Client identifier
- /// @param hwaddr client's hardware address info
- /// @param hint a hint that the client provided
- /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
+ /// @param hwaddr Client's hardware address info
+ /// @param hint A hint that the client provided
+ /// @param fwd_dns_update Indicates whether forward DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param rev_dns_update Indicates whether reverse DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param hostname A string carrying hostname to be used for DNS updates.
+ /// @param fake_allocation Is this real i.e. REQUEST (false) or just picking
/// an address for DISCOVER that is not really allocated (true)
+ /// @param callout_handle A callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed.
+ /// @param [out] old_lease Holds the pointer to a previous instance of a
+ /// lease. The NULL pointer indicates that lease didn't exist prior
+ /// to calling this function (e.g. new lease has been allocated).
+ ///
/// @return Allocated IPv4 lease (or NULL if allocation failed)
Lease4Ptr
- allocateAddress4(const SubnetPtr& subnet,
- const ClientIdPtr& clientid,
- const HWAddrPtr& hwaddr,
- const isc::asiolink::IOAddress& hint,
- bool fake_allocation);
+ allocateLease4(const SubnetPtr& subnet, const ClientIdPtr& clientid,
+ const HWAddrPtr& hwaddr,
+ const isc::asiolink::IOAddress& hint,
+ const bool fwd_dns_update, const bool rev_dns_update,
+ const std::string& hostname, bool fake_allocation,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
+ Lease4Ptr& old_lease);
/// @brief Renews a IPv4 lease
///
@@ -210,17 +292,28 @@ protected:
/// to get a new lease. It thinks that it gets a new lease, but in fact
/// we are only renewing the still valid lease for that client.
///
- /// @param subnet subnet the client is attached to
- /// @param clientid client identifier
- /// @param hwaddr client's hardware address
- /// @param lease lease to be renewed
- /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
+ /// @param subnet A subnet the client is attached to
+ /// @param clientid Client identifier
+ /// @param hwaddr Client's hardware address
+ /// @param fwd_dns_update Indicates whether forward DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param rev_dns_update Indicates whether reverse DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param hostname A string carrying hostname to be used for DNS updates.
+ /// @param lease A lease to be renewed
+ /// @param callout_handle a callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed.
+ /// @param fake_allocation Is this real i.e. REQUEST (false) or just picking
/// an address for DISCOVER that is not really allocated (true)
Lease4Ptr
renewLease4(const SubnetPtr& subnet,
const ClientIdPtr& clientid,
const HWAddrPtr& hwaddr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
const Lease4Ptr& lease,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation /* = false */);
/// @brief Allocates an IPv6 lease
@@ -233,15 +326,32 @@ protected:
/// @param duid Client's DUID
/// @param iaid iaid field from the IA_NA container that client sent
/// @param hint a hint that the client provided
+ /// @param type lease type (IA, TA or PD)
+ /// @param fwd_dns_update A boolean value which indicates that server takes
+ /// responsibility for the forward DNS Update for this lease
+ /// (if true).
+ /// @param rev_dns_update A boolean value which indicates that server takes
+ /// responsibility for the reverse DNS Update for this lease
+ /// (if true).
+ /// @param hostname A fully qualified domain-name of the client.
/// @param fake_allocation is this real i.e. REQUEST (false) or just picking
/// an address for SOLICIT that is not really allocated (true)
- /// @return Allocated IPv6 lease (or NULL if allocation failed)
- Lease6Ptr
- allocateAddress6(const Subnet6Ptr& subnet,
- const DuidPtr& duid,
- uint32_t iaid,
- const isc::asiolink::IOAddress& hint,
- bool fake_allocation);
+ /// @param callout_handle a callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed.
+ ///
+ /// @return Allocated IPv6 leases (may be empty if allocation failed)
+ Lease6Collection
+ allocateLeases6(const Subnet6Ptr& subnet, const DuidPtr& duid, uint32_t iaid,
+ const isc::asiolink::IOAddress& hint, Lease::Type type,
+ const bool fwd_dns_update, const bool rev_dns_update,
+ const std::string& hostname, bool fake_allocation,
+ const isc::hooks::CalloutHandlePtr& callout_handle);
+
+ /// @brief returns allocator for a given pool type
+ /// @param type type of pool (V4, IA, TA or PD)
+ /// @throw BadValue if allocator for a given type is missing
+ /// @return pointer to allocator handing a given resource types
+ AllocatorPtr getAllocator(Lease::Type type);
/// @brief Destructor. Used during DHCPv6 service shutdown.
virtual ~AllocEngine();
@@ -253,17 +363,29 @@ private:
/// into the database. That may fail in some cases, e.g. when there is another
/// allocation process and we lost a race to a specific lease.
///
- /// @param subnet subnet the lease is allocated from
- /// @param clientid client identifier
- /// @param hwaddr client's hardware address
- /// @param addr an address that was selected and is confirmed to be available
- /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
+ /// @param subnet Subnet the lease is allocated from
+ /// @param clientid Client identifier
+ /// @param hwaddr Client's hardware address
+ /// @param addr An address that was selected and is confirmed to be available
+ /// @param fwd_dns_update Indicates whether forward DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param rev_dns_update Indicates whether reverse DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param hostname A string carrying hostname to be used for DNS updates.
+ /// @param callout_handle a callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed (and there are callouts
+ /// registered)
+ /// @param fake_allocation Is this real i.e. REQUEST (false) or just picking
/// an address for DISCOVER that is not really allocated (true)
/// @return allocated lease (or NULL in the unlikely case of the lease just
/// becomed unavailable)
Lease4Ptr createLease4(const SubnetPtr& subnet, const DuidPtr& clientid,
const HWAddrPtr& hwaddr,
const isc::asiolink::IOAddress& addr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation = false);
/// @brief creates a lease and inserts it in LeaseMgr if necessary
@@ -275,13 +397,31 @@ private:
/// @param subnet subnet the lease is allocated from
/// @param duid client's DUID
/// @param iaid IAID from the IA_NA container the client sent to us
- /// @param addr an address that was selected and is confirmed to be available
+ /// @param addr an address that was selected and is confirmed to be
+ /// available
+ /// @param prefix_len length of the prefix (for PD only)
+ /// should be 128 for other lease types
+ /// @param type lease type (IA, TA or PD)
+ /// @param fwd_dns_update A boolean value which indicates that server takes
+ /// responsibility for the forward DNS Update for this lease
+ /// (if true).
+ /// @param rev_dns_update A boolean value which indicates that server takes
+ /// responsibility for the reverse DNS Update for this lease
+ /// (if true).
+ /// @param hostname A fully qualified domain-name of the client.
+ /// @param callout_handle a callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed (and there are callouts
+ /// registered)
/// @param fake_allocation is this real i.e. REQUEST (false) or just picking
/// an address for SOLICIT that is not really allocated (true)
/// @return allocated lease (or NULL in the unlikely case of the lease just
- /// becomed unavailable)
+ /// became unavailable)
Lease6Ptr createLease6(const Subnet6Ptr& subnet, const DuidPtr& duid,
- uint32_t iaid, const isc::asiolink::IOAddress& addr,
+ const uint32_t iaid, const isc::asiolink::IOAddress& addr,
+ const uint8_t prefix_len, const Lease::Type type,
+ const bool fwd_dns_update, const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation = false);
/// @brief Reuses expired IPv4 lease
@@ -290,17 +430,29 @@ private:
/// is updated if this is real (i.e. REQUEST, fake_allocation = false), not
/// dummy allocation request (i.e. DISCOVER, fake_allocation = true).
///
- /// @param expired old, expired lease
- /// @param subnet subnet the lease is allocated from
- /// @param clientid client identifier
- /// @param hwaddr client's hardware address
- /// @param fake_allocation is this real i.e. REQUEST (false) or just picking
+ /// @param expired Old, expired lease
+ /// @param subnet Subnet the lease is allocated from
+ /// @param clientid Client identifier
+ /// @param hwaddr Client's hardware address
+ /// @param fwd_dns_update Indicates whether forward DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param rev_dns_update Indicates whether reverse DNS update will be
+ /// performed for the client (true) or not (false).
+ /// @param hostname A string carrying hostname to be used for DNS updates.
+ /// @param callout_handle A callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed.
+ /// @param fake_allocation Is this real i.e. REQUEST (false) or just picking
/// an address for DISCOVER that is not really allocated (true)
/// @return refreshed lease
/// @throw BadValue if trying to recycle lease that is still valid
- Lease4Ptr reuseExpiredLease(Lease4Ptr& expired, const SubnetPtr& subnet,
+ Lease4Ptr reuseExpiredLease(Lease4Ptr& expired,
+ const SubnetPtr& subnet,
const ClientIdPtr& clientid,
const HWAddrPtr& hwaddr,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation = false);
/// @brief Reuses expired IPv6 lease
@@ -313,19 +465,42 @@ private:
/// @param subnet subnet the lease is allocated from
/// @param duid client's DUID
/// @param iaid IAID from the IA_NA container the client sent to us
+ /// @param prefix_len prefix length (for PD leases)
+ /// Should be 128 for other lease types
+ /// @param fwd_dns_update A boolean value which indicates that server takes
+ /// responsibility for the forward DNS Update for this lease
+ /// (if true).
+ /// @param rev_dns_update A boolean value which indicates that server takes
+ /// responsibility for the reverse DNS Update for this lease
+ /// (if true).
+ /// @param hostname A fully qualified domain-name of the client.
+ /// @param callout_handle a callout handle (used in hooks). A lease callouts
+ /// will be executed if this parameter is passed.
/// @param fake_allocation is this real i.e. REQUEST (false) or just picking
/// an address for SOLICIT that is not really allocated (true)
/// @return refreshed lease
/// @throw BadValue if trying to recycle lease that is still valid
Lease6Ptr reuseExpiredLease(Lease6Ptr& expired, const Subnet6Ptr& subnet,
- const DuidPtr& duid, uint32_t iaid,
+ const DuidPtr& duid, const uint32_t iaid,
+ uint8_t prefix_len,
+ const bool fwd_dns_update,
+ const bool rev_dns_update,
+ const std::string& hostname,
+ const isc::hooks::CalloutHandlePtr& callout_handle,
bool fake_allocation = false);
/// @brief a pointer to currently used allocator
- boost::shared_ptr<Allocator> allocator_;
+ ///
+ /// For IPv4, there will be only one allocator: TYPE_V4
+ /// For IPv6, there will be 3 allocators: TYPE_NA, TYPE_TA, TYPE_PD
+ std::map<Lease::Type, AllocatorPtr> allocators_;
/// @brief number of attempts before we give up lease allocation (0=unlimited)
unsigned int attempts_;
+
+ // hook name indexes (used in hooks callouts)
+ int hook_index_lease4_select_; ///< index for lease4_select hook
+ int hook_index_lease6_select_; ///< index for lease6_select hook
};
}; // namespace isc::dhcp
diff --git a/src/lib/dhcpsrv/callout_handle_store.h b/src/lib/dhcpsrv/callout_handle_store.h
new file mode 100644
index 0000000..697ba3a
--- /dev/null
+++ b/src/lib/dhcpsrv/callout_handle_store.h
@@ -0,0 +1,91 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef CALLOUT_HANDLE_STORE_H
+#define CALLOUT_HANDLE_STORE_H
+
+#include <hooks/hooks_manager.h>
+#include <hooks/callout_handle.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief CalloutHandle Store
+///
+/// When using the Hooks Framework, there is a need to associate an
+/// isc::hooks::CalloutHandle object with each request passing through the
+/// server. For the DHCP servers, the association is provided by this function.
+///
+/// The DHCP servers process a single request at a time. At points where the
+/// CalloutHandle is required, the pointer to the current request (packet) is
+/// passed to this function. If the request is a new one, a pointer to
+/// the request is stored, a new CalloutHandle is allocated (and stored) and
+/// a pointer to the latter object returned to the caller. If the request
+/// matches the one stored, the pointer to the stored CalloutHandle is
+/// returned.
+///
+/// A special case is a null pointer being passed. This has the effect of
+/// clearing the stored pointers to the packet being processed and
+/// CalloutHandle. As the stored pointers are shared pointers, clearing them
+/// removes one reference that keeps the pointed-to objects in existence.
+///
+/// @note If the behaviour of the server changes so that multiple packets can
+/// be active at the same time, this simplistic approach will no longer
+/// be adequate and a more complicated structure (such as a map) will
+/// be needed.
+///
+/// @param pktptr Pointer to the packet being processed. This is typically a
+/// Pkt4Ptr or Pkt6Ptr object. An empty pointer is passed to clear
+/// the stored pointers.
+///
+/// @return Shared pointer to a CalloutHandle. This is the previously-stored
+/// CalloutHandle if pktptr points to a packet that has been seen
+/// before or a new CalloutHandle if it points to a new one. An empty
+/// pointer is returned if pktptr is itself an empty pointer.
+
+template <typename T>
+isc::hooks::CalloutHandlePtr getCalloutHandle(const T& pktptr) {
+
+ // Stored data is declared static, so is initialized when first accessed
+ static T stored_pointer; // Pointer to last packet seen
+ static isc::hooks::CalloutHandlePtr stored_handle;
+ // Pointer to stored handle
+
+ if (pktptr) {
+
+ // Pointer given, have we seen it before? (If we have, we don't need to
+ // do anything as we will automatically return the stored handle.)
+ if (pktptr != stored_pointer) {
+
+ // Not seen before, so store the pointer passed to us and get a new
+ // CalloutHandle. (The latter operation frees and probably deletes
+ // (depending on other pointers) the stored one.)
+ stored_pointer = pktptr;
+ stored_handle = isc::hooks::HooksManager::createCalloutHandle();
+ }
+
+ } else {
+
+ // Empty pointer passed, clear stored data
+ stored_pointer.reset();
+ stored_handle.reset();
+ }
+
+ return (stored_handle);
+}
+
+} // namespace shcp
+} // namespace isc
+
+#endif // CALLOUT_HANDLE_STORE_H
diff --git a/src/lib/dhcpsrv/cfgmgr.cc b/src/lib/dhcpsrv/cfgmgr.cc
index b5e83e3..6f008f1 100644
--- a/src/lib/dhcpsrv/cfgmgr.cc
+++ b/src/lib/dhcpsrv/cfgmgr.cc
@@ -16,6 +16,7 @@
#include <dhcp/libdhcp++.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/dhcpsrv_log.h>
+#include <string>
using namespace isc::asiolink;
using namespace isc::util;
@@ -40,8 +41,7 @@ CfgMgr::addOptionSpace4(const OptionSpacePtr& space) {
isc_throw(InvalidOptionSpace, "option space " << space->getName()
<< " already added.");
}
- spaces4_.insert(std::pair<std::string,
- OptionSpacePtr>(space->getName(), space));
+ spaces4_.insert(make_pair(space->getName(), space));
}
void
@@ -55,8 +55,7 @@ CfgMgr::addOptionSpace6(const OptionSpacePtr& space) {
isc_throw(InvalidOptionSpace, "option space " << space->getName()
<< " already added.");
}
- spaces6_.insert(std::pair<std::string,
- OptionSpacePtr>(space->getName(), space));
+ spaces6_.insert(make_pair(space->getName(), space));
}
void
@@ -147,7 +146,7 @@ CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
// If there's only one subnet configured, let's just use it
// The idea is to keep small deployments easy. In a small network - one
- // router that also runs DHCPv6 server. Users specifies a single pool and
+ // router that also runs DHCPv6 server. User specifies a single pool and
// expects it to just work. Without this, the server would complain that it
// doesn't have IP address on its interfaces that matches that
// configuration. Such requirement makes sense in IPv4, but not in IPv6.
@@ -178,14 +177,30 @@ CfgMgr::getSubnet6(const isc::asiolink::IOAddress& hint) {
return (Subnet6Ptr());
}
-Subnet6Ptr CfgMgr::getSubnet6(OptionPtr /*interfaceId*/) {
- /// @todo: Implement get subnet6 by interface-id (for relayed traffic)
- isc_throw(NotImplemented, "Relayed DHCPv6 traffic is not supported yet.");
+Subnet6Ptr CfgMgr::getSubnet6(OptionPtr iface_id_option) {
+ if (!iface_id_option) {
+ return (Subnet6Ptr());
+ }
+
+ // Let's iterate over all subnets and for those that have interface-id
+ // defined, check if the interface-id is equal to what we are looking for
+ for (Subnet6Collection::iterator subnet = subnets6_.begin();
+ subnet != subnets6_.end(); ++subnet) {
+ if ( (*subnet)->getInterfaceId() &&
+ ((*subnet)->getInterfaceId()->equal(iface_id_option))) {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+ DHCPSRV_CFGMGR_SUBNET6_IFACE_ID)
+ .arg((*subnet)->toText());
+ return (*subnet);
+ }
+ }
+ return (Subnet6Ptr());
}
void CfgMgr::addSubnet6(const Subnet6Ptr& subnet) {
/// @todo: Check that this new subnet does not cross boundaries of any
/// other already defined subnet.
+ /// @todo: Check that there is no subnet with the same interface-id
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_SUBNET6)
.arg(subnet->toText());
subnets6_.push_back(subnet);
@@ -248,13 +263,111 @@ void CfgMgr::deleteSubnets6() {
subnets6_.clear();
}
+
std::string CfgMgr::getDataDir() {
return (datadir_);
}
+void
+CfgMgr::addActiveIface(const std::string& iface) {
+
+ size_t pos = iface.find("/");
+ std::string iface_copy = iface;
+
+ if (pos != std::string::npos) {
+ std::string addr_string = iface.substr(pos + 1);
+ try {
+ IOAddress addr(addr_string);
+ iface_copy = iface.substr(0,pos);
+ unicast_addrs_.insert(make_pair(iface_copy, addr));
+ } catch (...) {
+ isc_throw(BadValue, "Can't convert '" << addr_string
+ << "' into address in interface defition ('"
+ << iface << "')");
+ }
+ }
+
+ if (isIfaceListedActive(iface_copy)) {
+ isc_throw(DuplicateListeningIface,
+ "attempt to add duplicate interface '" << iface_copy << "'"
+ " to the set of interfaces on which server listens");
+ }
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_ADD_IFACE)
+ .arg(iface_copy);
+ active_ifaces_.push_back(iface_copy);
+}
+
+void
+CfgMgr::activateAllIfaces() {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+ DHCPSRV_CFGMGR_ALL_IFACES_ACTIVE);
+ all_ifaces_active_ = true;
+}
+
+void
+CfgMgr::deleteActiveIfaces() {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE,
+ DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES);
+ active_ifaces_.clear();
+ all_ifaces_active_ = false;
+
+ unicast_addrs_.clear();
+}
+
+bool
+CfgMgr::isActiveIface(const std::string& iface) const {
+
+ // @todo Verify that the interface with the specified name is
+ // present in the system.
+
+ // If all interfaces are marked active, there is no need to check that
+ // the name of this interface has been explicitly listed.
+ if (all_ifaces_active_) {
+ return (true);
+ }
+ return (isIfaceListedActive(iface));
+}
+
+bool
+CfgMgr::isIfaceListedActive(const std::string& iface) const {
+ for (ActiveIfacesCollection::const_iterator it = active_ifaces_.begin();
+ it != active_ifaces_.end(); ++it) {
+ if (iface == *it) {
+ return (true);
+ }
+ }
+ return (false);
+}
+
+const isc::asiolink::IOAddress*
+CfgMgr::getUnicast(const std::string& iface) const {
+ UnicastIfacesCollection::const_iterator addr = unicast_addrs_.find(iface);
+ if (addr == unicast_addrs_.end()) {
+ return (NULL);
+ }
+ return (&(*addr).second);
+}
+
+void
+CfgMgr::setD2ClientConfig(D2ClientConfigPtr& new_config) {
+ d2_client_mgr_.setD2ClientConfig(new_config);
+}
+
+bool
+CfgMgr::ddnsEnabled() {
+ return (d2_client_mgr_.ddnsEnabled());
+}
+
+const D2ClientConfigPtr&
+CfgMgr::getD2ClientConfig() const {
+ return (d2_client_mgr_.getD2ClientConfig());
+}
+
CfgMgr::CfgMgr()
- :datadir_(DHCP_DATA_DIR) {
+ : datadir_(DHCP_DATA_DIR),
+ all_ifaces_active_(false), echo_v4_client_id_(true),
+ d2_client_mgr_() {
// DHCP_DATA_DIR must be set set with -DDHCP_DATA_DIR="..." in Makefile.am
// Note: the definition of DHCP_DATA_DIR needs to include quotation marks
// See AM_CPPFLAGS definition in Makefile.am
diff --git a/src/lib/dhcpsrv/cfgmgr.h b/src/lib/dhcpsrv/cfgmgr.h
index 00f7e25..fcec8bf 100644
--- a/src/lib/dhcpsrv/cfgmgr.h
+++ b/src/lib/dhcpsrv/cfgmgr.h
@@ -19,6 +19,7 @@
#include <dhcp/option.h>
#include <dhcp/option_definition.h>
#include <dhcp/option_space.h>
+#include <dhcpsrv/d2_client.h>
#include <dhcpsrv/option_space_container.h>
#include <dhcpsrv/pool.h>
#include <dhcpsrv/subnet.h>
@@ -30,10 +31,21 @@
#include <map>
#include <string>
#include <vector>
+#include <list>
namespace isc {
namespace dhcp {
+/// @brief Exception thrown when the same interface has been specified twice.
+///
+/// In particular, this exception is thrown when adding interface to the set
+/// of interfaces on which server is supposed to listen.
+class DuplicateListeningIface : public Exception {
+public:
+ DuplicateListeningIface(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
/// @brief Configuration Manager
///
@@ -174,7 +186,6 @@ public:
/// @param interface_id content of interface-id option returned by a relay
///
/// @return a subnet object
- /// @todo This method is not currently supported.
Subnet6Ptr getSubnet6(OptionPtr interface_id);
/// @brief adds an IPv6 subnet
@@ -202,6 +213,26 @@ public:
/// completely new?
void deleteSubnets6();
+ /// @brief returns const reference to all subnets6
+ ///
+ /// This is used in a hook (subnet4_select), where the hook is able
+ /// to choose a different subnet. Server code has to offer a list
+ /// of possible choices (i.e. all subnets).
+ /// @return a pointer to const Subnet6 collection
+ const Subnet4Collection* getSubnets4() {
+ return (&subnets4_);
+ }
+
+ /// @brief returns const reference to all subnets6
+ ///
+ /// This is used in a hook (subnet6_select), where the hook is able
+ /// to choose a different subnet. Server code has to offer a list
+ /// of possible choices (i.e. all subnets).
+ /// @return a pointer to const Subnet6 collection
+ const Subnet6Collection* getSubnets6() {
+ return (&subnets6_);
+ }
+
/// @brief get IPv4 subnet by address
///
/// Finds a matching subnet, based on an address. This can be used
@@ -238,6 +269,88 @@ public:
/// @return data directory
std::string getDataDir();
+ /// @brief Adds the name of the interface to the set of interfaces on which
+ /// server should listen.
+ ///
+ /// @param iface A name of the interface being added to the listening set.
+ void addActiveIface(const std::string& iface);
+
+ /// @brief Sets the flag which indicates that server is supposed to listen
+ /// on all available interfaces.
+ ///
+ /// This function does not close or open sockets. It simply marks that
+ /// server should start to listen on all interfaces the next time sockets
+ /// are reopened. Server should examine this flag when it gets reconfigured
+ /// and configuration changes the interfaces that server should listen on.
+ void activateAllIfaces();
+
+ /// @brief Clear the collection of the interfaces that server should listen
+ /// on.
+ ///
+ /// Apart from clearing the list of interfaces specified with
+ /// @c CfgMgr::addListeningInterface, it also disables listening on all
+ /// interfaces if it has been enabled using
+ /// @c CfgMgr::activateAllInterfaces.
+ /// Likewise @c CfgMgr::activateAllIfaces, this function does not close or
+ /// open sockets. It marks all interfaces inactive for DHCP traffic.
+ /// Server should examine this new setting when it attempts to
+ /// reopen sockets (as a result of reconfiguration).
+ void deleteActiveIfaces();
+
+ /// @brief Check if specified interface should be used to listen to DHCP
+ /// traffic.
+ ///
+ /// @param iface A name of the interface to be checked.
+ ///
+ /// @return true if the specified interface belongs to the set of the
+ /// interfaces on which server is configured to listen.
+ bool isActiveIface(const std::string& iface) const;
+
+ /// @brief returns unicast a given interface should listen on (or NULL)
+ ///
+ /// This method will return an address for a specified interface, if the
+ /// server is supposed to listen on unicast address. This address is
+ /// intended to be used immediately. This pointer is valid only until
+ /// the next configuration change.
+ ///
+ /// @return IOAddress pointer (or NULL if none)
+ const isc::asiolink::IOAddress*
+ getUnicast(const std::string& iface) const;
+
+ /// @brief Sets whether server should send back client-id in DHCPv4
+ ///
+ /// This is a compatibility flag. The default (true) is compliant with
+ /// RFC6842. False is for backward compatibility.
+ ///
+ /// @param echo should the client-id be sent or not
+ void echoClientId(const bool echo) {
+ echo_v4_client_id_ = echo;
+ }
+
+ /// @brief Returns whether server should send back client-id in DHCPv4.
+ /// @return true if client-id should be returned, false otherwise.
+ bool echoClientId() const {
+ return (echo_v4_client_id_);
+ }
+
+ /// @brief Updates the DHCP-DDNS client configuration to the given value.
+ ///
+ /// @param new_config pointer to the new client configuration.
+ ///
+ /// @throw Underlying method(s) will throw D2ClientError if given an empty
+ /// pointer.
+ void setD2ClientConfig(D2ClientConfigPtr& new_config);
+
+ /// @param Convenience method for checking if DHCP-DDNS updates are enabled.
+ ///
+ /// @return True if the D2 configuration is enabled.
+ bool ddnsEnabled();
+
+ /// @brief Fetches the DHCP-DDNS configuration pointer.
+ ///
+ /// @return a reference to the current configuration pointer.
+ const D2ClientConfigPtr& getD2ClientConfig() const;
+
protected:
/// @brief Protected constructor.
@@ -269,12 +382,26 @@ protected:
private:
+ /// @brief Checks if the specified interface is listed as active.
+ ///
+ /// This function searches for the specified interface name on the list of
+ /// active interfaces: @c CfgMgr::active_ifaces_. It does not take into
+ /// account @c CfgMgr::all_ifaces_active_ flag. If this flag is set to true
+ /// but the specified interface does not belong to
+ /// @c CfgMgr::active_ifaces_, it will return false.
+ ///
+ /// @param iface interface name.
+ ///
+ /// @return true if specified interface belongs to
+ /// @c CfgMgr::active_ifaces_.
+ bool isIfaceListedActive(const std::string& iface) const;
+
/// @brief A collection of option definitions.
///
/// A collection of option definitions that can be accessed
/// using option space name they belong to.
OptionSpaceContainer<OptionDefContainer,
- OptionDefinitionPtr> option_def_spaces_;
+ OptionDefinitionPtr, std::string> option_def_spaces_;
/// @brief Container for defined DHCPv6 option spaces.
OptionSpaceCollection spaces6_;
@@ -284,6 +411,28 @@ private:
/// @brief directory where data files (e.g. server-id) are stored
std::string datadir_;
+
+ /// @name A collection of interface names on which server listens.
+ //@{
+ typedef std::list<std::string> ActiveIfacesCollection;
+ std::list<std::string> active_ifaces_;
+ //@}
+
+ /// @name a collection of unicast addresses and the interfaces names the
+ // server is supposed to listen on
+ //@{
+ typedef std::map<std::string, isc::asiolink::IOAddress> UnicastIfacesCollection;
+ UnicastIfacesCollection unicast_addrs_;
+
+ /// A flag which indicates that server should listen on all available
+ /// interfaces.
+ bool all_ifaces_active_;
+
+ /// Indicates whether v4 server should send back client-id
+ bool echo_v4_client_id_;
+
+ /// @brief Manages the DHCP-DDNS client and its configuration.
+ D2ClientMgr d2_client_mgr_;
};
} // namespace isc::dhcp
diff --git a/src/lib/dhcpsrv/d2_client.cc b/src/lib/dhcpsrv/d2_client.cc
new file mode 100644
index 0000000..d1b06ae
--- /dev/null
+++ b/src/lib/dhcpsrv/d2_client.cc
@@ -0,0 +1,185 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/dhcpsrv_log.h>
+
+#include <string>
+
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+D2ClientConfig::D2ClientConfig(const bool enable_updates,
+ const isc::asiolink::IOAddress& server_ip,
+ const size_t server_port,
+ const dhcp_ddns::
+ NameChangeProtocol& ncr_protocol,
+ const dhcp_ddns::
+ NameChangeFormat& ncr_format,
+ const bool remove_on_renew,
+ const bool always_include_fqdn,
+ const bool override_no_update,
+ const bool override_client_update,
+ const bool replace_client_name,
+ const std::string& generated_prefix,
+ const std::string& qualifying_suffix)
+ : enable_updates_(enable_updates),
+ server_ip_(server_ip.getAddress()),
+ server_port_(server_port),
+ ncr_protocol_(ncr_protocol),
+ ncr_format_(ncr_format),
+ remove_on_renew_(remove_on_renew),
+ always_include_fqdn_(always_include_fqdn),
+ override_no_update_(override_no_update),
+ override_client_update_(override_client_update),
+ replace_client_name_(replace_client_name),
+ generated_prefix_(generated_prefix),
+ qualifying_suffix_(qualifying_suffix) {
+ validateContents();
+}
+
+D2ClientConfig::D2ClientConfig()
+ : enable_updates_(false),
+ server_ip_(isc::asiolink::IOAddress("0.0.0.0")),
+ server_port_(0),
+ ncr_protocol_(dhcp_ddns::NCR_UDP),
+ ncr_format_(dhcp_ddns::FMT_JSON),
+ remove_on_renew_(false),
+ always_include_fqdn_(false),
+ override_no_update_(false),
+ override_client_update_(false),
+ replace_client_name_(false),
+ generated_prefix_(""),
+ qualifying_suffix_("") {
+ validateContents();
+}
+
+D2ClientConfig::~D2ClientConfig(){};
+
+void
+D2ClientConfig::validateContents() {
+ if (ncr_format_ != dhcp_ddns::FMT_JSON) {
+ isc_throw(D2ClientError, "D2ClientConfig: NCR Format:"
+ << dhcp_ddns::ncrFormatToString(ncr_format_)
+ << " is not yet supported");
+ }
+
+ if (ncr_protocol_ != dhcp_ddns::NCR_UDP) {
+ isc_throw(D2ClientError, "D2ClientConfig: NCR Protocol:"
+ << dhcp_ddns::ncrProtocolToString(ncr_protocol_)
+ << " is not yet supported");
+ }
+
+ // @todo perhaps more validation we should do yet?
+ // Are there any invalid combinations of options we need to test against?
+ // Also do we care about validating contents if it's disabled?
+}
+
+bool
+D2ClientConfig::operator == (const D2ClientConfig& other) const {
+ return ((enable_updates_ == other.enable_updates_) &&
+ (server_ip_ == other.server_ip_) &&
+ (server_port_ == other.server_port_) &&
+ (ncr_protocol_ == other.ncr_protocol_) &&
+ (ncr_format_ == other.ncr_format_) &&
+ (remove_on_renew_ == other.remove_on_renew_) &&
+ (always_include_fqdn_ == other.always_include_fqdn_) &&
+ (override_no_update_ == other.override_no_update_) &&
+ (override_client_update_ == other.override_client_update_) &&
+ (replace_client_name_ == other.replace_client_name_) &&
+ (generated_prefix_ == other.generated_prefix_) &&
+ (qualifying_suffix_ == other.qualifying_suffix_));
+}
+
+bool
+D2ClientConfig::operator != (const D2ClientConfig& other) const {
+ return (!(*this == other));
+}
+
+std::string
+D2ClientConfig::toText() const {
+ std::ostringstream stream;
+
+ stream << "enable_updates: " << (enable_updates_ ? "yes" : "no");
+ if (enable_updates_) {
+ stream << ", server_ip: " << server_ip_.toText()
+ << ", server_port: " << server_port_
+ << ", ncr_protocol: " << ncr_protocol_
+ << ", ncr_format: " << ncr_format_
+ << ", remove_on_renew: " << (remove_on_renew_ ? "yes" : "no")
+ << ", always_include_fqdn: " << (always_include_fqdn_ ?
+ "yes" : "no")
+ << ", override_no_update: " << (override_no_update_ ?
+ "yes" : "no")
+ << ", override_client_update: " << (override_client_update_ ?
+ "yes" : "no")
+ << ", replace_client_name: " << (replace_client_name_ ?
+ "yes" : "no")
+ << ", generated_prefix: [" << generated_prefix_ << "]"
+ << ", qualifying_suffix: [" << qualifying_suffix_ << "]";
+ }
+
+ return (stream.str());
+}
+
+std::ostream&
+operator<<(std::ostream& os, const D2ClientConfig& config) {
+ os << config.toText();
+ return (os);
+}
+
+D2ClientMgr::D2ClientMgr() : d2_client_config_(new D2ClientConfig()) {
+ // Default contstructor initializes with a disabled config.
+}
+
+D2ClientMgr::~D2ClientMgr(){
+}
+
+void
+D2ClientMgr::setD2ClientConfig(D2ClientConfigPtr& new_config) {
+ if (!new_config) {
+ isc_throw(D2ClientError,
+ "D2ClientMgr cannot set DHCP-DDNS configuration to NULL.");
+ }
+
+ // @todo When NameChangeSender is integrated, we will need to handle these
+ // scenarios:
+ // 1. D2 was enabled but now it is disabled
+ // - destroy the sender, flush any queued
+ // 2. D2 is still enabled but server params have changed
+ // - preserve any queued, reconnect based on sender params
+ // 3. D2 was was disabled now it is enabled.
+ // - create sender
+ //
+ // For now we just update the configuration.
+ d2_client_config_ = new_config;
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE, DHCPSRV_CFGMGR_CFG_DHCP_DDNS)
+ .arg(!ddnsEnabled() ? "DHCP-DDNS updates disabled" :
+ "DHCP_DDNS updates enabled");
+}
+
+bool
+D2ClientMgr::ddnsEnabled() {
+ return (d2_client_config_->getEnableUpdates());
+}
+
+const D2ClientConfigPtr&
+D2ClientMgr::getD2ClientConfig() const {
+ return (d2_client_config_);
+}
+
+}; // namespace dhcp
+}; // namespace isc
diff --git a/src/lib/dhcpsrv/d2_client.h b/src/lib/dhcpsrv/d2_client.h
new file mode 100644
index 0000000..2f84515
--- /dev/null
+++ b/src/lib/dhcpsrv/d2_client.h
@@ -0,0 +1,278 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef D2_CLIENT_H
+#define D2_CLIENT_H
+
+/// @file d2_client.h Defines the D2ClientConfig and D2ClientMgr classes.
+/// This file defines the classes Kea uses to act as a client of the b10-
+/// dhcp-ddns module (aka D2).
+///
+#include <asiolink/io_address.h>
+#include <dhcp_ddns/ncr_io.h>
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+
+/// An exception that is thrown if an error occurs while configuring
+/// the D2 DHCP DDNS client.
+class D2ClientError : public isc::Exception {
+public:
+
+ /// @brief constructor
+ ///
+ /// @param file name of the file, where exception occurred
+ /// @param line line of the file, where exception occurred
+ /// @param what text description of the issue that caused exception
+ D2ClientError(const char* file, size_t line, const char* what)
+ : isc::Exception(file, line, what) {}
+};
+
+/// @brief Acts as a storage vault for D2 client configuration
+///
+/// A simple container class for storing and retrieving the configuration
+/// parameters associated with DHCP-DDNS and acting as a client of D2.
+/// Instances of this class may be constructed through configuration parsing.
+///
+class D2ClientConfig {
+public:
+ /// @brief Constructor
+ ///
+ /// @param enable_updates Enables DHCP-DDNS updates
+ /// @param server_ip IP address of the b10-dhcp-ddns server (IPv4 or IPv6)
+ /// @param server_port IP port of the b10-dhcp-ddns server
+ /// @param ncr_protocol Socket protocol to use with b10-dhcp-ddns
+ /// Currently only UDP is supported.
+ /// @param ncr_format Format of the b10-dhcp-ddns requests.
+ /// Currently only JSON format is supported.
+ /// @param remove_on_renew Enables DNS Removes when renewing a lease
+ /// If true, Kea should request an explicit DNS remove prior to requesting
+ /// a DNS update when renewing a lease.
+ /// (Note: b10-dhcp-ddns is implemented per RFC 4703 and such a remove
+ /// is unnecessary).
+ /// @param always_include_fqdn Enables always including the FQDN option in
+ /// DHCP responses.
+ /// @param override_no_update Enables updates, even if clients request no
+ /// updates.
+ /// @param override_client_update Perform updates, even if client requested
+ /// delegation.
+ /// @param replace_client_name enables replacement of the domain-name
+ /// supplied by the client with a generated name.
+ /// @param generated_prefix Prefix to use when generating domain-names.
+ /// @param qualifying_suffix Suffix to use to qualify partial domain-names.
+ ///
+ /// @throw D2ClientError if given an invalid protocol or format.
+ D2ClientConfig(const bool enable_updates,
+ const isc::asiolink::IOAddress& server_ip,
+ const size_t server_port,
+ const dhcp_ddns::NameChangeProtocol& ncr_protocol,
+ const dhcp_ddns::NameChangeFormat& ncr_format,
+ const bool remove_on_renew,
+ const bool always_include_fqdn,
+ const bool override_no_update,
+ const bool override_client_update,
+ const bool replace_client_name,
+ const std::string& generated_prefix,
+ const std::string& qualifying_suffix);
+
+ /// @brief Default constructor
+ /// The default constructor creates an instance that has updates disabled.
+ D2ClientConfig();
+
+ /// @brief Destructor
+ virtual ~D2ClientConfig();
+
+ /// @brief Return whether or not DHCP-DDNS updating is enabled.
+ bool getEnableUpdates() const {
+ return(enable_updates_);
+ }
+
+ /// @brief Return the IP address of b10-dhcp-ddns (IPv4 or IPv6).
+ const isc::asiolink::IOAddress& getServerIp() const {
+ return(server_ip_);
+ }
+
+ /// @brief Return the IP port of b10-dhcp-ddns.
+ size_t getServerPort() const {
+ return(server_port_);
+ }
+
+ /// @brief Return the socket protocol to use with b10-dhcp-ddns.
+ const dhcp_ddns::NameChangeProtocol& getNcrProtocol() const {
+ return(ncr_protocol_);
+ }
+
+ /// @brief Return the b10-dhcp-ddns request format.
+ const dhcp_ddns::NameChangeFormat& getNcrFormat() const {
+ return(ncr_format_);
+ }
+
+ /// @brief Return whether or not removes should be sent for lease renewals.
+ bool getRemoveOnRenew() const {
+ return(remove_on_renew_);
+ }
+
+ /// @brief Return whether or not FQDN is always included in DHCP responses.
+ bool getAlwaysIncludeFqdn() const {
+ return(always_include_fqdn_);
+ }
+
+ /// @brief Return if updates are done even if clients request no updates.
+ bool getOverrideNoUpdate() const {
+ return(override_no_update_);
+ }
+
+ /// @brief Return if updates are done even when clients request delegation.
+ bool getOverrideClientUpdate() const {
+ return(override_client_update_);
+ }
+
+ /// @brief Return whether or not client's domain-name is always replaced.
+ bool getReplaceClientName() const {
+ return(replace_client_name_);
+ }
+
+ /// @brief Return the prefix to use when generating domain-names.
+ const std::string& getGeneratedPrefix() const {
+ return(generated_prefix_);
+ }
+
+ /// @brief Return the suffix to use to qualify partial domain-names.
+ const std::string& getQualifyingSuffix() const {
+ return(qualifying_suffix_);
+ }
+
+ /// @brief Compares two D2ClientConfigs for equality
+ bool operator == (const D2ClientConfig& other) const;
+
+ /// @brief Compares two D2ClientConfigs for inequality
+ bool operator != (const D2ClientConfig& other) const;
+
+ /// @brief Generates a string representation of the class contents.
+ std::string toText() const;
+
+protected:
+ /// @brief Validates member values.
+ ///
+ /// Method is used by the constructor to validate member contents.
+ ///
+ /// @throw D2ClientError if given an invalid protocol or format.
+ virtual void validateContents();
+
+private:
+ /// @brief Indicates whether or not DHCP DDNS updating is enabled.
+ bool enable_updates_;
+
+ /// @brief IP address of the b10-dhcp-ddns server (IPv4 or IPv6).
+ isc::asiolink::IOAddress server_ip_;
+
+ /// @brief IP port of the b10-dhcp-ddns server.
+ size_t server_port_;
+
+ /// @brief The socket protocol to use with b10-dhcp-ddns.
+ /// Currently only UPD is supported.
+ dhcp_ddns::NameChangeProtocol ncr_protocol_;
+
+ /// @brief Format of the b10-dhcp-ddns requests.
+ /// Currently only JSON format is supported.
+ dhcp_ddns::NameChangeFormat ncr_format_;
+
+ /// @brief Should Kea request a DNS Remove when renewing a lease.
+ /// If true, Kea should request an explicit DNS remove prior to requesting
+ /// a DNS update when renewing a lease.
+ /// (Note: b10-dhcp-ddns is implemented per RFC 4703 and such a remove
+ /// is unnecessary).
+ bool remove_on_renew_;
+
+ /// @brief Should Kea always include the FQDN option in its response.
+ bool always_include_fqdn_;
+
+ /// @brief Should Kea perform updates, even if client requested no updates.
+ /// Overrides the client request for no updates via the N flag.
+ bool override_no_update_;
+
+ /// @brief Should Kea perform updates, even if client requested delegation.
+ bool override_client_update_;
+
+ /// @brief Should Kea replace the domain-name supplied by the client.
+ bool replace_client_name_;
+
+ /// @brief Prefix Kea should use when generating domain-names.
+ std::string generated_prefix_;
+
+ /// @brief Suffix Kea should use when to qualify partial domain-names.
+ std::string qualifying_suffix_;
+};
+
+std::ostream&
+operator<<(std::ostream& os, const D2ClientConfig& config);
+
+/// @brief Defines a pointer for D2ClientConfig instances.
+typedef boost::shared_ptr<D2ClientConfig> D2ClientConfigPtr;
+
+/// @brief D2ClientMgr isolates Kea from the details of being a D2 client.
+///
+/// Provides services for managing the current D2ClientConfig and managing
+/// communications with D2. (@todo The latter will be added once communication
+/// with D2 is implemented through the integration of
+/// dhcp_ddns::NameChangeSender interface(s)).
+///
+class D2ClientMgr {
+public:
+ /// @brief Constructor
+ ///
+ /// Default constructor which constructs an instance which has DHCP-DDNS
+ /// updates disabled.
+ D2ClientMgr();
+
+ /// @brief Destructor.
+ ~D2ClientMgr();
+
+ /// @brief Updates the DHCP-DDNS client configuration to the given value.
+ ///
+ /// @param new_config pointer to the new client configuration.
+ /// @throw D2ClientError if passed an empty pointer.
+ void setD2ClientConfig(D2ClientConfigPtr& new_config);
+
+ /// @brief Convenience method for checking if DHCP-DDNS is enabled.
+ ///
+ /// @return True if the D2 configuration is enabled.
+ bool ddnsEnabled();
+
+ /// @brief Fetches the DHCP-DDNS configuration pointer.
+ ///
+ /// @return a reference to the current configuration pointer.
+ const D2ClientConfigPtr& getD2ClientConfig() const;
+
+private:
+ /// @brief Container class for DHCP-DDNS configuration parameters.
+ D2ClientConfigPtr d2_client_config_;
+};
+
+/// @brief Defines a pointer for D2ClientMgr instances.
+typedef boost::shared_ptr<D2ClientMgr> D2ClientMgrPtr;
+
+
+} // namespace isc
+} // namespace dhcp
+
+#endif
diff --git a/src/lib/dhcpsrv/database_backends.dox b/src/lib/dhcpsrv/database_backends.dox
index 43594fd..174b5e2 100644
--- a/src/lib/dhcpsrv/database_backends.dox
+++ b/src/lib/dhcpsrv/database_backends.dox
@@ -26,7 +26,7 @@
- The MySQL lease manager uses the freely available MySQL as its backend
database. This is not included in BIND 10 DHCP by default:
- the --with-dhcp-mysql switch must be supplied to "configure" for support
+ the \--with-dhcp-mysql switch must be supplied to "configure" for support
to be compiled into the software.
- Memfile is an in-memory lease database, with (currently) nothing being
written to persistent storage. The long-term plans for the backend do
@@ -83,9 +83,42 @@
@subsection dhcp-mysql-unittest MySQL
- A database called <i>keatest</i> needs to be set up using the MySQL
- <b>CREATE DATABASE</b> command. A database user, also called <i>keatest</i>
- (with a password <i>keatest</i>) must be given full privileges in that
- database. The unit tests create the schema in the database before each test
- and delete it afterwards.
+ A database called <i>keatest</i> must be created. A database user, also called
+ <i>keatest</i> (and with a password <i>keatest</i>) must also be created and
+ be given full privileges in that database. The unit tests create the schema
+ in the database before each test and delete it afterwards.
+
+ In detail, the steps to create the database and user are:
+
+ -# Log into MySQL as root:
+ @verbatim
+ % mysql -u root -p
+ Enter password:
+ :
+ mysql>@endverbatim\n
+ -# Create the test database. This must be called "keatest":
+ @verbatim
+ mysql> CREATE DATABASE keatest;
+ mysql>@endverbatim\n
+ -# Create the user under which the test client will connect to the database
+ (the apostrophes around the words <i>keatest</i> and <i>localhost</i> are
+ required):
+ @verbatim
+ mysql> CREATE USER 'keatest'@'localhost' IDENTIFIED BY 'keatest';
+ mysql>@endverbatim\n
+ -# Grant the created user permissions to access the <i>keatest</i> database
+ (again, the apostrophes around the words <i>keatest</i> and <i>localhost</i>
+ are required):
+ @verbatim
+ mysql> GRANT ALL ON keatest.* TO 'keatest'@'localhost';
+ mysql>@endverbatim\n
+ -# Exit MySQL:
+ @verbatim
+ mysql> quit
+ Bye
+ %@endverbatim
+
+ The unit tests are run automatically when "make check" is executed (providing
+ that BIND 10 has been build with the \--with-dhcp-mysql switch (see the installation
+ section in the <a href="http://bind10.isc.org/docs/bind10-guide.html">BIND 10 Guide</a>).
*/
diff --git a/src/lib/dhcpsrv/dhcp_config_parser.h b/src/lib/dhcpsrv/dhcp_config_parser.h
index 9d340da..77e46c8 100644
--- a/src/lib/dhcpsrv/dhcp_config_parser.h
+++ b/src/lib/dhcpsrv/dhcp_config_parser.h
@@ -130,79 +130,6 @@ public:
virtual void commit() = 0;
};
-/// @brief A template class that stores named elements of a given data type.
-///
-/// This template class is provides data value storage for configuration parameters
-/// of a given data type. The values are stored by parameter name and as instances
-/// of type "ValueType".
-///
-/// @param ValueType is the data type of the elements to store.
-template<typename ValueType>
-class ValueStorage {
- public:
- /// @brief Stores the the parameter and its value in the store.
- ///
- /// If the parameter does not exist in the store, then it will be added,
- /// otherwise its data value will be updated with the given value.
- ///
- /// @param name is the name of the paramater to store.
- /// @param value is the data value to store.
- void setParam(const std::string name, const ValueType& value) {
- values_[name] = value;
- }
-
- /// @brief Returns the data value for the given parameter.
- ///
- /// Finds and returns the data value for the given parameter.
- /// @param name is the name of the parameter for which the data
- /// value is desired.
- ///
- /// @return The parameter's data value of type \<ValueType\>.
- /// @throw DhcpConfigError if the parameter is not found.
- ValueType getParam(const std::string& name) const {
- typename std::map<std::string, ValueType>::const_iterator param
- = values_.find(name);
-
- if (param == values_.end()) {
- isc_throw(DhcpConfigError, "Missing parameter '"
- << name << "'");
- }
-
- return (param->second);
- }
-
- /// @brief Remove the parameter from the store.
- ///
- /// Deletes the entry for the given parameter from the store if it
- /// exists.
- ///
- /// @param name is the name of the paramater to delete.
- void delParam(const std::string& name) {
- values_.erase(name);
- }
-
- /// @brief Deletes all of the entries from the store.
- ///
- void clear() {
- values_.clear();
- }
-
-
- private:
- /// @brief An std::map of the data values, keyed by parameter names.
- std::map<std::string, ValueType> values_;
-};
-
-
-/// @brief a collection of elements that store uint32 values (e.g. renew-timer = 900)
-typedef ValueStorage<uint32_t> Uint32Storage;
-
-/// @brief a collection of elements that store string values
-typedef ValueStorage<std::string> StringStorage;
-
-/// @brief Storage for parsed boolean values.
-typedef ValueStorage<bool> BooleanStorage;
-
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/lib/dhcpsrv/dhcp_parsers.cc b/src/lib/dhcpsrv/dhcp_parsers.cc
new file mode 100644
index 0000000..c647954
--- /dev/null
+++ b/src/lib/dhcpsrv/dhcp_parsers.cc
@@ -0,0 +1,1271 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcp/iface_mgr.h>
+#include <dhcp/libdhcp++.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <hooks/hooks_manager.h>
+#include <util/encode/hex.h>
+#include <util/strutil.h>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/foreach.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+using namespace std;
+using namespace isc::data;
+using namespace isc::hooks;
+
+namespace isc {
+namespace dhcp {
+
+namespace {
+const char* ALL_IFACES_KEYWORD = "*";
+}
+
+// *********************** ParserContext *************************
+
+ParserContext::ParserContext(Option::Universe universe):
+ boolean_values_(new BooleanStorage()),
+ uint32_values_(new Uint32Storage()),
+ string_values_(new StringStorage()),
+ options_(new OptionStorage()),
+ option_defs_(new OptionDefStorage()),
+ hooks_libraries_(),
+ universe_(universe)
+{
+}
+
+ParserContext::ParserContext(const ParserContext& rhs):
+ boolean_values_(),
+ uint32_values_(),
+ string_values_(),
+ options_(),
+ option_defs_(),
+ hooks_libraries_(),
+ universe_(rhs.universe_)
+{
+ copyContext(rhs);
+}
+
+ParserContext&
+// The cppcheck version 1.56 doesn't recognize that copyContext
+// copies all context fields.
+// cppcheck-suppress operatorEqVarError
+ParserContext::operator=(const ParserContext& rhs) {
+ if (this != &rhs) {
+ copyContext(rhs);
+ }
+
+ return (*this);
+}
+
+void
+ParserContext::copyContext(const ParserContext& ctx) {
+ copyContextPointer(ctx.boolean_values_, boolean_values_);
+ copyContextPointer(ctx.uint32_values_, uint32_values_);
+ copyContextPointer(ctx.string_values_, string_values_);
+ copyContextPointer(ctx.options_, options_);
+ copyContextPointer(ctx.option_defs_, option_defs_);
+ copyContextPointer(ctx.hooks_libraries_, hooks_libraries_);
+ // Copy universe.
+ universe_ = ctx.universe_;
+}
+
+template<typename T>
+void
+ParserContext::copyContextPointer(const boost::shared_ptr<T>& source_ptr,
+ boost::shared_ptr<T>& dest_ptr) {
+ if (source_ptr) {
+ dest_ptr.reset(new T(*source_ptr));
+ } else {
+ dest_ptr.reset();
+ }
+}
+
+
+// **************************** DebugParser *************************
+
+DebugParser::DebugParser(const std::string& param_name)
+ :param_name_(param_name) {
+}
+
+void
+DebugParser::build(ConstElementPtr new_config) {
+ value_ = new_config;
+ std::cout << "Build for token: [" << param_name_ << "] = ["
+ << value_->str() << "]" << std::endl;
+}
+
+void
+DebugParser::commit() {
+ // Debug message. The whole DebugParser class is used only for parser
+ // debugging, and is not used in production code. It is very convenient
+ // to keep it around. Please do not turn this cout into logger calls.
+ std::cout << "Commit for token: [" << param_name_ << "] = ["
+ << value_->str() << "]" << std::endl;
+}
+
+// **************************** BooleanParser *************************
+
+template<> void ValueParser<bool>::build(isc::data::ConstElementPtr value) {
+ // The Config Manager checks if user specified a
+ // valid value for a boolean parameter: True or False.
+ // We should have a boolean Element, use value directly
+ try {
+ value_ = value->boolValue();
+ } catch (const isc::data::TypeError &) {
+ isc_throw(BadValue, " Wrong value type for " << param_name_
+ << " : build called with a non-boolean element.");
+ }
+}
+
+// **************************** Uin32Parser *************************
+
+template<> void ValueParser<uint32_t>::build(ConstElementPtr value) {
+ int64_t check;
+ string x = value->str();
+ try {
+ check = boost::lexical_cast<int64_t>(x);
+ } catch (const boost::bad_lexical_cast &) {
+ isc_throw(BadValue, "Failed to parse value " << value->str()
+ << " as unsigned 32-bit integer.");
+ }
+ if (check > std::numeric_limits<uint32_t>::max()) {
+ isc_throw(BadValue, "Value " << value->str() << "is too large"
+ << " for unsigned 32-bit integer.");
+ }
+ if (check < 0) {
+ isc_throw(BadValue, "Value " << value->str() << "is negative."
+ << " Only 0 or larger are allowed for unsigned 32-bit integer.");
+ }
+
+ // value is small enough to fit
+ value_ = static_cast<uint32_t>(check);
+}
+
+// **************************** StringParser *************************
+
+template <> void ValueParser<std::string>::build(ConstElementPtr value) {
+ value_ = value->str();
+ boost::erase_all(value_, "\"");
+}
+
+// ******************** InterfaceListConfigParser *************************
+
+InterfaceListConfigParser::
+InterfaceListConfigParser(const std::string& param_name)
+ : activate_all_(false),
+ param_name_(param_name) {
+ if (param_name_ != "interfaces") {
+ isc_throw(BadValue, "Internal error. Interface configuration "
+ "parser called for the wrong parameter: " << param_name);
+ }
+}
+
+void
+InterfaceListConfigParser::build(ConstElementPtr value) {
+ // First, we iterate over all specified entries and add it to the
+ // local container so as we can do some basic validation, e.g. eliminate
+ // duplicates.
+ BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
+ std::string iface_name = iface->stringValue();
+ if (iface_name != ALL_IFACES_KEYWORD) {
+ // Let's eliminate duplicates. We could possibly allow duplicates,
+ // but if someone specified duplicated interface name it is likely
+ // that he mistyped the configuration. Failing here should draw his
+ // attention.
+ if (isIfaceAdded(iface_name)) {
+ isc_throw(isc::dhcp::DhcpConfigError, "duplicate interface"
+ << " name '" << iface_name << "' specified in '"
+ << param_name_ << "' configuration parameter");
+ }
+ // @todo check that this interface exists in the system!
+ // The IfaceMgr exposes mechanisms to check this.
+
+ // Add the interface name if ok.
+ interfaces_.push_back(iface_name);
+
+ } else {
+ activate_all_ = true;
+
+ }
+ }
+}
+
+void
+InterfaceListConfigParser::commit() {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Remove active interfaces and clear a flag which marks all interfaces
+ // active
+ cfg_mgr.deleteActiveIfaces();
+
+ if (activate_all_) {
+ // Activate all interfaces. There is not need to add their names
+ // explicitly.
+ cfg_mgr.activateAllIfaces();
+
+ } else {
+ // Explicitly add names of the interfaces which server should listen on.
+ BOOST_FOREACH(std::string iface, interfaces_) {
+ cfg_mgr.addActiveIface(iface);
+ }
+ }
+}
+
+bool
+InterfaceListConfigParser::isIfaceAdded(const std::string& iface) const {
+
+ for (IfaceListStorage::const_iterator it = interfaces_.begin();
+ it != interfaces_.end(); ++it) {
+ if (iface == *it) {
+ return (true);
+ }
+ }
+ return (false);
+}
+
+// ******************** HooksLibrariesParser *************************
+
+HooksLibrariesParser::HooksLibrariesParser(const std::string& param_name)
+ : libraries_(), changed_(false)
+{
+ // Sanity check on the name.
+ if (param_name != "hooks-libraries") {
+ isc_throw(BadValue, "Internal error. Hooks libraries "
+ "parser called for the wrong parameter: " << param_name);
+ }
+}
+
+void
+HooksLibrariesParser::build(ConstElementPtr value) {
+ // Initialize.
+ libraries_.clear();
+ changed_ = false;
+
+ // Extract the list of libraries.
+ BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
+ string libname = iface->str();
+ boost::erase_all(libname, "\"");
+ libraries_.push_back(libname);
+ }
+
+ // Check if the list of libraries has changed. If not, nothing is done
+ // - the command "DhcpN libreload" is required to reload the same
+ // libraries (this prevents needless reloads when anything else in the
+ // configuration is changed).
+ vector<string> current_libraries = HooksManager::getLibraryNames();
+ if (current_libraries == libraries_) {
+ return;
+ }
+
+ // Library list has changed, validate each of the libraries specified.
+ vector<string> error_libs = HooksManager::validateLibraries(libraries_);
+ if (!error_libs.empty()) {
+
+ // Construct the list of libraries in error for the message.
+ string error_list = error_libs[0];
+ for (int i = 1; i < error_libs.size(); ++i) {
+ error_list += (string(", ") + error_libs[i]);
+ }
+ isc_throw(DhcpConfigError, "hooks libraries failed to validate - "
+ "library or libraries in error are: " + error_list);
+ }
+
+ // The library list has changed and the libraries are valid, so flag for
+ // update when commit() is called.
+ changed_ = true;
+}
+
+void
+HooksLibrariesParser::commit() {
+ /// Commits the list of libraries to the configuration manager storage if
+ /// the list of libraries has changed.
+ if (changed_) {
+ // TODO Delete any stored CalloutHandles before reloading the
+ // libraries
+ HooksManager::loadLibraries(libraries_);
+ }
+}
+
+// Method for testing
+void
+HooksLibrariesParser::getLibraries(std::vector<std::string>& libraries,
+ bool& changed) {
+ libraries = libraries_;
+ changed = changed_;
+}
+
+// **************************** OptionDataParser *************************
+OptionDataParser::OptionDataParser(const std::string&, OptionStoragePtr options,
+ ParserContextPtr global_context)
+ : boolean_values_(new BooleanStorage()),
+ string_values_(new StringStorage()), uint32_values_(new Uint32Storage()),
+ options_(options), option_descriptor_(false),
+ global_context_(global_context) {
+ if (!options_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "options storage may not be NULL");
+ }
+
+ if (!global_context_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "context may may not be NULL");
+ }
+}
+
+void
+OptionDataParser::build(ConstElementPtr option_data_entries) {
+ BOOST_FOREACH(ConfigPair param, option_data_entries->mapValue()) {
+ ParserPtr parser;
+ if (param.first == "name" || param.first == "data" ||
+ param.first == "space") {
+ StringParserPtr name_parser(new StringParser(param.first,
+ string_values_));
+ parser = name_parser;
+ } else if (param.first == "code") {
+ Uint32ParserPtr code_parser(new Uint32Parser(param.first,
+ uint32_values_));
+ parser = code_parser;
+ } else if (param.first == "csv-format") {
+ BooleanParserPtr value_parser(new BooleanParser(param.first,
+ boolean_values_));
+ parser = value_parser;
+ } else {
+ isc_throw(DhcpConfigError,
+ "Parser error: option-data parameter not supported: "
+ << param.first);
+ }
+
+ parser->build(param.second);
+ // Before we can create an option we need to get the data from
+ // the child parsers. The only way to do it is to invoke commit
+ // on them so as they store the values in appropriate storages
+ // that this class provided to them. Note that this will not
+ // modify values stored in the global storages so the configuration
+ // will remain consistent even parsing fails somewhere further on.
+ parser->commit();
+ }
+
+ // Try to create the option instance.
+ createOption();
+}
+
+void
+OptionDataParser::commit() {
+ if (!option_descriptor_.option) {
+ // Before we can commit the new option should be configured. If it is
+ // not than somebody must have called commit() before build().
+ isc_throw(isc::InvalidOperation,
+ "parser logic error: no option has been configured and"
+ " thus there is nothing to commit. Has build() been called?");
+ }
+
+ uint16_t opt_type = option_descriptor_.option->getType();
+ Subnet::OptionContainerPtr options = options_->getItems(option_space_);
+ // The getItems() should never return NULL pointer. If there are no
+ // options configured for the particular option space a pointer
+ // to an empty container should be returned.
+ assert(options);
+ Subnet::OptionContainerTypeIndex& idx = options->get<1>();
+ // Try to find options with the particular option code in the main
+ // storage. If found, remove these options because they will be
+ // replaced with new one.
+ Subnet::OptionContainerTypeRange range = idx.equal_range(opt_type);
+ if (std::distance(range.first, range.second) > 0) {
+ idx.erase(range.first, range.second);
+ }
+
+ // Append new option to the main storage.
+ options_->addItem(option_descriptor_, option_space_);
+}
+
+void
+OptionDataParser::createOption() {
+ // Option code is held in the uint32_t storage but is supposed to
+ // be uint16_t value. We need to check that value in the configuration
+ // does not exceed range of uint8_t and is not zero.
+ uint32_t option_code = uint32_values_->getParam("code");
+ if (option_code == 0) {
+ isc_throw(DhcpConfigError, "option code must not be zero."
+ << " Option code '0' is reserved in DHCPv4.");
+ } else if (option_code > std::numeric_limits<uint8_t>::max()) {
+ isc_throw(DhcpConfigError, "invalid option code '" << option_code
+ << "', it must not exceed '"
+ << std::numeric_limits<uint8_t>::max() << "'");
+ }
+
+ // Check that the option name has been specified, is non-empty and does not
+ // contain spaces
+ std::string option_name = string_values_->getParam("name");
+ if (option_name.empty()) {
+ isc_throw(DhcpConfigError, "name of the option with code '"
+ << option_code << "' is empty");
+ } else if (option_name.find(" ") != std::string::npos) {
+ isc_throw(DhcpConfigError, "invalid option name '" << option_name
+ << "', space character is not allowed");
+ }
+
+ std::string option_space = string_values_->getParam("space");
+ if (!OptionSpace::validateName(option_space)) {
+ isc_throw(DhcpConfigError, "invalid option space name '"
+ << option_space << "' specified for option '"
+ << option_name << "' (code '" << option_code
+ << "')");
+ }
+
+ const bool csv_format = boolean_values_->getParam("csv-format");
+
+ // Find the Option Definition for the option by its option code.
+ // findOptionDefinition will throw if not found, no need to test.
+ OptionDefinitionPtr def;
+ if (!(def = findServerSpaceOptionDefinition(option_space, option_code))) {
+ // If we are not dealing with a standard option then we
+ // need to search for its definition among user-configured
+ // options. They are expected to be in the global storage
+ // already.
+ OptionDefContainerPtr defs =
+ global_context_->option_defs_->getItems(option_space);
+
+ // The getItems() should never return the NULL pointer. If there are
+ // no option definitions for the particular option space a pointer
+ // to an empty container should be returned.
+ assert(defs);
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ OptionDefContainerTypeRange range = idx.equal_range(option_code);
+ if (std::distance(range.first, range.second) > 0) {
+ def = *range.first;
+ }
+
+ // It's ok if we don't have option format if the option is
+ // specified as hex
+ if (!def && csv_format) {
+ isc_throw(DhcpConfigError, "definition for the option '"
+ << option_space << "." << option_name
+ << "' having code '" << option_code
+ << "' does not exist");
+ }
+ }
+
+ // Get option data from the configuration database ('data' field).
+ const std::string option_data = string_values_->getParam("data");
+
+ // Transform string of hexadecimal digits into binary format.
+ std::vector<uint8_t> binary;
+ std::vector<std::string> data_tokens;
+
+ if (csv_format) {
+ // If the option data is specified as a string of comma
+ // separated values then we need to split this string into
+ // individual values - each value will be used to initialize
+ // one data field of an option.
+ data_tokens = isc::util::str::tokens(option_data, ",");
+ } else {
+ // Otherwise, the option data is specified as a string of
+ // hexadecimal digits that we have to turn into binary format.
+ try {
+ util::encode::decodeHex(option_data, binary);
+ } catch (...) {
+ isc_throw(DhcpConfigError, "option data is not a valid"
+ << " string of hexadecimal digits: " << option_data);
+ }
+ }
+
+ OptionPtr option;
+ if (!def) {
+ if (csv_format) {
+ isc_throw(DhcpConfigError, "the CSV option data format can be"
+ " used to specify values for an option that has a"
+ " definition. The option with code " << option_code
+ << " does not have a definition.");
+ }
+
+ // @todo We have a limited set of option definitions intiialized at
+ // the moment. In the future we want to initialize option definitions
+ // for all options. Consequently an error will be issued if an option
+ // definition does not exist for a particular option code. For now it is
+ // ok to create generic option if definition does not exist.
+ OptionPtr option(new Option(global_context_->universe_,
+ static_cast<uint16_t>(option_code), binary));
+ // The created option is stored in option_descriptor_ class member
+ // until the commit stage when it is inserted into the main storage.
+ // If an option with the same code exists in main storage already the
+ // old option is replaced.
+ option_descriptor_.option = option;
+ option_descriptor_.persistent = false;
+ } else {
+
+ // Option name should match the definition. The option name
+ // may seem to be redundant but in the future we may want
+ // to reference options and definitions using their names
+ // and/or option codes so keeping the option name in the
+ // definition of option value makes sense.
+ if (def->getName() != option_name) {
+ isc_throw(DhcpConfigError, "specified option name '"
+ << option_name << "' does not match the "
+ << "option definition: '" << option_space
+ << "." << def->getName() << "'");
+ }
+
+ // Option definition has been found so let's use it to create
+ // an instance of our option.
+ try {
+ OptionPtr option = csv_format ?
+ def->optionFactory(global_context_->universe_,
+ option_code, data_tokens) :
+ def->optionFactory(global_context_->universe_,
+ option_code, binary);
+ Subnet::OptionDescriptor desc(option, false);
+ option_descriptor_.option = option;
+ option_descriptor_.persistent = false;
+ } catch (const isc::Exception& ex) {
+ isc_throw(DhcpConfigError, "option data does not match"
+ << " option definition (space: " << option_space
+ << ", code: " << option_code << "): "
+ << ex.what());
+ }
+ }
+
+ // All went good, so we can set the option space name.
+ option_space_ = option_space;
+}
+
+// **************************** OptionDataListParser *************************
+OptionDataListParser::OptionDataListParser(const std::string&,
+ OptionStoragePtr options, ParserContextPtr global_context,
+ OptionDataParserFactory* optionDataParserFactory)
+ : options_(options), local_options_(new OptionStorage()),
+ global_context_(global_context),
+ optionDataParserFactory_(optionDataParserFactory) {
+ if (!options_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "options storage may not be NULL");
+ }
+
+ if (!options_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "context may not be NULL");
+ }
+
+ if (!optionDataParserFactory_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "option data parser factory may not be NULL");
+ }
+}
+
+void
+OptionDataListParser::build(ConstElementPtr option_data_list) {
+ BOOST_FOREACH(ConstElementPtr option_value, option_data_list->listValue()) {
+ boost::shared_ptr<OptionDataParser>
+ parser((*optionDataParserFactory_)("option-data",
+ local_options_, global_context_));
+
+ // options_ member will hold instances of all options thus
+ // each OptionDataParser takes it as a storage.
+ // Build the instance of a single option.
+ parser->build(option_value);
+ // Store a parser as it will be used to commit.
+ parsers_.push_back(parser);
+ }
+}
+
+void
+OptionDataListParser::commit() {
+ BOOST_FOREACH(ParserPtr parser, parsers_) {
+ parser->commit();
+ }
+
+ // Parsing was successful and we have all configured
+ // options in local storage. We can now replace old values
+ // with new values.
+ std::swap(*local_options_, *options_);
+}
+
+// ******************************** OptionDefParser ****************************
+OptionDefParser::OptionDefParser(const std::string&,
+ OptionDefStoragePtr storage)
+ : storage_(storage), boolean_values_(new BooleanStorage()),
+ string_values_(new StringStorage()), uint32_values_(new Uint32Storage()) {
+ if (!storage_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "options storage may not be NULL");
+ }
+}
+
+void
+OptionDefParser::build(ConstElementPtr option_def) {
+ // Parse the elements that make up the option definition.
+ BOOST_FOREACH(ConfigPair param, option_def->mapValue()) {
+ std::string entry(param.first);
+ ParserPtr parser;
+ if (entry == "name" || entry == "type" || entry == "record-types"
+ || entry == "space" || entry == "encapsulate") {
+ StringParserPtr str_parser(new StringParser(entry,
+ string_values_));
+ parser = str_parser;
+ } else if (entry == "code") {
+ Uint32ParserPtr code_parser(new Uint32Parser(entry,
+ uint32_values_));
+ parser = code_parser;
+ } else if (entry == "array") {
+ BooleanParserPtr array_parser(new BooleanParser(entry,
+ boolean_values_));
+ parser = array_parser;
+ } else {
+ isc_throw(DhcpConfigError, "invalid parameter: " << entry);
+ }
+
+ parser->build(param.second);
+ parser->commit();
+ }
+
+ // Create an instance of option definition.
+ createOptionDef();
+
+ // Get all items we collected so far for the particular option space.
+ OptionDefContainerPtr defs = storage_->getItems(option_space_name_);
+
+ // Check if there are any items with option code the same as the
+ // one specified for the definition we are now creating.
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ const OptionDefContainerTypeRange& range =
+ idx.equal_range(option_definition_->getCode());
+
+ // If there are any items with this option code already we need
+ // to issue an error because we don't allow duplicates for
+ // option definitions within an option space.
+ if (std::distance(range.first, range.second) > 0) {
+ isc_throw(DhcpConfigError, "duplicated option definition for"
+ << " code '" << option_definition_->getCode() << "'");
+ }
+}
+
+void
+OptionDefParser::commit() {
+ if (storage_ && option_definition_ &&
+ OptionSpace::validateName(option_space_name_)) {
+ storage_->addItem(option_definition_, option_space_name_);
+ }
+}
+
+void
+OptionDefParser::createOptionDef() {
+ // Get the option space name and validate it.
+ std::string space = string_values_->getParam("space");
+ if (!OptionSpace::validateName(space)) {
+ isc_throw(DhcpConfigError, "invalid option space name '"
+ << space << "'");
+ }
+
+ // Get other parameters that are needed to create the
+ // option definition.
+ std::string name = string_values_->getParam("name");
+ uint32_t code = uint32_values_->getParam("code");
+ std::string type = string_values_->getParam("type");
+ bool array_type = boolean_values_->getParam("array");
+ std::string encapsulates = string_values_->getParam("encapsulate");
+
+ // Create option definition.
+ OptionDefinitionPtr def;
+ // We need to check if user has set encapsulated option space
+ // name. If so, different constructor will be used.
+ if (!encapsulates.empty()) {
+ // Arrays can't be used together with sub-options.
+ if (array_type) {
+ isc_throw(DhcpConfigError, "option '" << space << "."
+ << "name" << "', comprising an array of data"
+ << " fields may not encapsulate any option space");
+
+ } else if (encapsulates == space) {
+ isc_throw(DhcpConfigError, "option must not encapsulate"
+ << " an option space it belongs to: '"
+ << space << "." << name << "' is set to"
+ << " encapsulate '" << space << "'");
+
+ } else {
+ def.reset(new OptionDefinition(name, code, type,
+ encapsulates.c_str()));
+ }
+
+ } else {
+ def.reset(new OptionDefinition(name, code, type, array_type));
+
+ }
+
+ // The record-types field may carry a list of comma separated names
+ // of data types that form a record.
+ std::string record_types = string_values_->getParam("record-types");
+
+ // Split the list of record types into tokens.
+ std::vector<std::string> record_tokens =
+ isc::util::str::tokens(record_types, ",");
+ // Iterate over each token and add a record type into
+ // option definition.
+ BOOST_FOREACH(std::string record_type, record_tokens) {
+ try {
+ boost::trim(record_type);
+ if (!record_type.empty()) {
+ def->addRecordField(record_type);
+ }
+ } catch (const Exception& ex) {
+ isc_throw(DhcpConfigError, "invalid record type values"
+ << " specified for the option definition: "
+ << ex.what());
+ }
+ }
+
+ // Check the option definition parameters are valid.
+ try {
+ def->validate();
+ } catch (const isc::Exception& ex) {
+ isc_throw(DhcpConfigError, "invalid option definition"
+ << " parameters: " << ex.what());
+ }
+
+ // Option definition has been created successfully.
+ option_space_name_ = space;
+ option_definition_ = def;
+}
+
+// ******************************** OptionDefListParser ************************
+OptionDefListParser::OptionDefListParser(const std::string&,
+ OptionDefStoragePtr storage) :storage_(storage) {
+ if (!storage_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "storage may not be NULL");
+ }
+}
+
+void
+OptionDefListParser::build(ConstElementPtr option_def_list) {
+ // Clear existing items in the storage.
+ // We are going to replace all of them.
+ storage_->clearItems();
+
+ if (!option_def_list) {
+ isc_throw(DhcpConfigError, "parser error: a pointer to a list of"
+ << " option definitions is NULL");
+ }
+
+ BOOST_FOREACH(ConstElementPtr option_def, option_def_list->listValue()) {
+ boost::shared_ptr<OptionDefParser>
+ parser(new OptionDefParser("single-option-def", storage_));
+ parser->build(option_def);
+ parser->commit();
+ }
+}
+
+void
+OptionDefListParser::commit() {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ cfg_mgr.deleteOptionDefs();
+
+ // We need to move option definitions from the temporary
+ // storage to the storage.
+ std::list<std::string> space_names =
+ storage_->getOptionSpaceNames();
+
+ BOOST_FOREACH(std::string space_name, space_names) {
+ BOOST_FOREACH(OptionDefinitionPtr def,
+ *(storage_->getItems(space_name))) {
+ // All option definitions should be initialized to non-NULL
+ // values. The validation is expected to be made by the
+ // OptionDefParser when creating an option definition.
+ assert(def);
+ cfg_mgr.addOptionDef(def, space_name);
+ }
+ }
+}
+
+//****************************** PoolParser ********************************
+PoolParser::PoolParser(const std::string&, PoolStoragePtr pools)
+ :pools_(pools) {
+
+ if (!pools_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "storage may not be NULL");
+ }
+}
+
+void
+PoolParser::build(ConstElementPtr pools_list) {
+ BOOST_FOREACH(ConstElementPtr text_pool, pools_list->listValue()) {
+ // That should be a single pool representation. It should contain
+ // text is form prefix/len or first - last. Note that spaces
+ // are allowed
+ string txt = text_pool->stringValue();
+
+ // first let's remove any whitespaces
+ boost::erase_all(txt, " "); // space
+ boost::erase_all(txt, "\t"); // tabulation
+
+ // Is this prefix/len notation?
+ size_t pos = txt.find("/");
+ if (pos != string::npos) {
+ isc::asiolink::IOAddress addr("::");
+ uint8_t len = 0;
+ try {
+ addr = isc::asiolink::IOAddress(txt.substr(0, pos));
+
+ // start with the first character after /
+ string prefix_len = txt.substr(pos + 1);
+
+ // It is lexical cast to int and then downcast to uint8_t.
+ // Direct cast to uint8_t (which is really an unsigned char)
+ // will result in interpreting the first digit as output
+ // value and throwing exception if length is written on two
+ // digits (because there are extra characters left over).
+
+ // No checks for values over 128. Range correctness will
+ // be checked in Pool4 constructor.
+ len = boost::lexical_cast<int>(prefix_len);
+ } catch (...) {
+ isc_throw(DhcpConfigError, "Failed to parse pool "
+ "definition: " << text_pool->stringValue());
+ }
+
+ PoolPtr pool(poolMaker(addr, len));
+ local_pools_.push_back(pool);
+ continue;
+ }
+
+ // Is this min-max notation?
+ pos = txt.find("-");
+ if (pos != string::npos) {
+ // using min-max notation
+ isc::asiolink::IOAddress min(txt.substr(0,pos));
+ isc::asiolink::IOAddress max(txt.substr(pos + 1));
+
+ PoolPtr pool(poolMaker(min, max));
+ local_pools_.push_back(pool);
+ continue;
+ }
+
+ isc_throw(DhcpConfigError, "Failed to parse pool definition:"
+ << text_pool->stringValue() <<
+ ". Does not contain - (for min-max) nor / (prefix/len)");
+ }
+}
+
+void
+PoolParser::commit() {
+ if (pools_) {
+ // local_pools_ holds the values produced by the build function.
+ // At this point parsing should have completed successfuly so
+ // we can append new data to the supplied storage.
+ pools_->insert(pools_->end(), local_pools_.begin(), local_pools_.end());
+ }
+}
+
+//****************************** SubnetConfigParser *************************
+
+SubnetConfigParser::SubnetConfigParser(const std::string&,
+ ParserContextPtr global_context)
+ : uint32_values_(new Uint32Storage()), string_values_(new StringStorage()),
+ pools_(new PoolStorage()), options_(new OptionStorage()),
+ global_context_(global_context) {
+ // The first parameter should always be "subnet", but we don't check
+ // against that here in case some wants to reuse this parser somewhere.
+ if (!global_context_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "context storage may not be NULL");
+ }
+}
+
+void
+SubnetConfigParser::build(ConstElementPtr subnet) {
+ BOOST_FOREACH(ConfigPair param, subnet->mapValue()) {
+ ParserPtr parser(createSubnetConfigParser(param.first));
+ parser->build(param.second);
+ parsers_.push_back(parser);
+ }
+
+ // In order to create new subnet we need to get the data out
+ // of the child parsers first. The only way to do it is to
+ // invoke commit on them because it will make them write
+ // parsed data into storages we have supplied.
+ // Note that triggering commits on child parsers does not
+ // affect global data because we supplied pointers to storages
+ // local to this object. Thus, even if this method fails
+ // later on, the configuration remains consistent.
+ BOOST_FOREACH(ParserPtr parser, parsers_) {
+ parser->commit();
+ }
+
+ // Create a subnet.
+ createSubnet();
+}
+
+void
+SubnetConfigParser::appendSubOptions(const std::string& option_space,
+ OptionPtr& option) {
+ // Only non-NULL options are stored in option container.
+ // If this option pointer is NULL this is a serious error.
+ assert(option);
+
+ OptionDefinitionPtr def;
+ if (isServerStdOption(option_space, option->getType())) {
+ def = getServerStdOptionDefinition(option->getType());
+ // Definitions for some of the standard options hasn't been
+ // implemented so it is ok to leave here.
+ if (!def) {
+ return;
+ }
+ } else {
+ const OptionDefContainerPtr defs =
+ global_context_->option_defs_->getItems(option_space);
+
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ const OptionDefContainerTypeRange& range =
+ idx.equal_range(option->getType());
+ // There is no definition so we have to leave.
+ if (std::distance(range.first, range.second) == 0) {
+ return;
+ }
+
+ def = *range.first;
+
+ // If the definition exists, it must be non-NULL.
+ // Otherwise it is a programming error.
+ assert(def);
+ }
+
+ // We need to get option definition for the particular option space
+ // and code. This definition holds the information whether our
+ // option encapsulates any option space.
+ // Get the encapsulated option space name.
+ std::string encapsulated_space = def->getEncapsulatedSpace();
+ // If option space name is empty it means that our option does not
+ // encapsulate any option space (does not include sub-options).
+ if (!encapsulated_space.empty()) {
+ // Get the sub-options that belong to the encapsulated
+ // option space.
+ const Subnet::OptionContainerPtr sub_opts =
+ global_context_->options_->getItems(encapsulated_space);
+ // Append sub-options to the option.
+ BOOST_FOREACH(Subnet::OptionDescriptor desc, *sub_opts) {
+ if (desc.option) {
+ option->addOption(desc.option);
+ }
+ }
+ }
+}
+
+void
+SubnetConfigParser::createSubnet() {
+ std::string subnet_txt;
+ try {
+ subnet_txt = string_values_->getParam("subnet");
+ } catch (const DhcpConfigError &) {
+ // rethrow with precise error
+ isc_throw(DhcpConfigError,
+ "Mandatory subnet definition in subnet missing");
+ }
+
+ // Remove any spaces or tabs.
+ boost::erase_all(subnet_txt, " ");
+ boost::erase_all(subnet_txt, "\t");
+
+ // The subnet format is prefix/len. We are going to extract
+ // the prefix portion of a subnet string to create IOAddress
+ // object from it. IOAddress will be passed to the Subnet's
+ // constructor later on. In order to extract the prefix we
+ // need to get all characters preceding "/".
+ size_t pos = subnet_txt.find("/");
+ if (pos == string::npos) {
+ isc_throw(DhcpConfigError,
+ "Invalid subnet syntax (prefix/len expected):" << subnet_txt);
+ }
+
+ // Try to create the address object. It also validates that
+ // the address syntax is ok.
+ isc::asiolink::IOAddress addr(subnet_txt.substr(0, pos));
+ uint8_t len = boost::lexical_cast<unsigned int>(subnet_txt.substr(pos + 1));
+
+ // Call the subclass's method to instantiate the subnet
+ initSubnet(addr, len);
+
+ // Add pools to it.
+ for (PoolStorage::iterator it = pools_->begin(); it != pools_->end();
+ ++it) {
+ subnet_->addPool(*it);
+ }
+
+ // Configure interface, if defined
+
+ // Get interface name. If it is defined, then the subnet is available
+ // directly over specified network interface.
+ std::string iface;
+ try {
+ iface = string_values_->getParam("interface");
+ } catch (const DhcpConfigError &) {
+ // iface not mandatory so swallow the exception
+ }
+
+ if (!iface.empty()) {
+ if (!IfaceMgr::instance().getIface(iface)) {
+ isc_throw(DhcpConfigError, "Specified interface name " << iface
+ << " for subnet " << subnet_->toText()
+ << " is not present" << " in the system.");
+ }
+
+ subnet_->setIface(iface);
+ }
+
+ // We are going to move configured options to the Subnet object.
+ // Configured options reside in the container where options
+ // are grouped by space names. Thus we need to get all space names
+ // and iterate over all options that belong to them.
+ std::list<std::string> space_names = options_->getOptionSpaceNames();
+ BOOST_FOREACH(std::string option_space, space_names) {
+ // Get all options within a particular option space.
+ BOOST_FOREACH(Subnet::OptionDescriptor desc,
+ *options_->getItems(option_space)) {
+ // The pointer should be non-NULL. The validation is expected
+ // to be performed by the OptionDataParser before adding an
+ // option descriptor to the container.
+ assert(desc.option);
+ // We want to check whether an option with the particular
+ // option code has been already added. If so, we want
+ // to issue a warning.
+ Subnet::OptionDescriptor existing_desc =
+ subnet_->getOptionDescriptor("option_space",
+ desc.option->getType());
+ if (existing_desc.option) {
+ duplicate_option_warning(desc.option->getType(), addr);
+ }
+ // Add sub-options (if any).
+ appendSubOptions(option_space, desc.option);
+
+ // Check if the option space defines a vendor-option
+ uint32_t vendor_id = optionSpaceToVendorId(option_space);
+ if (vendor_id) {
+ // This is a vendor option
+ subnet_->addVendorOption(desc.option, false, vendor_id);
+ } else {
+ // This is a normal option
+ subnet_->addOption(desc.option, false, option_space);
+ }
+ }
+ }
+
+ // Check all global options and add them to the subnet object if
+ // they have been configured in the global scope. If they have been
+ // configured in the subnet scope we don't add global option because
+ // the one configured in the subnet scope always takes precedence.
+ space_names = global_context_->options_->getOptionSpaceNames();
+ BOOST_FOREACH(std::string option_space, space_names) {
+ // Get all global options for the particular option space.
+ BOOST_FOREACH(Subnet::OptionDescriptor desc,
+ *(global_context_->options_->getItems(option_space))) {
+ // The pointer should be non-NULL. The validation is expected
+ // to be performed by the OptionDataParser before adding an
+ // option descriptor to the container.
+ assert(desc.option);
+ // Check if the particular option has been already added.
+ // This would mean that it has been configured in the
+ // subnet scope. Since option values configured in the
+ // subnet scope take precedence over globally configured
+ // values we don't add option from the global storage
+ // if there is one already.
+ Subnet::OptionDescriptor existing_desc =
+ subnet_->getOptionDescriptor(option_space,
+ desc.option->getType());
+ if (!existing_desc.option) {
+ // Add sub-options (if any).
+ appendSubOptions(option_space, desc.option);
+
+ uint32_t vendor_id = optionSpaceToVendorId(option_space);
+ if (vendor_id) {
+ // This is a vendor option
+ subnet_->addVendorOption(desc.option, false, vendor_id);
+ } else {
+ // This is a normal option
+ subnet_->addOption(desc.option, false, option_space);
+ }
+ }
+ }
+ }
+}
+
+uint32_t
+SubnetConfigParser::optionSpaceToVendorId(const std::string& option_space) {
+ if (option_space.size() < 8) {
+ // 8 is a minimal length of "vendor-X" format
+ return (0);
+ }
+ if (option_space.substr(0,7) != "vendor-") {
+ return (0);
+ }
+
+ // text after "vendor-", supposedly numbers only
+ string x = option_space.substr(7);
+
+ int64_t check;
+ try {
+ check = boost::lexical_cast<int64_t>(x);
+ } catch (const boost::bad_lexical_cast &) {
+ /// @todo: Should we throw here?
+ // isc_throw(BadValue, "Failed to parse vendor-X value (" << x
+ // << ") as unsigned 32-bit integer.");
+ return (0);
+ }
+ if (check > std::numeric_limits<uint32_t>::max()) {
+ /// @todo: Should we throw here?
+ //isc_throw(BadValue, "Value " << x << "is too large"
+ // << " for unsigned 32-bit integer.");
+ return (0);
+ }
+ if (check < 0) {
+ /// @todo: Should we throw here?
+ // isc_throw(BadValue, "Value " << x << "is negative."
+ // << " Only 0 or larger are allowed for unsigned 32-bit integer.");
+ return (0);
+ }
+
+ // value is small enough to fit
+ return (static_cast<uint32_t>(check));
+}
+
+isc::dhcp::Triplet<uint32_t>
+SubnetConfigParser::getParam(const std::string& name) {
+ uint32_t value = 0;
+ try {
+ // look for local value
+ value = uint32_values_->getParam(name);
+ } catch (const DhcpConfigError &) {
+ try {
+ // no local, use global value
+ value = global_context_->uint32_values_->getParam(name);
+ } catch (const DhcpConfigError &) {
+ isc_throw(DhcpConfigError, "Mandatory parameter " << name
+ << " missing (no global default and no subnet-"
+ << "specific value)");
+ }
+ }
+
+ return (Triplet<uint32_t>(value));
+}
+
+//**************************** D2ClientConfigParser **********************
+D2ClientConfigParser::D2ClientConfigParser(const std::string& entry_name)
+ : entry_name_(entry_name), boolean_values_(new BooleanStorage()),
+ uint32_values_(new Uint32Storage()), string_values_(new StringStorage()),
+ local_client_config_() {
+}
+
+D2ClientConfigParser::~D2ClientConfigParser() {
+}
+
+void
+D2ClientConfigParser::build(isc::data::ConstElementPtr client_config) {
+ BOOST_FOREACH(ConfigPair param, client_config->mapValue()) {
+ ParserPtr parser(createConfigParser(param.first));
+ parser->build(param.second);
+ parser->commit();
+ }
+
+ bool enable_updates = boolean_values_->getParam("enable-updates");
+ if (!enable_updates) {
+ // If it's not enabled, don't bother validating the rest. This
+ // allows for an abbreviated config entry that only contains
+ // the flag. The default constructor creates a disabled instance.
+ local_client_config_.reset(new D2ClientConfig());
+ return;
+ }
+
+ // Get all parameters that are needed to create the D2ClientConfig.
+ asiolink::IOAddress server_ip(string_values_->getParam("server-ip"));
+
+ uint32_t server_port = uint32_values_->getParam("server-port");
+
+ dhcp_ddns::NameChangeProtocol
+ ncr_protocol = dhcp_ddns:: stringToNcrProtocol(string_values_->
+ getParam("ncr-protocol"));
+
+ dhcp_ddns::NameChangeFormat
+ ncr_format = dhcp_ddns::stringToNcrFormat(string_values_->
+ getParam("ncr-format"));
+
+ std::string generated_prefix = string_values_->getParam("generated-prefix");
+ std::string qualifying_suffix = string_values_->
+ getParam("qualifying-suffix");
+
+ bool remove_on_renew = boolean_values_->getParam("remove-on-renew");
+ bool always_include_fqdn = boolean_values_->getParam("always-include-fqdn");
+ bool override_no_update = boolean_values_->getParam("override-no-update");
+ bool override_client_update = boolean_values_->
+ getParam("override-client-update");
+ bool replace_client_name = boolean_values_->getParam("replace-client-name");
+
+ // Attempt to create the new client config.
+ local_client_config_.reset(new D2ClientConfig(enable_updates, server_ip,
+ server_port, ncr_protocol,
+ ncr_format, remove_on_renew,
+ always_include_fqdn,
+ override_no_update,
+ override_client_update,
+ replace_client_name,
+ generated_prefix,
+ qualifying_suffix));
+}
+
+isc::dhcp::ParserPtr
+D2ClientConfigParser::createConfigParser(const std::string& config_id) {
+ DhcpConfigParser* parser = NULL;
+ if (config_id.compare("server-port") == 0) {
+ parser = new Uint32Parser(config_id, uint32_values_);
+ } else if ((config_id.compare("server-ip") == 0) ||
+ (config_id.compare("ncr-protocol") == 0) ||
+ (config_id.compare("ncr-format") == 0) ||
+ (config_id.compare("generated-prefix") == 0) ||
+ (config_id.compare("qualifying-suffix") == 0)) {
+ parser = new StringParser(config_id, string_values_);
+ } else if ((config_id.compare("enable-updates") == 0) ||
+ (config_id.compare("remove-on-renew") == 0) ||
+ (config_id.compare("always-include-fqdn") == 0) ||
+ (config_id.compare("allow-client-update") == 0) ||
+ (config_id.compare("override-no-update") == 0) ||
+ (config_id.compare("override-client-update") == 0) ||
+ (config_id.compare("replace-client-name") == 0)) {
+ parser = new BooleanParser(config_id, boolean_values_);
+ } else {
+ isc_throw(NotImplemented,
+ "parser error: D2ClientConfig parameter not supported: "
+ << config_id);
+ }
+
+ return (isc::dhcp::ParserPtr(parser));
+}
+
+void
+D2ClientConfigParser::commit() {
+ // @todo if local_client_config_ is empty then shutdown the listener...
+ // @todo Should this also attempt to start a listener?
+ // In keeping with Interface, Subnet, and Hooks parsers, then this
+ // should initialize the listener. Failure to init it, should cause
+ // rollback. This gets sticky, because who owns the listener instance?
+ // Does CfgMgr maintain it or does the server class? If the latter
+ // how do we get that value here?
+ // I'm thinkikng D2ClientConfig could contain the listener instance
+ CfgMgr::instance().setD2ClientConfig(local_client_config_);
+}
+
+}; // namespace dhcp
+}; // namespace isc
diff --git a/src/lib/dhcpsrv/dhcp_parsers.h b/src/lib/dhcpsrv/dhcp_parsers.h
new file mode 100644
index 0000000..00a07a0
--- /dev/null
+++ b/src/lib/dhcpsrv/dhcp_parsers.h
@@ -0,0 +1,960 @@
+// Copyright (C) 2013-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef DHCP_PARSERS_H
+#define DHCP_PARSERS_H
+
+#include <asiolink/io_address.h>
+#include <cc/data.h>
+#include <dhcp/option_definition.h>
+#include <dhcpsrv/d2_client.h>
+#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/option_space_container.h>
+#include <dhcpsrv/subnet.h>
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Storage for option definitions.
+typedef OptionSpaceContainer<OptionDefContainer,
+ OptionDefinitionPtr, std::string> OptionDefStorage;
+/// @brief Shared pointer to option definitions storage.
+typedef boost::shared_ptr<OptionDefStorage> OptionDefStoragePtr;
+
+/// Collection of containers holding option spaces. Each container within
+/// a particular option space holds so-called option descriptors.
+typedef OptionSpaceContainer<Subnet::OptionContainer,
+ Subnet::OptionDescriptor, std::string> OptionStorage;
+/// @brief Shared pointer to option storage.
+typedef boost::shared_ptr<OptionStorage> OptionStoragePtr;
+
+/// @brief Shared pointer to collection of hooks libraries.
+typedef boost::shared_ptr<std::vector<std::string> > HooksLibsStoragePtr;
+
+/// @brief A template class that stores named elements of a given data type.
+///
+/// This template class is provides data value storage for configuration parameters
+/// of a given data type. The values are stored by parameter name and as instances
+/// of type "ValueType".
+///
+/// @param ValueType is the data type of the elements to store.
+template<typename ValueType>
+class ValueStorage {
+ public:
+ /// @brief Stores the the parameter and its value in the store.
+ ///
+ /// If the parameter does not exist in the store, then it will be added,
+ /// otherwise its data value will be updated with the given value.
+ ///
+ /// @param name is the name of the paramater to store.
+ /// @param value is the data value to store.
+ void setParam(const std::string& name, const ValueType& value) {
+ values_[name] = value;
+ }
+
+ /// @brief Returns the data value for the given parameter.
+ ///
+ /// Finds and returns the data value for the given parameter.
+ /// @param name is the name of the parameter for which the data
+ /// value is desired.
+ ///
+ /// @return The paramater's data value of type @c ValueType.
+ /// @throw DhcpConfigError if the parameter is not found.
+ ValueType getParam(const std::string& name) const {
+ typename std::map<std::string, ValueType>::const_iterator param
+ = values_.find(name);
+
+ if (param == values_.end()) {
+ isc_throw(DhcpConfigError, "Missing parameter '"
+ << name << "'");
+ }
+
+ return (param->second);
+ }
+
+ /// @brief Remove the parameter from the store.
+ ///
+ /// Deletes the entry for the given parameter from the store if it
+ /// exists.
+ ///
+ /// @param name is the name of the paramater to delete.
+ void delParam(const std::string& name) {
+ values_.erase(name);
+ }
+
+ /// @brief Deletes all of the entries from the store.
+ ///
+ void clear() {
+ values_.clear();
+ }
+
+ private:
+ /// @brief An std::map of the data values, keyed by parameter names.
+ std::map<std::string, ValueType> values_;
+};
+
+
+/// @brief a collection of elements that store uint32 values
+typedef ValueStorage<uint32_t> Uint32Storage;
+typedef boost::shared_ptr<Uint32Storage> Uint32StoragePtr;
+
+/// @brief a collection of elements that store string values
+typedef ValueStorage<std::string> StringStorage;
+typedef boost::shared_ptr<StringStorage> StringStoragePtr;
+
+/// @brief Storage for parsed boolean values.
+typedef ValueStorage<bool> BooleanStorage;
+typedef boost::shared_ptr<BooleanStorage> BooleanStoragePtr;
+
+/// @brief Container for the current parsing context. It provides a
+/// single enclosure for the storage of configuration parameters,
+/// options, option definitions, and other context specific information
+/// that needs to be accessible throughout the parsing and parsing
+/// constructs.
+class ParserContext {
+public:
+ /// @brief Constructor
+ ///
+ /// @param universe is the Option::Universe value of this
+ /// context.
+ ParserContext(Option::Universe universe);
+
+ /// @brief Copy constructor
+ ParserContext(const ParserContext& rhs);
+
+ /// @brief Storage for boolean parameters.
+ BooleanStoragePtr boolean_values_;
+
+ /// @brief Storage for uint32 parameters.
+ Uint32StoragePtr uint32_values_;
+
+ /// @brief Storage for string parameters.
+ StringStoragePtr string_values_;
+
+ /// @brief Storage for options.
+ OptionStoragePtr options_;
+
+ /// @brief Storage for option definitions.
+ OptionDefStoragePtr option_defs_;
+
+ /// @brief Hooks libraries pointer.
+ ///
+ /// The hooks libraries information is a vector of strings, each containing
+ /// the name of a library. Hooks libraries should only be reloaded if the
+ /// list of names has changed, so the list of current DHCP parameters
+ /// (in isc::dhcp::CfgMgr) contains an indication as to whether the list has
+ /// altered. This indication is implemented by storing a pointer to the
+ /// list of library names which is cleared when the libraries are loaded.
+ /// So either the pointer is null (meaning don't reload the libraries and
+ /// the list of current names can be obtained from the HooksManager) or it
+ /// is non-null (this is the new list of names, reload the libraries when
+ /// possible).
+ HooksLibsStoragePtr hooks_libraries_;
+
+ /// @brief The parsing universe of this context.
+ Option::Universe universe_;
+
+ /// @brief Assignment operator
+ ParserContext& operator=(const ParserContext& rhs);
+
+ /// @brief Copy the context fields.
+ ///
+ /// This class method initializes the context data by copying the data
+ /// stored in the context instance provided as an argument. Note that
+ /// this function will also handle copying the NULL pointers.
+ ///
+ /// @param ctx context to be copied.
+ void copyContext(const ParserContext& ctx);
+
+ template<typename T>
+ void copyContextPointer(const boost::shared_ptr<T>& source_ptr,
+ boost::shared_ptr<T>& dest_ptr);
+};
+
+/// @brief Pointer to various parser context.
+typedef boost::shared_ptr<ParserContext> ParserContextPtr;
+
+/// @brief Simple data-type parser template class
+///
+/// This is the template class for simple data-type parsers. It supports
+/// parsing a configuration parameter with specific data-type for its
+/// possible values. It provides a common constructor, commit, and templated
+/// data storage. The "build" method implementation must be provided by a
+/// declaring type.
+/// @param ValueType is the data type of the configuration paramater value
+/// the parser should handle.
+template<typename ValueType>
+class ValueParser : public DhcpConfigParser {
+public:
+
+ /// @brief Constructor.
+ ///
+ /// @param param_name name of the parameter.
+ /// @param storage is a pointer to the storage container where the parsed
+ /// value be stored upon commit.
+ /// @throw isc::dhcp::DhcpConfigError if a provided parameter's
+ /// name is empty.
+ /// @throw isc::dhcp::DhcpConfigError if storage is null.
+ ValueParser(const std::string& param_name,
+ boost::shared_ptr<ValueStorage<ValueType> > storage)
+ : storage_(storage), param_name_(param_name), value_() {
+ // Empty parameter name is invalid.
+ if (param_name_.empty()) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "empty parameter name provided");
+ }
+
+ // NUll storage is invalid.
+ if (!storage_) {
+ isc_throw(isc::dhcp::DhcpConfigError, "parser logic error:"
+ << "storage may not be NULL");
+ }
+ }
+
+
+ /// @brief Parse a given element into a value of type @c ValueType
+ ///
+ /// @param value a value to be parsed.
+ ///
+ /// @throw isc::BadValue Typically the implementing type will throw
+ /// a BadValue exception when given an invalid Element to parse.
+ void build(isc::data::ConstElementPtr value);
+
+ /// @brief Put a parsed value to the storage.
+ void commit() {
+ // If a given parameter already exists in the storage we override
+ // its value. If it doesn't we insert a new element.
+ storage_->setParam(param_name_, value_);
+ }
+
+private:
+ /// Pointer to the storage where committed value is stored.
+ boost::shared_ptr<ValueStorage<ValueType> > storage_;
+
+ /// Name of the parameter which value is parsed with this parser.
+ std::string param_name_;
+
+ /// Parsed value.
+ ValueType value_;
+};
+
+/// @brief typedefs for simple data type parsers
+typedef ValueParser<bool> BooleanParser;
+typedef ValueParser<uint32_t> Uint32Parser;
+typedef ValueParser<std::string> StringParser;
+
+/// @brief a dummy configuration parser
+///
+/// It is a debugging parser. It does not configure anything,
+/// will accept any configuration and will just print it out
+/// on commit. Useful for debugging existing configurations and
+/// adding new ones.
+class DebugParser : public DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ ///
+ /// @param param_name name of the parsed parameter
+ DebugParser(const std::string& param_name);
+
+ /// @brief builds parameter value
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ ///
+ /// @param new_config pointer to the new configuration
+ virtual void build(isc::data::ConstElementPtr new_config);
+
+ /// @brief pretends to apply the configuration
+ ///
+ /// This is a method required by base class. It pretends to apply the
+ /// configuration, but in fact it only prints the parameter out.
+ ///
+ /// See @ref DhcpConfigParser class for details.
+ virtual void commit();
+
+private:
+ /// name of the parsed parameter
+ std::string param_name_;
+
+ /// pointer to the actual value of the parameter
+ isc::data::ConstElementPtr value_;
+
+};
+
+/// @brief parser for interface list definition
+///
+/// This parser handles Dhcp4/interface entry.
+/// It contains a list of network interfaces that the server listens on.
+/// In particular, it can contain an entry called "all" or "any" that
+/// designates all interfaces.
+///
+/// It is useful for parsing Dhcp4/interface parameter.
+class InterfaceListConfigParser : public DhcpConfigParser {
+public:
+
+ /// @brief constructor
+ ///
+ /// As this is a dedicated parser, it must be used to parse
+ /// "interface" parameter only. All other types will throw exception.
+ ///
+ /// @param param_name name of the configuration parameter being parsed
+ /// @throw BadValue if supplied parameter name is not "interface"
+ InterfaceListConfigParser(const std::string& param_name);
+
+ /// @brief parses parameters value
+ ///
+ /// Parses configuration entry (list of parameters) and adds each element
+ /// to the interfaces list.
+ ///
+ /// @param value pointer to the content of parsed values
+ virtual void build(isc::data::ConstElementPtr value);
+
+ /// @brief commits interfaces list configuration
+ virtual void commit();
+
+private:
+ /// @brief Check that specified interface exists in
+ /// @c InterfaceListConfigParser::interfaces_.
+ ///
+ /// @param iface A name of the interface.
+ ///
+ /// @return true if specified interface name was found.
+ bool isIfaceAdded(const std::string& iface) const;
+
+ /// contains list of network interfaces
+ typedef std::list<std::string> IfaceListStorage;
+ IfaceListStorage interfaces_;
+
+ // Should server listen on all interfaces.
+ bool activate_all_;
+
+ // Parsed parameter name
+ std::string param_name_;
+};
+
+/// @brief Parser for hooks library list
+///
+/// This parser handles the list of hooks libraries. This is an optional list,
+/// which may be empty.
+///
+/// However, the parser does more than just check the list of library names.
+/// It does two other things:
+///
+/// -# The problem faced with the hooks libraries is that we wish to avoid
+/// reloading the libraries if they have not changed. (This would cause the
+/// "unload" and "load" methods to run. Although libraries should be written
+/// to cope with this, it is feasible that such an action may be constly in
+/// terms of time and resources, or may cause side effects such as clearning
+/// an internal cache.) To this end, the parser also checks the list against
+/// the list of libraries current loaded and notes if there are changes.
+/// -# If there are, the parser validates the libraries; it opens them and
+/// checks that the "version" function exists and returns the correct value.
+///
+/// Only if the library list has changed and the libraries are valid will the
+/// change be applied.
+class HooksLibrariesParser : public DhcpConfigParser {
+public:
+
+ /// @brief Constructor
+ ///
+ /// As this is a dedicated parser, it must be used to parse
+ /// "hooks-libraries" parameter only. All other types will throw exception.
+ ///
+ /// @param param_name name of the configuration parameter being parsed.
+ ///
+ /// @throw BadValue if supplied parameter name is not "hooks-libraries"
+ HooksLibrariesParser(const std::string& param_name);
+
+ /// @brief Parses parameters value
+ ///
+ /// Parses configuration entry (list of parameters) and adds each element
+ /// to the hooks libraries list. The method also checks whether the
+ /// list of libraries is the same as that already loaded. If not, it
+ /// checks each of the libraries in the list for validity (they exist and
+ /// have a "version" function that returns the correct value).
+ ///
+ /// @param value pointer to the content of parsed values
+ virtual void build(isc::data::ConstElementPtr value);
+
+ /// @brief Commits hooks libraries data
+ ///
+ /// Providing that the specified libraries are valid and are different
+ /// to those already loaded, this method loads the new set of libraries
+ /// (and unloads the existing set).
+ virtual void commit();
+
+ /// @brief Returns list of parsed libraries
+ ///
+ /// Principally for testing, this returns the list of libraries as well as
+ /// an indication as to whether the list is different from the list of
+ /// libraries already loaded.
+ ///
+ /// @param [out] libraries List of libraries that were specified in the
+ /// new configuration.
+ /// @param [out] changed true if the list is different from that currently
+ /// loaded.
+ void getLibraries(std::vector<std::string>& libraries, bool& changed);
+
+private:
+ /// List of hooks libraries.
+ std::vector<std::string> libraries_;
+
+ /// Indicator flagging that the list of libraries has changed.
+ bool changed_;
+};
+
+/// @brief Parser for option data value.
+///
+/// This parser parses configuration entries that specify value of
+/// a single option. These entries include option name, option code
+/// and data carried by the option. The option data can be specified
+/// in one of the two available formats: binary value represented as
+/// a string of hexadecimal digits or a list of comma separated values.
+/// The format being used is controlled by csv-format configuration
+/// parameter. When setting this value to True, the latter format is
+/// used. The subsequent values in the CSV format apply to relevant
+/// option data fields in the configured option. For example the
+/// configuration: "data" : "192.168.2.0, 56, hello world" can be
+/// used to set values for the option comprising IPv4 address,
+/// integer and string data field. Note that order matters. If the
+/// order of values does not match the order of data fields within
+/// an option the configuration will not be accepted. If parsing
+/// is successful then an instance of an option is created and
+/// added to the storage provided by the calling class.
+class OptionDataParser : public DhcpConfigParser {
+public:
+ /// @brief Constructor.
+ ///
+ /// @param dummy first argument is ignored, all Parser constructors
+ /// accept string as first argument.
+ /// @param options is the option storage in which to store the parsed option
+ /// upon "commit".
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ /// @throw isc::dhcp::DhcpConfigError if options or global_context are null.
+ OptionDataParser(const std::string& dummy, OptionStoragePtr options,
+ ParserContextPtr global_context);
+
+ /// @brief Parses the single option data.
+ ///
+ /// This method parses the data of a single option from the configuration.
+ /// The option data includes option name, option code and data being
+ /// carried by this option. Eventually it creates the instance of the
+ /// option.
+ ///
+ /// @param option_data_entries collection of entries that define value
+ /// for a particular option.
+ /// @throw DhcpConfigError if invalid parameter specified in
+ /// the configuration.
+ /// @throw isc::InvalidOperation if failed to set storage prior to
+ /// calling build.
+ virtual void build(isc::data::ConstElementPtr option_data_entries);
+
+ /// @brief Commits option value.
+ ///
+ /// This function adds a new option to the storage or replaces an existing
+ /// option with the same code.
+ ///
+ /// @throw isc::InvalidOperation if failed to set pointer to storage or
+ /// failed
+ /// to call build() prior to commit. If that happens data in the storage
+ /// remain un-modified.
+ virtual void commit();
+
+ /// @brief virtual destructor to ensure orderly destruction of derivations.
+ virtual ~OptionDataParser(){};
+
+protected:
+ /// @brief Finds an option definition within the server's option space
+ ///
+ /// Given an option space and an option code, find the correpsonding
+ /// option defintion within the server's option defintion storage. This
+ /// method is pure virtual requiring derivations to manage which option
+ /// space(s) is valid for search.
+ ///
+ /// @param option_space name of the parameter option space
+ /// @param option_code numeric value of the parameter to find
+ /// @return OptionDefintionPtr of the option defintion or an
+ /// empty OptionDefinitionPtr if not found.
+ /// @throw DhcpConfigError if the option space requested is not valid
+ /// for this server.
+ virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
+ std::string& option_space, uint32_t option_code) = 0;
+
+private:
+
+ /// @brief Create option instance.
+ ///
+ /// Creates an instance of an option and adds it to the provided
+ /// options storage. If the option data parsed by \ref build function
+ /// are invalid or insufficient this function emits an exception.
+ ///
+ /// @warning this function does not check if options_ storage pointer
+ /// is intitialized but this check is not needed here because it is done
+ /// in the \ref build function.
+ ///
+ /// @throw DhcpConfigError if parameters provided in the configuration
+ /// are invalid.
+ void createOption();
+
+ /// Storage for boolean values.
+ BooleanStoragePtr boolean_values_;
+
+ /// Storage for string values (e.g. option name or data).
+ StringStoragePtr string_values_;
+
+ /// Storage for uint32 values (e.g. option code).
+ Uint32StoragePtr uint32_values_;
+
+ /// Pointer to options storage. This storage is provided by
+ /// the calling class and is shared by all OptionDataParser objects.
+ OptionStoragePtr options_;
+
+ /// Option descriptor holds newly configured option.
+ Subnet::OptionDescriptor option_descriptor_;
+
+ /// Option space name where the option belongs to.
+ std::string option_space_;
+
+ /// Parsing context which contains global values, options and option
+ /// definitions.
+ ParserContextPtr global_context_;
+};
+
+///@brief Function pointer for OptionDataParser factory methods
+typedef OptionDataParser *OptionDataParserFactory(const std::string&,
+ OptionStoragePtr options, ParserContextPtr global_context);
+
+/// @brief Parser for option data values within a subnet.
+///
+/// This parser iterates over all entries that define options
+/// data for a particular subnet and creates a collection of options.
+/// If parsing is successful, all these options are added to the Subnet
+/// object.
+class OptionDataListParser : public DhcpConfigParser {
+public:
+ /// @brief Constructor.
+ ///
+ /// @param dummy nominally would be param name, this is always ignored.
+ /// @param options parsed option storage for options in this list
+ /// @param global_context is a pointer to the global context which
+ /// stores global scope parameters, options, option defintions.
+ /// @param optionDataParserFactory factory method for creating individual
+ /// option parsers
+ /// @throw isc::dhcp::DhcpConfigError if options or global_context are null.
+ OptionDataListParser(const std::string& dummy, OptionStoragePtr options,
+ ParserContextPtr global_context,
+ OptionDataParserFactory *optionDataParserFactory);
+
+ /// @brief Parses entries that define options' data for a subnet.
+ ///
+ /// This method iterates over all entries that define option data
+ /// for options within a single subnet and creates options' instances.
+ ///
+ /// @param option_data_list pointer to a list of options' data sets.
+ /// @throw DhcpConfigError if option parsing failed.
+ void build(isc::data::ConstElementPtr option_data_list);
+
+ /// @brief Commit all option values.
+ ///
+ /// This function invokes commit for all option values.
+ void commit();
+
+private:
+ /// Pointer to options instances storage.
+ OptionStoragePtr options_;
+
+ /// Intermediate option storage. This storage is used by
+ /// lower level parsers to add new options. Values held
+ /// in this storage are assigned to main storage (options_)
+ /// if overall parsing was successful.
+ OptionStoragePtr local_options_;
+
+ /// Collection of parsers;
+ ParserCollection parsers_;
+
+ /// Parsing context which contains global values, options and option
+ /// definitions.
+ ParserContextPtr global_context_;
+
+ /// Factory to create server-specific option data parsers
+ OptionDataParserFactory *optionDataParserFactory_;
+};
+
+
+/// @brief Parser for a single option definition.
+///
+/// This parser creates an instance of a single option definition.
+class OptionDefParser : public DhcpConfigParser {
+public:
+ /// @brief Constructor.
+ ///
+ /// @param dummy first argument is ignored, all Parser constructors
+ /// accept string as first argument.
+ /// @param storage is the definition storage in which to store the parsed
+ /// definition upon "commit".
+ /// @throw isc::dhcp::DhcpConfigError if storage is null.
+ OptionDefParser(const std::string& dummy, OptionDefStoragePtr storage);
+
+ /// @brief Parses an entry that describes single option definition.
+ ///
+ /// @param option_def a configuration entry to be parsed.
+ ///
+ /// @throw DhcpConfigError if parsing was unsuccessful.
+ void build(isc::data::ConstElementPtr option_def);
+
+ /// @brief Stores the parsed option definition in a storage.
+ void commit();
+
+private:
+
+ /// @brief Create option definition from the parsed parameters.
+ void createOptionDef();
+
+ /// Instance of option definition being created by this parser.
+ OptionDefinitionPtr option_definition_;
+ /// Name of the space the option definition belongs to.
+ std::string option_space_name_;
+
+ /// Pointer to a storage where the option definition will be
+ /// added when \ref commit is called.
+ OptionDefStoragePtr storage_;
+
+ /// Storage for boolean values.
+ BooleanStoragePtr boolean_values_;
+
+ /// Storage for string values.
+ StringStoragePtr string_values_;
+
+ /// Storage for uint32 values.
+ Uint32StoragePtr uint32_values_;
+};
+
+/// @brief Parser for a list of option definitions.
+///
+/// This parser iterates over all configuration entries that define
+/// option definitions and creates instances of these definitions.
+/// If the parsing is successful, the collection of created definitions
+/// is put into the provided storage.
+class OptionDefListParser : public DhcpConfigParser {
+public:
+ /// @brief Constructor.
+ ///
+ /// @param dummy first argument is ignored, all Parser constructors
+ /// accept string as first argument.
+ /// @param storage is the definition storage in which to store the parsed
+ /// definitions in this list
+ /// @throw isc::dhcp::DhcpConfigError if storage is null.
+ OptionDefListParser(const std::string& dummy, OptionDefStoragePtr storage);
+
+ /// @brief Parse configuration entries.
+ ///
+ /// This function parses configuration entries and creates instances
+ /// of option definitions.
+ ///
+ /// @param option_def_list pointer to an element that holds entries
+ /// that define option definitions.
+ /// @throw DhcpConfigError if configuration parsing fails.
+ void build(isc::data::ConstElementPtr option_def_list);
+
+ /// @brief Stores option definitions in the CfgMgr.
+ void commit();
+
+private:
+ /// @brief storage for option definitions.
+ OptionDefStoragePtr storage_;
+};
+
+/// @brief a collection of pools
+///
+/// That type is used as intermediate storage, when pools are parsed, but there is
+/// no subnet object created yet to store them.
+typedef std::vector<PoolPtr> PoolStorage;
+typedef boost::shared_ptr<PoolStorage> PoolStoragePtr;
+
+/// @brief parser for pool definition
+///
+/// This abstract parser handles pool definitions, i.e. a list of entries of one
+/// of two syntaxes: min-max and prefix/len. Pool objects are created
+/// and stored in chosen PoolStorage container.
+///
+/// It is useful for parsing Dhcp<4/6>/subnet<4/6>[X]/pool parameters.
+class PoolParser : public DhcpConfigParser {
+public:
+
+ /// @brief constructor.
+ ///
+ /// @param dummy first argument is ignored, all Parser constructors
+ /// accept string as first argument.
+ /// @param pools is the storage in which to store the parsed pool
+ /// upon "commit".
+ /// @throw isc::dhcp::DhcpConfigError if storage is null.
+ PoolParser(const std::string& dummy, PoolStoragePtr pools);
+
+ /// @brief parses the actual list
+ ///
+ /// This method parses the actual list of interfaces.
+ /// No validation is done at this stage, everything is interpreted as
+ /// interface name.
+ /// @param pools_list list of pools defined for a subnet
+ /// @throw isc::dhcp::DhcpConfigError when pool parsing fails
+ virtual void build(isc::data::ConstElementPtr pools_list);
+
+ /// @brief Stores the parsed values in a storage provided
+ /// by an upper level parser.
+ virtual void commit();
+
+protected:
+ /// @brief Creates a Pool object given a IPv4 prefix and the prefix length.
+ ///
+ /// @param addr is the IP prefix of the pool.
+ /// @param len is the prefix length.
+ /// @param ptype is the type of pool to create.
+ /// @return returns a PoolPtr to the new Pool object.
+ virtual PoolPtr poolMaker(isc::asiolink::IOAddress &addr, uint32_t len,
+ int32_t ptype=0) = 0;
+
+ /// @brief Creates a Pool object given starting and ending IP addresses.
+ ///
+ /// @param min is the first IP address in the pool.
+ /// @param max is the last IP address in the pool.
+ /// @param ptype is the type of pool to create (not used by all derivations)
+ /// @return returns a PoolPtr to the new Pool object.
+ virtual PoolPtr poolMaker(isc::asiolink::IOAddress &min,
+ isc::asiolink::IOAddress &max, int32_t ptype=0) = 0;
+
+ /// @brief pointer to the actual Pools storage
+ ///
+ /// That is typically a storage somewhere in Subnet parser
+ /// (an upper level parser).
+ PoolStoragePtr pools_;
+
+ /// A temporary storage for pools configuration. It is a
+ /// storage where pools are stored by build function.
+ PoolStorage local_pools_;
+};
+
+/// @brief this class parses a single subnet
+///
+/// This class parses the whole subnet definition. It creates parsers
+/// for received configuration parameters as needed.
+class SubnetConfigParser : public DhcpConfigParser {
+public:
+
+ /// @brief constructor
+ SubnetConfigParser(const std::string&, ParserContextPtr global_context);
+
+ /// @brief parses parameter value
+ ///
+ /// @param subnet pointer to the content of subnet definition
+ ///
+ /// @throw isc::DhcpConfigError if subnet configuration parsing failed.
+ virtual void build(isc::data::ConstElementPtr subnet);
+
+ /// @brief Adds the created subnet to a server's configuration.
+ virtual void commit() = 0;
+
+ /// @brief tries to convert option_space string to numeric vendor_id
+ ///
+ /// This will work if the option_space has format "vendor-X", where
+ /// X can be any value between 1 and MAX_UINT32.
+ /// This is used to detect whether a given option-space is a vendor
+ /// space or not. Returns 0 if the format is different.
+ /// @return numeric vendor-id (or 0 if the format does not match)
+ static uint32_t optionSpaceToVendorId(const std::string& option_space);
+
+protected:
+ /// @brief creates parsers for entries in subnet definition
+ ///
+ /// @param config_id name od the entry
+ ///
+ /// @return parser object for specified entry name
+ /// @throw isc::dhcp::DhcpConfigError if trying to create a parser
+ /// for unknown config element
+ virtual DhcpConfigParser* createSubnetConfigParser(
+ const std::string& config_id) = 0;
+
+ /// @brief Determines if the given option space name and code describe
+ /// a standard option for the server.
+ ///
+ /// @param option_space is the name of the option space to consider
+ /// @param code is the numeric option code to consider
+ /// @return returns true if the space and code are part of the server's
+ /// standard options.
+ virtual bool isServerStdOption(std::string option_space, uint32_t code) = 0;
+
+ /// @brief Returns the option definition for a given option code from
+ /// the server's standard set of options.
+ /// @param code is the numeric option code of the desired option definition.
+ /// @return returns a pointer the option definition
+ virtual OptionDefinitionPtr getServerStdOptionDefinition (
+ uint32_t code) = 0;
+
+ /// @brief Issues a server specific warning regarding duplicate subnet
+ /// options.
+ ///
+ /// @param code is the numeric option code of the duplicate option
+ /// @param addr is the subnet address
+ /// @todo a means to know the correct logger and perhaps a common
+ /// message would allow this method to be emitted by the base class.
+ virtual void duplicate_option_warning(uint32_t code,
+ isc::asiolink::IOAddress& addr) = 0;
+
+ /// @brief Instantiates the subnet based on a given IP prefix and prefix
+ /// length.
+ ///
+ /// @param addr is the IP prefix of the subnet.
+ /// @param len is the prefix length
+ virtual void initSubnet(isc::asiolink::IOAddress addr, uint8_t len) = 0;
+
+ /// @brief Returns value for a given parameter (after using inheritance)
+ ///
+ /// This method implements inheritance. For a given parameter name, it first
+ /// checks if there is a global value for it and overwrites it with specific
+ /// value if such value was defined in subnet.
+ ///
+ /// @param name name of the parameter
+ /// @return triplet with the parameter name
+ /// @throw DhcpConfigError when requested parameter is not present
+ isc::dhcp::Triplet<uint32_t> getParam(const std::string& name);
+
+private:
+
+ /// @brief Append sub-options to an option.
+ ///
+ /// @param option_space a name of the encapsulated option space.
+ /// @param option option instance to append sub-options to.
+ void appendSubOptions(const std::string& option_space, OptionPtr& option);
+
+ /// @brief Create a new subnet using a data from child parsers.
+ ///
+ /// @throw isc::dhcp::DhcpConfigError if subnet configuration parsing
+ /// failed.
+ void createSubnet();
+
+protected:
+
+ /// Storage for subnet-specific integer values.
+ Uint32StoragePtr uint32_values_;
+
+ /// Storage for subnet-specific string values.
+ StringStoragePtr string_values_;
+
+ /// Storage for pools belonging to this subnet.
+ PoolStoragePtr pools_;
+
+ /// Storage for options belonging to this subnet.
+ OptionStoragePtr options_;
+
+ /// Parsers are stored here.
+ ParserCollection parsers_;
+
+ /// Pointer to the created subnet object.
+ isc::dhcp::SubnetPtr subnet_;
+
+ /// Parsing context which contains global values, options and option
+ /// definitions.
+ ParserContextPtr global_context_;
+};
+
+/// @brief Parser for D2ClientConfig
+///
+/// This class parses the configuration element "dhcp-ddns" common to the
+/// spec files for both dhcp4 and dhcp6. It creates an instance of a
+/// D2ClientConfig.
+class D2ClientConfigParser : public isc::dhcp::DhcpConfigParser {
+public:
+ /// @brief Constructor
+ ///
+ /// @param entry_name is an arbitrary label assigned to this configuration
+ /// definition.
+ D2ClientConfigParser(const std::string& entry_name);
+
+ /// @brief Destructor
+ virtual ~D2ClientConfigParser();
+
+ /// @brief Performs the parsing of the given dhcp-ddns element.
+ ///
+ /// The results of the parsing are retained internally for use during
+ /// commit.
+ ///
+ /// @param client_config is the "dhcp-ddns" configuration to parse
+ virtual void build(isc::data::ConstElementPtr client_config);
+
+ /// @brief Creates a parser for the given "dhcp-ddns" member element id.
+ ///
+ /// The elements currently supported are (see isc::dhcp::D2ClientConfig
+ /// for details on each):
+ /// -# enable-updates
+ /// -# server-ip
+ /// -# server-port
+ /// -# ncr-protocol
+ /// -# ncr-format
+ /// -# remove-on-renew
+ /// -# always-include-fqdn
+ /// -# allow-client-update
+ /// -# override-no-update
+ /// -# override-client-update
+ /// -# replace-client-name
+ /// -# generated-prefix
+ /// -# qualifying-suffix
+ ///
+ /// @param config_id is the "item_name" for a specific member element of
+ /// the "dns_server" specification.
+ ///
+ /// @return returns a pointer to newly created parser.
+ virtual isc::dhcp::ParserPtr createConfigParser(const std::string&
+ config_id);
+
+ /// @brief Instantiates a D2ClientConfig from internal data values
+ /// passes to CfgMgr singleton.
+ virtual void commit();
+
+private:
+ /// @brief Arbitrary label assigned to this parser instance.
+ /// Primarily used for diagnostics.
+ std::string entry_name_;
+
+ /// Storage for subnet-specific boolean values.
+ BooleanStoragePtr boolean_values_;
+
+ /// Storage for subnet-specific integer values.
+ Uint32StoragePtr uint32_values_;
+
+ /// Storage for subnet-specific string values.
+ StringStoragePtr string_values_;
+
+ /// @brief Pointer to temporary local instance created during build.
+ D2ClientConfigPtr local_client_config_ ;
+};
+
+// Pointers to various parser objects.
+typedef boost::shared_ptr<BooleanParser> BooleanParserPtr;
+typedef boost::shared_ptr<StringParser> StringParserPtr;
+typedef boost::shared_ptr<Uint32Parser> Uint32ParserPtr;
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // DHCP_PARSERS_H
+
diff --git a/src/lib/dhcpsrv/dhcpdb_create.mysql b/src/lib/dhcpsrv/dhcpdb_create.mysql
index f0da337..10531e3 100644
--- a/src/lib/dhcpsrv/dhcpdb_create.mysql
+++ b/src/lib/dhcpsrv/dhcpdb_create.mysql
@@ -36,7 +36,10 @@ CREATE TABLE lease4 (
client_id VARBINARY(128), # Client ID
valid_lifetime INT UNSIGNED, # Length of the lease (seconds)
expire TIMESTAMP, # Expiration time of the lease
- subnet_id INT UNSIGNED # Subnet identification
+ subnet_id INT UNSIGNED, # Subnet identification
+ fqdn_fwd BOOL, # Has forward DNS update been performed by a server
+ fqdn_rev BOOL, # Has reverse DNS update been performed by a server
+ hostname VARCHAR(255) # The FQDN of the client
) ENGINE = INNODB;
@@ -60,7 +63,11 @@ CREATE TABLE lease6 (
lease_type TINYINT, # Lease type (see lease6_types
# table for possible values)
iaid INT UNSIGNED, # See Section 10 of RFC 3315
- prefix_len TINYINT UNSIGNED # For IA_PD only
+ prefix_len TINYINT UNSIGNED, # For IA_PD only
+ fqdn_fwd BOOL, # Has forward DNS update been performed by a server
+ fqdn_rev BOOL, # Has reverse DNS update been performed by a server
+ hostname VARCHAR(255) # The FQDN of the client
+
) ENGINE = INNODB;
# Create search indexes for lease4 table
@@ -69,7 +76,9 @@ CREATE INDEX lease6_by_iaid_subnet_id_duid ON lease6 (iaid, subnet_id, duid);
# ... and a definition of lease6 types. This table is a convenience for
# users of the database - if they want to view the lease table and use the
-# type names, they can join this table with the lease6 table
+# type names, they can join this table with the lease6 table.
+# Make sure those values match Lease6::LeaseType enum (see src/bin/dhcpsrv/
+# lease_mgr.h)
CREATE TABLE lease6_types (
lease_type TINYINT PRIMARY KEY NOT NULL, # Lease type code.
name VARCHAR(5) # Name of the lease type
diff --git a/src/lib/dhcpsrv/dhcpsrv_log.h b/src/lib/dhcpsrv/dhcpsrv_log.h
index 9b6350a..fe997ff 100644
--- a/src/lib/dhcpsrv/dhcpsrv_log.h
+++ b/src/lib/dhcpsrv/dhcpsrv_log.h
@@ -50,6 +50,9 @@ const int DHCPSRV_DBG_TRACE_DETAIL = DBGLVL_TRACE_DETAIL;
/// Record detailed (and verbose) data on the server.
const int DHCPSRV_DBG_TRACE_DETAIL_DATA = DBGLVL_TRACE_DETAIL_DATA;
+// Trace hook related operations
+const int DHCPSRV_DBG_HOOKS = DBGLVL_TRACE_BASIC;
+
///@}
diff --git a/src/lib/dhcpsrv/dhcpsrv_messages.mes b/src/lib/dhcpsrv/dhcpsrv_messages.mes
index 9b8a0ce..5c7854e 100644
--- a/src/lib/dhcpsrv/dhcpsrv_messages.mes
+++ b/src/lib/dhcpsrv/dhcpsrv_messages.mes
@@ -54,6 +54,10 @@ consider reducing the lease lifetime. In this way, addresses allocated
to clients that are no longer active on the network will become available
available sooner.
+% DHCPSRV_CFGMGR_ADD_IFACE adding listening interface %1
+A debug message issued when new interface is being added to the collection of
+interfaces on which server listens to DHCP messages.
+
% DHCPSRV_CFGMGR_ADD_SUBNET4 adding subnet %1
A debug message reported when the DHCP configuration manager is adding the
specified IPv4 subnet to its database.
@@ -62,6 +66,19 @@ specified IPv4 subnet to its database.
A debug message reported when the DHCP configuration manager is adding the
specified IPv6 subnet to its database.
+% DHCPSRV_CFGMGR_ALL_IFACES_ACTIVE enabling listening on all interfaces
+A debug message issued when server is being configured to listen on all
+interfaces.
+
+% DHCPSRV_CFGMGR_CFG_DHCP_DDNS Setting DHCP-DDNS configuration to: %1
+A debug message issued when the server's DHCP-DDNS settings are changed.
+
+% DHCPSRV_CFGMGR_CLEAR_ACTIVE_IFACES stop listening on all interfaces
+A debug message issued when configuration manager clears the internal list
+of active interfaces. This doesn't prevent the server from listening to
+the DHCP traffic through open sockets, but will rather be used by Interface
+Manager to select active interfaces when sockets are re-opened.
+
% DHCPSRV_CFGMGR_DELETE_SUBNET4 deleting all IPv4 subnets
A debug message noting that the DHCP configuration manager has deleted all IPv4
subnets in its database.
@@ -105,7 +122,14 @@ This is a debug message reporting that the DHCP configuration manager
has returned the specified IPv6 subnet for a packet received over
given interface. This particular subnet was selected, because it
was specified as being directly reachable over given interface. (see
-'interface' parameter in subnet6 definition).
+'interface' parameter in the subnet6 definition).
+
+% DHCPSRV_CFGMGR_SUBNET6_IFACE_ID selected subnet %1 (interface-id match) for incoming packet
+This is a debug message reporting that the DHCP configuration manager
+has returned the specified IPv6 subnet for a received packet. This particular
+subnet was selected, because value of interface-id option matched what was
+configured in server's interface-id option for that selected subnet6.
+(see 'interface-id' parameter in the subnet6 definition).
% DHCPSRV_CLOSE_DB closing currently open %1 database
This is a debug message, issued when the DHCP server closes the currently
@@ -114,6 +138,24 @@ the database access parameters are changed: in the latter case, the
server closes the currently open database, and opens a database using
the new parameters.
+% DHCPSRV_HOOK_LEASE4_SELECT_SKIP Lease4 creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease4_select
+hook point sets the skip flag. It means that the server was told that
+no lease4 should be assigned. The server will not put that lease in its
+database and the client will get a NAK packet.
+
+% DHCPSRV_HOOK_LEASE4_RENEW_SKIP DHCPv4 lease was not renewed because a callout set the skip flag.
+This debug message is printed when a callout installed on lease4_renew
+hook point set the skip flag. For this particular hook point, the setting
+of the flag by a callout instructs the server to not renew a lease. The
+server will use existing lease as it is, without extending its lifetime.
+
+% DHCPSRV_HOOK_LEASE6_SELECT_SKIP Lease6 (non-temporary) creation was skipped, because of callout skip flag.
+This debug message is printed when a callout installed on lease6_select
+hook point sets the skip flag. It means that the server was told that
+no lease6 should be assigned. The server will not put that lease in its
+database and the client will get a NoAddrsAvail for that IA_NA option.
+
% DHCPSRV_INVALID_ACCESS invalid database access string: %1
This is logged when an attempt has been made to parse a database access string
and the attempt ended in error. The access string in question - which
@@ -171,6 +213,11 @@ A debug message issued when the server is attempting to obtain an IPv6
lease from the memory file database for a client with the specified IAID
(Identity Association ID), Subnet ID and DUID (DHCP Unique Identifier).
+% DHCPSRV_MEMFILE_GET_CLIENTID_HWADDR_SUBID obtaining IPv4 lease for client ID %1, hardware address %2 and subnet ID %3
+A debug message issued when the server is attempting to obtain an IPv4
+lease from the memory file database for a client with the specified
+client ID, hardware address and subnet ID.
+
% DHCPSRV_MEMFILE_GET_SUBID_CLIENTID obtaining IPv4 lease for subnet ID %1 and client ID %2
A debug message issued when the server is attempting to obtain an IPv4
lease from the memory file database for a client with the specified
@@ -208,7 +255,7 @@ recommended.
A debug message issued when the server is about to add an IPv4 lease
with the specified address to the MySQL backend database.
-% DHCPSRV_MYSQL_ADD_ADDR6 adding IPv6 lease with address %1
+% DHCPSRV_MYSQL_ADD_ADDR6 adding IPv6 lease with address %1, lease type %2
A debug message issued when the server is about to add an IPv6 lease
with the specified address to the MySQL backend database.
@@ -231,7 +278,7 @@ the specified address from the MySQL database for the specified address.
A debug message issued when the server is attempting to obtain an IPv4
lease from the MySQL database for the specified address.
-% DHCPSRV_MYSQL_GET_ADDR6 obtaining IPv6 lease for address %1
+% DHCPSRV_MYSQL_GET_ADDR6 obtaining IPv6 lease for address %1, lease type %2
A debug message issued when the server is attempting to obtain an IPv6
lease from the MySQL database for the specified address.
@@ -245,12 +292,12 @@ A debug message issued when the server is attempting to obtain a set
of IPv4 leases from the MySQL database for a client with the specified
hardware address.
-% DHCPSRV_MYSQL_GET_IAID_DUID obtaining IPv4 leases for IAID %1 and DUID %2
+% DHCPSRV_MYSQL_GET_IAID_DUID obtaining IPv6 leases for IAID %1, DUID %2, lease type %3
A debug message issued when the server is attempting to obtain a set of
IPv6 lease from the MySQL database for a client with the specified IAID
(Identity Association ID) and DUID (DHCP Unique Identifier).
-% DHCPSRV_MYSQL_GET_IAID_SUBID_DUID obtaining IPv4 leases for IAID %1, Subnet ID %2 and DUID %3
+% DHCPSRV_MYSQL_GET_IAID_SUBID_DUID obtaining IPv6 leases for IAID %1, Subnet ID %2, DUID %3, lease type %4
A debug message issued when the server is attempting to obtain an IPv6
lease from the MySQL database for a client with the specified IAID
(Identity Association ID), Subnet ID and DUID (DHCP Unique Identifier).
@@ -277,7 +324,7 @@ be rolled back and not committed to the database.
A debug message issued when the server is attempting to update IPv4
lease from the MySQL database for the specified address.
-% DHCPSRV_MYSQL_UPDATE_ADDR6 updating IPv6 lease for address %1
+% DHCPSRV_MYSQL_UPDATE_ADDR6 updating IPv6 lease for address %1, lease type %2
A debug message issued when the server is attempting to update IPv6
lease from the MySQL database for the specified address.
diff --git a/src/lib/dhcpsrv/lease.cc b/src/lib/dhcpsrv/lease.cc
new file mode 100644
index 0000000..a15d300
--- /dev/null
+++ b/src/lib/dhcpsrv/lease.cc
@@ -0,0 +1,257 @@
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcpsrv/lease.h>
+#include <sstream>
+
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+Lease::Lease(const isc::asiolink::IOAddress& addr, uint32_t t1, uint32_t t2,
+ uint32_t valid_lft, SubnetID subnet_id, time_t cltt,
+ const bool fqdn_fwd, const bool fqdn_rev,
+ const std::string& hostname)
+ :addr_(addr), t1_(t1), t2_(t2), valid_lft_(valid_lft), cltt_(cltt),
+ subnet_id_(subnet_id), fixed_(false), hostname_(hostname),
+ fqdn_fwd_(fqdn_fwd), fqdn_rev_(fqdn_rev) {
+}
+
+std::string
+Lease::typeToText(Lease::Type type) {
+ switch (type) {
+ case Lease::TYPE_V4:
+ return string("V4");
+ case Lease::TYPE_NA:
+ return string("IA_NA");
+ case Lease::TYPE_TA:
+ return string("IA_TA");
+ case Lease::TYPE_PD:
+ return string("IA_PD");
+ break;
+ default: {
+ stringstream tmp;
+ tmp << "unknown (" << type << ")";
+ return (tmp.str());
+ }
+ }
+}
+
+bool Lease::expired() const {
+
+ // Let's use int64 to avoid problems with negative/large uint32 values
+ int64_t expire_time = cltt_ + valid_lft_;
+ return (expire_time < time(NULL));
+}
+
+Lease4::Lease4(const Lease4& other)
+ : Lease(other.addr_, other.t1_, other.t2_, other.valid_lft_,
+ other.subnet_id_, other.cltt_, other.fqdn_fwd_,
+ other.fqdn_rev_, other.hostname_), ext_(other.ext_),
+ hwaddr_(other.hwaddr_) {
+
+ fixed_ = other.fixed_;
+ comments_ = other.comments_;
+
+ if (other.client_id_) {
+ client_id_.reset(new ClientId(other.client_id_->getClientId()));
+
+ } else {
+ client_id_.reset();
+
+ }
+}
+
+const std::vector<uint8_t>&
+Lease4::getClientIdVector() const {
+ if(!client_id_) {
+ static std::vector<uint8_t> empty_vec;
+ return (empty_vec);
+ }
+
+ return (client_id_->getClientId());
+}
+
+bool
+Lease4::matches(const Lease4& other) const {
+ if ((client_id_ && !other.client_id_) ||
+ (!client_id_ && other.client_id_)) {
+ // One lease has client-id, but the other doesn't
+ return false;
+ }
+
+ if (client_id_ && other.client_id_ &&
+ *client_id_ != *other.client_id_) {
+ // Different client-ids
+ return false;
+ }
+
+ return (addr_ == other.addr_ &&
+ ext_ == other.ext_ &&
+ hwaddr_ == other.hwaddr_);
+
+}
+
+Lease4&
+Lease4::operator=(const Lease4& other) {
+ if (this != &other) {
+ addr_ = other.addr_;
+ t1_ = other.t1_;
+ t2_ = other.t2_;
+ valid_lft_ = other.valid_lft_;
+ cltt_ = other.cltt_;
+ subnet_id_ = other.subnet_id_;
+ fixed_ = other.fixed_;
+ hostname_ = other.hostname_;
+ fqdn_fwd_ = other.fqdn_fwd_;
+ fqdn_rev_ = other.fqdn_rev_;
+ comments_ = other.comments_;
+ ext_ = other.ext_;
+ hwaddr_ = other.hwaddr_;
+
+ if (other.client_id_) {
+ client_id_.reset(new ClientId(other.client_id_->getClientId()));
+ } else {
+ client_id_.reset();
+ }
+ }
+ return (*this);
+}
+
+Lease6::Lease6(Type type, const isc::asiolink::IOAddress& addr,
+ DuidPtr duid, uint32_t iaid, uint32_t preferred, uint32_t valid,
+ uint32_t t1, uint32_t t2, SubnetID subnet_id, uint8_t prefixlen)
+ : Lease(addr, t1, t2, valid, subnet_id, 0/*cltt*/, false, false, ""),
+ type_(type), prefixlen_(prefixlen), iaid_(iaid), duid_(duid),
+ preferred_lft_(preferred) {
+ if (!duid) {
+ isc_throw(InvalidOperation, "DUID must be specified for a lease");
+ }
+
+ cltt_ = time(NULL);
+}
+
+Lease6::Lease6(Type type, const isc::asiolink::IOAddress& addr,
+ DuidPtr duid, uint32_t iaid, uint32_t preferred, uint32_t valid,
+ uint32_t t1, uint32_t t2, SubnetID subnet_id,
+ const bool fqdn_fwd, const bool fqdn_rev,
+ const std::string& hostname, uint8_t prefixlen)
+ : Lease(addr, t1, t2, valid, subnet_id, 0/*cltt*/,
+ fqdn_fwd, fqdn_rev, hostname),
+ type_(type), prefixlen_(prefixlen), iaid_(iaid), duid_(duid),
+ preferred_lft_(preferred) {
+ if (!duid) {
+ isc_throw(InvalidOperation, "DUID must be specified for a lease");
+ }
+
+ cltt_ = time(NULL);
+}
+
+const std::vector<uint8_t>&
+Lease6::getDuidVector() const {
+ if (!duid_) {
+ static std::vector<uint8_t> empty_vec;
+ return (empty_vec);
+ }
+
+ return (duid_->getDuid());
+}
+
+std::string
+Lease6::toText() const {
+ ostringstream stream;
+
+ stream << "Type: " << typeToText(type_) << "("
+ << static_cast<int>(type_) << ") ";
+ stream << "Address: " << addr_ << "\n"
+ << "Prefix length: " << static_cast<int>(prefixlen_) << "\n"
+ << "IAID: " << iaid_ << "\n"
+ << "Pref life: " << preferred_lft_ << "\n"
+ << "Valid life: " << valid_lft_ << "\n"
+ << "Cltt: " << cltt_ << "\n"
+ << "Subnet ID: " << subnet_id_ << "\n";
+
+ return (stream.str());
+}
+
+std::string
+Lease4::toText() const {
+ ostringstream stream;
+
+ stream << "Address: " << addr_ << "\n"
+ << "Valid life: " << valid_lft_ << "\n"
+ << "T1: " << t1_ << "\n"
+ << "T2: " << t2_ << "\n"
+ << "Cltt: " << cltt_ << "\n"
+ << "Subnet ID: " << subnet_id_ << "\n";
+
+ return (stream.str());
+}
+
+
+bool
+Lease4::operator==(const Lease4& other) const {
+ if ( (client_id_ && !other.client_id_) ||
+ (!client_id_ && other.client_id_) ) {
+ // One lease has client-id, but the other doesn't
+ return false;
+ }
+
+ if (client_id_ && other.client_id_ &&
+ *client_id_ != *other.client_id_) {
+ // Different client-ids
+ return false;
+ }
+
+ return (matches(other) &&
+ subnet_id_ == other.subnet_id_ &&
+ t1_ == other.t1_ &&
+ t2_ == other.t2_ &&
+ valid_lft_ == other.valid_lft_ &&
+ cltt_ == other.cltt_ &&
+ fixed_ == other.fixed_ &&
+ hostname_ == other.hostname_ &&
+ fqdn_fwd_ == other.fqdn_fwd_ &&
+ fqdn_rev_ == other.fqdn_rev_ &&
+ comments_ == other.comments_);
+}
+
+bool
+Lease6::matches(const Lease6& other) const {
+ return (addr_ == other.addr_ &&
+ type_ == other.type_ &&
+ prefixlen_ == other.prefixlen_ &&
+ iaid_ == other.iaid_ &&
+ *duid_ == *other.duid_);
+}
+
+bool
+Lease6::operator==(const Lease6& other) const {
+ return (matches(other) &&
+ preferred_lft_ == other.preferred_lft_ &&
+ valid_lft_ == other.valid_lft_ &&
+ t1_ == other.t1_ &&
+ t2_ == other.t2_ &&
+ cltt_ == other.cltt_ &&
+ subnet_id_ == other.subnet_id_ &&
+ fixed_ == other.fixed_ &&
+ hostname_ == other.hostname_ &&
+ fqdn_fwd_ == other.fqdn_fwd_ &&
+ fqdn_rev_ == other.fqdn_rev_ &&
+ comments_ == other.comments_);
+}
+
+} // namespace isc::dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/lease.h b/src/lib/dhcpsrv/lease.h
new file mode 100644
index 0000000..8626620
--- /dev/null
+++ b/src/lib/dhcpsrv/lease.h
@@ -0,0 +1,402 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LEASE_H
+#define LEASE_H
+
+#include <asiolink/io_address.h>
+#include <dhcp/duid.h>
+#include <dhcp/option.h>
+#include <dhcp/hwaddr.h>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Unique identifier for a subnet (both v4 and v6)
+///
+/// Let's copy SubnetID definition from subnet.h. We can't include it directly,
+/// because subnet.h needs Lease::Type, so it includes lease.h
+typedef uint32_t SubnetID;
+
+/// @brief a common structure for IPv4 and IPv6 leases
+///
+/// This structure holds all information that is common between IPv4 and IPv6
+/// leases.
+struct Lease {
+
+ /// @brief Type of lease or pool
+ typedef enum {
+ TYPE_NA = 0, /// the lease contains non-temporary IPv6 address
+ TYPE_TA = 1, /// the lease contains temporary IPv6 address
+ TYPE_PD = 2, /// the lease contains IPv6 prefix (for prefix delegation)
+ TYPE_V4 = 3 /// IPv4 lease
+ } Type;
+
+ /// @brief returns text representation of a lease type
+ /// @param type lease or pool type to be converted
+ /// @return text decription
+ static std::string typeToText(Type type);
+
+ /// @brief Constructor
+ ///
+ /// @param addr IP address
+ /// @param t1 renewal time
+ /// @param t2 rebinding time
+ /// @param valid_lft Lifetime of the lease
+ /// @param subnet_id Subnet identification
+ /// @param cltt Client last transmission time
+ /// @param fqdn_fwd If true, forward DNS update is performed for a lease.
+ /// @param fqdn_rev If true, reverse DNS update is performed for a lease.
+ /// @param hostname FQDN of the client which gets the lease.
+ Lease(const isc::asiolink::IOAddress& addr, uint32_t t1, uint32_t t2,
+ uint32_t valid_lft, SubnetID subnet_id, time_t cltt,
+ const bool fqdn_fwd, const bool fqdn_rev,
+ const std::string& hostname);
+
+ /// @brief Destructor
+ virtual ~Lease() {}
+
+ /// @brief IPv4 ot IPv6 address
+ ///
+ /// IPv4, IPv6 address or, in the case of a prefix delegation, the prefix.
+ isc::asiolink::IOAddress addr_;
+
+ /// @brief Renewal timer
+ ///
+ /// Specifies renewal time. Although technically it is a property of the
+ /// IA container and not the address itself, since our data model does not
+ /// define a separate IA entity, we are keeping it in the lease. In the
+ /// case of multiple addresses/prefixes for the same IA, each must have
+ /// consistent T1 and T2 values. This is specified in seconds since cltt.
+ uint32_t t1_;
+
+ /// @brief Rebinding timer
+ ///
+ /// Specifies rebinding time. Although technically it is a property of the
+ /// IA container and not the address itself, since our data model does not
+ /// define a separate IA entity, we are keeping it in the lease. In the
+ /// case of multiple addresses/prefixes for the same IA, each must have
+ /// consistent T1 and T2 values. This is specified in seconds since cltt.
+ uint32_t t2_;
+
+ /// @brief Valid lifetime
+ ///
+ /// Expressed as number of seconds since cltt.
+ uint32_t valid_lft_;
+
+ /// @brief Client last transmission time
+ ///
+ /// Specifies a timestamp giving the time when the last transmission from a
+ /// client was received.
+ time_t cltt_;
+
+ /// @brief Subnet identifier
+ ///
+ /// Specifies the identification of the subnet to which the lease belongs.
+ SubnetID subnet_id_;
+
+ /// @brief Fixed lease?
+ ///
+ /// Fixed leases are kept after they are released/expired.
+ bool fixed_;
+
+ /// @brief Client hostname
+ ///
+ /// This field may be empty
+ std::string hostname_;
+
+ /// @brief Forward zone updated?
+ ///
+ /// Set true if the DNS AAAA record for this lease has been updated.
+ bool fqdn_fwd_;
+
+ /// @brief Reverse zone updated?
+ ///
+ /// Set true if the DNS PTR record for this lease has been updated.
+ bool fqdn_rev_;
+
+ /// @brief Lease comments
+ ///
+ /// Currently not used. It may be used for keeping comments made by the
+ /// system administrator.
+ std::string comments_;
+
+ /// @brief Convert Lease to Printable Form
+ ///
+ /// @return String form of the lease
+ virtual std::string toText() const = 0;
+
+ /// @brief returns true if the lease is expired
+ /// @return true if the lease is expired
+ bool expired() const;
+
+};
+
+/// @brief Structure that holds a lease for IPv4 address
+///
+/// For performance reasons it is a simple structure, not a class. If we chose
+/// make it a class, all fields would have to made private and getters/setters
+/// would be required. As this is a critical part of the code that will be used
+/// extensively, direct access is warranted.
+struct Lease4 : public Lease {
+
+ /// @brief Address extension
+ ///
+ /// It is envisaged that in some cases IPv4 address will be accompanied
+ /// with some additional data. One example of such use are Address + Port
+ /// solutions (or Port-restricted Addresses), where several clients may get
+ /// the same address, but different port ranges. This feature is not
+ /// expected to be widely used. Under normal circumstances, the value
+ /// should be 0.
+ uint32_t ext_;
+
+ /// @brief Hardware address
+ std::vector<uint8_t> hwaddr_;
+
+ /// @brief Client identifier
+ ///
+ /// @todo Should this be a pointer to a client ID or the ID itself?
+ /// Compare with the DUID in the Lease6 structure.
+ ClientIdPtr client_id_;
+
+ /// @brief Constructor
+ ///
+ /// @param addr IPv4 address.
+ /// @param hwaddr Hardware address buffer
+ /// @param hwaddr_len Length of hardware address buffer
+ /// @param clientid Client identification buffer
+ /// @param clientid_len Length of client identification buffer
+ /// @param valid_lft Lifetime of the lease
+ /// @param t1 renewal time
+ /// @param t2 rebinding time
+ /// @param cltt Client last transmission time
+ /// @param subnet_id Subnet identification
+ /// @param fqdn_fwd If true, forward DNS update is performed for a lease.
+ /// @param fqdn_rev If true, reverse DNS update is performed for a lease.
+ /// @param hostname FQDN of the client which gets the lease.
+ Lease4(const isc::asiolink::IOAddress& addr, const uint8_t* hwaddr, size_t hwaddr_len,
+ const uint8_t* clientid, size_t clientid_len, uint32_t valid_lft,
+ uint32_t t1, uint32_t t2, time_t cltt, uint32_t subnet_id,
+ const bool fqdn_fwd = false, const bool fqdn_rev = false,
+ const std::string& hostname = "")
+ : Lease(addr, t1, t2, valid_lft, subnet_id, cltt, fqdn_fwd, fqdn_rev,
+ hostname),
+ ext_(0), hwaddr_(hwaddr, hwaddr + hwaddr_len) {
+ if (clientid_len) {
+ client_id_.reset(new ClientId(clientid, clientid_len));
+ }
+ }
+
+ /// @brief Default constructor
+ ///
+ /// Initialize fields that don't have a default constructor.
+ Lease4() : Lease(0, 0, 0, 0, 0, 0, false, false, "") {
+ }
+
+ /// @brief Copy constructor
+ ///
+ /// @param other the @c Lease4 object to be copied.
+ Lease4(const Lease4& other);
+
+ /// @brief Returns a client identifier.
+ ///
+ /// @warning Since the function returns the reference to a vector (not a
+ /// copy), the returned object should be used with caution because it will
+ /// remain valid only for the period of time when an object which returned
+ /// it exists.
+ ///
+ /// @return A reference to a vector holding client identifier,
+ /// or an empty vector if client identifier is NULL.
+ const std::vector<uint8_t>& getClientIdVector() const;
+
+ /// @brief Check if two objects encapsulate the lease for the same
+ /// client.
+ ///
+ /// Checks if two @c Lease4 objects have the same address, client id,
+ /// HW address and ext_ value. If these parameters match it is an
+ /// indication that both objects describe the lease for the same
+ /// client but apparently one is a result of renewal of the other. The
+ /// special case of the matching lease is the one that is equal to another.
+ ///
+ /// @param other A lease to compare with.
+ ///
+ /// @return true if the selected parameters of the two leases match.
+ bool matches(const Lease4& other) const;
+
+ /// @brief Assignment operator.
+ ///
+ /// @param other the @c Lease4 object to be assigned.
+ Lease4& operator=(const Lease4& other);
+
+ /// @brief Compare two leases for equality
+ ///
+ /// @param other lease6 object with which to compare
+ bool operator==(const Lease4& other) const;
+
+ /// @brief Compare two leases for inequality
+ ///
+ /// @param other lease6 object with which to compare
+ bool operator!=(const Lease4& other) const {
+ return (!operator==(other));
+ }
+
+ /// @brief Convert lease to printable form
+ ///
+ /// @return Textual represenation of lease data
+ virtual std::string toText() const;
+
+ /// @todo: Add DHCPv4 failover related fields here
+};
+
+/// @brief Pointer to a Lease4 structure.
+typedef boost::shared_ptr<Lease4> Lease4Ptr;
+
+/// @brief A collection of IPv4 leases.
+typedef std::vector<Lease4Ptr> Lease4Collection;
+
+/// @brief Structure that holds a lease for IPv6 address and/or prefix
+///
+/// For performance reasons it is a simple structure, not a class. If we chose
+/// make it a class, all fields would have to made private and getters/setters
+/// would be required. As this is a critical part of the code that will be used
+/// extensively, direct access is warranted.
+struct Lease6 : public Lease {
+
+ /// @brief Lease type
+ ///
+ /// One of normal address, temporary address, or prefix.
+ Type type_;
+
+ /// @brief IPv6 prefix length
+ ///
+ /// This is used only for prefix delegations and is ignored otherwise.
+ uint8_t prefixlen_;
+
+ /// @brief Identity Association Identifier (IAID)
+ ///
+ /// DHCPv6 stores all addresses and prefixes in IA containers (IA_NA,
+ /// IA_TA, IA_PD). All containers may appear more than once in a message.
+ /// To differentiate between them, the IAID field is present
+ uint32_t iaid_;
+
+ /// @brief Client identifier
+ DuidPtr duid_;
+
+ /// @brief preferred lifetime
+ ///
+ /// This parameter specifies the preferred lifetime since the lease was
+ /// assigned or renewed (cltt), expressed in seconds.
+ uint32_t preferred_lft_;
+
+ /// @todo: Add DHCPv6 failover related fields here
+
+ /// @brief Constructor
+ /// @param type Lease type.
+ /// @param addr Assigned address.
+ /// @param duid A pointer to an object representing DUID.
+ /// @param iaid IAID.
+ /// @param preferred Preferred lifetime.
+ /// @param valid Valid lifetime.
+ /// @param t1 A value of the T1 timer.
+ /// @param t2 A value of the T2 timer.
+ /// @param subnet_id A Subnet identifier.
+ /// @param prefixlen An address prefix length.
+ Lease6(Type type, const isc::asiolink::IOAddress& addr, DuidPtr duid,
+ uint32_t iaid, uint32_t preferred, uint32_t valid, uint32_t t1,
+ uint32_t t2, SubnetID subnet_id, uint8_t prefixlen = 128);
+
+ /// @brief Constructor, including FQDN data.
+ ///
+ /// @param type Lease type.
+ /// @param addr Assigned address.
+ /// @param duid A pointer to an object representing DUID.
+ /// @param iaid IAID.
+ /// @param preferred Preferred lifetime.
+ /// @param valid Valid lifetime.
+ /// @param t1 A value of the T1 timer.
+ /// @param t2 A value of the T2 timer.
+ /// @param subnet_id A Subnet identifier.
+ /// @param fqdn_fwd If true, forward DNS update is performed for a lease.
+ /// @param fqdn_rev If true, reverse DNS update is performed for a lease.
+ /// @param hostname FQDN of the client which gets the lease.
+ /// @param prefixlen An address prefix length.
+ Lease6(Type type, const isc::asiolink::IOAddress& addr, DuidPtr duid,
+ uint32_t iaid, uint32_t preferred, uint32_t valid, uint32_t t1,
+ uint32_t t2, SubnetID subnet_id, const bool fqdn_fwd,
+ const bool fqdn_rev, const std::string& hostname,
+ uint8_t prefixlen = 0);
+
+ /// @brief Constructor
+ ///
+ /// Initialize fields that don't have a default constructor.
+ Lease6() : Lease(isc::asiolink::IOAddress("::"), 0, 0, 0, 0, 0,
+ false, false, ""),
+ type_(TYPE_NA) {
+ }
+
+ /// @brief Returns a reference to a vector representing a DUID.
+ ///
+ /// @warning Since the function returns the reference to a vector (not a
+ /// copy), the returned object should be used with caution because it will
+ /// remain valid only for the period of time when an object which returned
+ /// it exists.
+ ///
+ /// @return A reference to a vector holding a DUID.
+ const std::vector<uint8_t>& getDuidVector() const;
+
+ /// @brief Checks if two lease objects encapsulate the lease for the same
+ /// client.
+ ///
+ /// This function compares address, type, prefix length, IAID and DUID
+ /// parameters between two @c Lease6 objects. If these parameters match
+ /// it is an indication that both objects describe the lease for the same
+ /// client but apparently one is a result of renewal of the other. The
+ /// special case of the matching lease is the one that is equal to another.
+ ///
+ /// @param other A lease to compare to.
+ ///
+ /// @return true if selected parameters of the two leases match.
+ bool matches(const Lease6& other) const;
+
+ /// @brief Compare two leases for equality
+ ///
+ /// @param other lease6 object with which to compare
+ bool operator==(const Lease6& other) const;
+
+ /// @brief Compare two leases for inequality
+ ///
+ /// @param other lease6 object with which to compare
+ bool operator!=(const Lease6& other) const {
+ return (!operator==(other));
+ }
+
+ /// @brief Convert Lease to Printable Form
+ ///
+ /// @return String form of the lease
+ virtual std::string toText() const;
+};
+
+/// @brief Pointer to a Lease6 structure.
+typedef boost::shared_ptr<Lease6> Lease6Ptr;
+
+/// @brief Pointer to a const Lease6 structure.
+typedef boost::shared_ptr<const Lease6> ConstLease6Ptr;
+
+/// @brief A collection of IPv6 leases.
+typedef std::vector<Lease6Ptr> Lease6Collection;
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // LEASE_H
diff --git a/src/lib/dhcpsrv/lease_mgr.cc b/src/lib/dhcpsrv/lease_mgr.cc
index 2310dd4..e4fa07f 100644
--- a/src/lib/dhcpsrv/lease_mgr.cc
+++ b/src/lib/dhcpsrv/lease_mgr.cc
@@ -32,33 +32,6 @@ using namespace std;
namespace isc {
namespace dhcp {
-Lease::Lease(const isc::asiolink::IOAddress& addr, uint32_t t1, uint32_t t2,
- uint32_t valid_lft, SubnetID subnet_id, time_t cltt)
- :addr_(addr), t1_(t1), t2_(t2), valid_lft_(valid_lft), cltt_(cltt),
- subnet_id_(subnet_id), fixed_(false), fqdn_fwd_(false), fqdn_rev_(false) {
-}
-
-Lease6::Lease6(LeaseType type, const isc::asiolink::IOAddress& addr,
- DuidPtr duid, uint32_t iaid, uint32_t preferred, uint32_t valid,
- uint32_t t1, uint32_t t2, SubnetID subnet_id, uint8_t prefixlen)
- : Lease(addr, t1, t2, valid, subnet_id, 0/*cltt*/),
- type_(type), prefixlen_(prefixlen), iaid_(iaid), duid_(duid),
- preferred_lft_(preferred) {
- if (!duid) {
- isc_throw(InvalidOperation, "DUID must be specified for a lease");
- }
-
- cltt_ = time(NULL);
-}
-
-bool Lease::expired() const {
-
- // Let's use int64 to avoid problems with negative/large uint32 values
- int64_t expire_time = cltt_ + valid_lft_;
- return (expire_time < time(NULL));
-}
-
-
std::string LeaseMgr::getParameter(const std::string& name) const {
ParameterMap::const_iterator param = parameters_.find(name);
if (param == parameters_.end()) {
@@ -67,101 +40,21 @@ std::string LeaseMgr::getParameter(const std::string& name) const {
return (param->second);
}
-std::string
-Lease6::toText() const {
- ostringstream stream;
+Lease6Ptr
+LeaseMgr::getLease6(Lease::Type type, const DUID& duid,
+ uint32_t iaid, SubnetID subnet_id) const {
+ Lease6Collection col = getLeases6(type, duid, iaid, subnet_id);
- stream << "Type: " << static_cast<int>(type_) << " (";
- switch (type_) {
- case Lease6::LEASE_IA_NA:
- stream << "IA_NA)\n";
- break;
- case Lease6::LEASE_IA_TA:
- stream << "IA_TA)\n";
- break;
- case Lease6::LEASE_IA_PD:
- stream << "IA_PD)\n";
- break;
- default:
- stream << "unknown)\n";
+ if (col.size() > 1) {
+ isc_throw(MultipleRecords, "More than one lease found for type "
+ << static_cast<int>(type) << ", duid "
+ << duid.toText() << ", iaid " << iaid
+ << " and subnet-id " << subnet_id);
}
- stream << "Address: " << addr_.toText() << "\n"
- << "Prefix length: " << static_cast<int>(prefixlen_) << "\n"
- << "IAID: " << iaid_ << "\n"
- << "Pref life: " << preferred_lft_ << "\n"
- << "Valid life: " << valid_lft_ << "\n"
- << "Cltt: " << cltt_ << "\n"
- << "Subnet ID: " << subnet_id_ << "\n";
-
- return (stream.str());
-}
-
-std::string
-Lease4::toText() const {
- ostringstream stream;
-
- stream << "Address: " << addr_.toText() << "\n"
- << "Valid life: " << valid_lft_ << "\n"
- << "T1: " << t1_ << "\n"
- << "T2: " << t2_ << "\n"
- << "Cltt: " << cltt_ << "\n"
- << "Subnet ID: " << subnet_id_ << "\n";
-
- return (stream.str());
-}
-
-
-bool
-Lease4::operator==(const Lease4& other) const {
- if ( (client_id_ && !other.client_id_) ||
- (!client_id_ && other.client_id_) ) {
- // One lease has client-id, but the other doesn't
- return false;
+ if (col.empty()) {
+ return (Lease6Ptr());
}
-
- if (client_id_ && other.client_id_ &&
- *client_id_ != *other.client_id_) {
- // Different client-ids
- return false;
- }
-
- return (
- addr_ == other.addr_ &&
- ext_ == other.ext_ &&
- hwaddr_ == other.hwaddr_ &&
- t1_ == other.t1_ &&
- t2_ == other.t2_ &&
- valid_lft_ == other.valid_lft_ &&
- cltt_ == other.cltt_ &&
- subnet_id_ == other.subnet_id_ &&
- fixed_ == other.fixed_ &&
- hostname_ == other.hostname_ &&
- fqdn_fwd_ == other.fqdn_fwd_ &&
- fqdn_rev_ == other.fqdn_rev_ &&
- comments_ == other.comments_
- );
-}
-
-bool
-Lease6::operator==(const Lease6& other) const {
- return (
- addr_ == other.addr_ &&
- type_ == other.type_ &&
- prefixlen_ == other.prefixlen_ &&
- iaid_ == other.iaid_ &&
- *duid_ == *other.duid_ &&
- preferred_lft_ == other.preferred_lft_ &&
- valid_lft_ == other.valid_lft_ &&
- t1_ == other.t1_ &&
- t2_ == other.t2_ &&
- cltt_ == other.cltt_ &&
- subnet_id_ == other.subnet_id_ &&
- fixed_ == other.fixed_ &&
- hostname_ == other.hostname_ &&
- fqdn_fwd_ == other.fqdn_fwd_ &&
- fqdn_rev_ == other.fqdn_rev_ &&
- comments_ == other.comments_
- );
+ return (*col.begin());
}
} // namespace isc::dhcp
diff --git a/src/lib/dhcpsrv/lease_mgr.h b/src/lib/dhcpsrv/lease_mgr.h
index 02e517e..aab7b9e 100644
--- a/src/lib/dhcpsrv/lease_mgr.h
+++ b/src/lib/dhcpsrv/lease_mgr.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -19,6 +19,7 @@
#include <dhcp/duid.h>
#include <dhcp/option.h>
#include <dhcp/hwaddr.h>
+#include <dhcpsrv/lease.h>
#include <dhcpsrv/subnet.h>
#include <exceptions/exceptions.h>
@@ -108,266 +109,6 @@ public:
isc::Exception(file, line, what) {}
};
-/// @brief a common structure for IPv4 and IPv6 leases
-///
-/// This structure holds all information that is common between IPv4 and IPv6
-/// leases.
-struct Lease {
-
- /// @brief Constructor
- ///
- /// @param addr IP address
- /// @param t1 renewal time
- /// @param t2 rebinding time
- /// @param valid_lft Lifetime of the lease
- /// @param subnet_id Subnet identification
- /// @param cltt Client last transmission time
- Lease(const isc::asiolink::IOAddress& addr, uint32_t t1, uint32_t t2,
- uint32_t valid_lft, SubnetID subnet_id, time_t cltt);
-
- /// @brief Destructor
- virtual ~Lease() {}
-
- /// @brief IPv4 ot IPv6 address
- ///
- /// IPv4, IPv6 address or, in the case of a prefix delegation, the prefix.
- isc::asiolink::IOAddress addr_;
-
- /// @brief Renewal timer
- ///
- /// Specifies renewal time. Although technically it is a property of the
- /// IA container and not the address itself, since our data model does not
- /// define a separate IA entity, we are keeping it in the lease. In the
- /// case of multiple addresses/prefixes for the same IA, each must have
- /// consistent T1 and T2 values. This is specified in seconds since cltt.
- uint32_t t1_;
-
- /// @brief Rebinding timer
- ///
- /// Specifies rebinding time. Although technically it is a property of the
- /// IA container and not the address itself, since our data model does not
- /// define a separate IA entity, we are keeping it in the lease. In the
- /// case of multiple addresses/prefixes for the same IA, each must have
- /// consistent T1 and T2 values. This is specified in seconds since cltt.
- uint32_t t2_;
-
- /// @brief Valid lifetime
- ///
- /// Expressed as number of seconds since cltt.
- uint32_t valid_lft_;
-
- /// @brief Client last transmission time
- ///
- /// Specifies a timestamp giving the time when the last transmission from a
- /// client was received.
- time_t cltt_;
-
- /// @brief Subnet identifier
- ///
- /// Specifies the identification of the subnet to which the lease belongs.
- SubnetID subnet_id_;
-
- /// @brief Fixed lease?
- ///
- /// Fixed leases are kept after they are released/expired.
- bool fixed_;
-
- /// @brief Client hostname
- ///
- /// This field may be empty
- std::string hostname_;
-
- /// @brief Forward zone updated?
- ///
- /// Set true if the DNS AAAA record for this lease has been updated.
- bool fqdn_fwd_;
-
- /// @brief Reverse zone updated?
- ///
- /// Set true if the DNS PTR record for this lease has been updated.
- bool fqdn_rev_;
-
- /// @brief Lease comments
- ///
- /// Currently not used. It may be used for keeping comments made by the
- /// system administrator.
- std::string comments_;
-
- /// @brief Convert Lease to Printable Form
- ///
- /// @return String form of the lease
- virtual std::string toText() const = 0;
-
- /// @brief returns true if the lease is expired
- /// @return true if the lease is expired
- bool expired() const;
-
-};
-
-/// @brief Structure that holds a lease for IPv4 address
-///
-/// For performance reasons it is a simple structure, not a class. If we chose
-/// make it a class, all fields would have to made private and getters/setters
-/// would be required. As this is a critical part of the code that will be used
-/// extensively, direct access is warranted.
-struct Lease4 : public Lease {
-
- /// @brief Address extension
- ///
- /// It is envisaged that in some cases IPv4 address will be accompanied
- /// with some additional data. One example of such use are Address + Port
- /// solutions (or Port-restricted Addresses), where several clients may get
- /// the same address, but different port ranges. This feature is not
- /// expected to be widely used. Under normal circumstances, the value
- /// should be 0.
- uint32_t ext_;
-
- /// @brief Hardware address
- std::vector<uint8_t> hwaddr_;
-
- /// @brief Client identifier
- ///
- /// @todo Should this be a pointer to a client ID or the ID itself?
- /// Compare with the DUID in the Lease6 structure.
- ClientIdPtr client_id_;
-
- /// @brief Constructor
- ///
- /// @param addr IPv4 address.
- /// @param hwaddr Hardware address buffer
- /// @param hwaddr_len Length of hardware address buffer
- /// @param clientid Client identification buffer
- /// @param clientid_len Length of client identification buffer
- /// @param valid_lft Lifetime of the lease
- /// @param t1 renewal time
- /// @param t2 rebinding time
- /// @param cltt Client last transmission time
- /// @param subnet_id Subnet identification
- Lease4(const isc::asiolink::IOAddress& addr, const uint8_t* hwaddr, size_t hwaddr_len,
- const uint8_t* clientid, size_t clientid_len, uint32_t valid_lft,
- uint32_t t1, uint32_t t2, time_t cltt, uint32_t subnet_id)
- : Lease(addr, t1, t2, valid_lft, subnet_id, cltt),
- ext_(0), hwaddr_(hwaddr, hwaddr + hwaddr_len) {
- if (clientid_len) {
- client_id_.reset(new ClientId(clientid, clientid_len));
- }
- }
-
- /// @brief Default constructor
- ///
- /// Initialize fields that don't have a default constructor.
- Lease4() : Lease(0, 0, 0, 0, 0, 0) {
- }
-
- /// @brief Compare two leases for equality
- ///
- /// @param other lease6 object with which to compare
- bool operator==(const Lease4& other) const;
-
- /// @brief Compare two leases for inequality
- ///
- /// @param other lease6 object with which to compare
- bool operator!=(const Lease4& other) const {
- return (!operator==(other));
- }
-
- /// @brief Convert lease to printable form
- ///
- /// @return Textual represenation of lease data
- virtual std::string toText() const;
-
- /// @todo: Add DHCPv4 failover related fields here
-};
-
-/// @brief Pointer to a Lease4 structure.
-typedef boost::shared_ptr<Lease4> Lease4Ptr;
-
-/// @brief A collection of IPv4 leases.
-typedef std::vector<Lease4Ptr> Lease4Collection;
-
-
-
-/// @brief Structure that holds a lease for IPv6 address and/or prefix
-///
-/// For performance reasons it is a simple structure, not a class. If we chose
-/// make it a class, all fields would have to made private and getters/setters
-/// would be required. As this is a critical part of the code that will be used
-/// extensively, direct access is warranted.
-struct Lease6 : public Lease {
-
- /// @brief Type of lease contents
- typedef enum {
- LEASE_IA_NA, /// the lease contains non-temporary IPv6 address
- LEASE_IA_TA, /// the lease contains temporary IPv6 address
- LEASE_IA_PD /// the lease contains IPv6 prefix (for prefix delegation)
- } LeaseType;
-
- /// @brief Lease type
- ///
- /// One of normal address, temporary address, or prefix.
- LeaseType type_;
-
- /// @brief IPv6 prefix length
- ///
- /// This is used only for prefix delegations and is ignored otherwise.
- uint8_t prefixlen_;
-
- /// @brief Identity Association Identifier (IAID)
- ///
- /// DHCPv6 stores all addresses and prefixes in IA containers (IA_NA,
- /// IA_TA, IA_PD). All containers may appear more than once in a message.
- /// To differentiate between them, the IAID field is present
- uint32_t iaid_;
-
- /// @brief Client identifier
- DuidPtr duid_;
-
- /// @brief preferred lifetime
- ///
- /// This parameter specifies the preferred lifetime since the lease was
- /// assigned or renewed (cltt), expressed in seconds.
- uint32_t preferred_lft_;
-
- /// @todo: Add DHCPv6 failover related fields here
-
- /// @brief Constructor
- Lease6(LeaseType type, const isc::asiolink::IOAddress& addr, DuidPtr duid,
- uint32_t iaid, uint32_t preferred, uint32_t valid, uint32_t t1,
- uint32_t t2, SubnetID subnet_id, uint8_t prefixlen_ = 0);
-
- /// @brief Constructor
- ///
- /// Initialize fields that don't have a default constructor.
- Lease6() : Lease(isc::asiolink::IOAddress("::"), 0, 0, 0, 0, 0),
- type_(LEASE_IA_NA) {
- }
-
- /// @brief Compare two leases for equality
- ///
- /// @param other lease6 object with which to compare
- bool operator==(const Lease6& other) const;
-
- /// @brief Compare two leases for inequality
- ///
- /// @param other lease6 object with which to compare
- bool operator!=(const Lease6& other) const {
- return (!operator==(other));
- }
-
- /// @brief Convert Lease to Printable Form
- ///
- /// @return String form of the lease
- virtual std::string toText() const;
-};
-
-/// @brief Pointer to a Lease6 structure.
-typedef boost::shared_ptr<Lease6> Lease6Ptr;
-
-/// @brief Pointer to a const Lease6 structure.
-typedef boost::shared_ptr<const Lease6> ConstLease6Ptr;
-
-/// @brief A collection of IPv6 leases.
-typedef std::vector<Lease6Ptr> Lease6Collection;
/// @brief Abstract Lease Manager
///
@@ -401,7 +142,7 @@ public:
///
/// @result true if the lease was added, false if not (because a lease
/// with the same address was already there).
- virtual bool addLease(const Lease4Ptr& lease) = 0;
+ virtual bool addLease(const isc::dhcp::Lease4Ptr& lease) = 0;
/// @brief Adds an IPv6 lease.
///
@@ -462,6 +203,20 @@ public:
/// @return lease collection
virtual Lease4Collection getLease4(const ClientId& clientid) const = 0;
+ /// @brief Returns existing IPv4 lease for a given client identifier,
+ /// HW address and subnet identifier.
+ ///
+ /// @todo Consider whether this function is needed or not. In the basic
+ /// DHCPv4 server implementation it is not used by the allocation engine.
+ ///
+ /// @param client_id A client identifier.
+ /// @param hwaddr Hardware address.
+ /// @param subnet_id A subnet identifier.
+ ///
+ /// @return A pointer to the lease or NULL if the lease is not found.
+ virtual Lease4Ptr getLease4(const ClientId& client_id, const HWAddr& hwaddr,
+ SubnetID subnet_id) const = 0;
+
/// @brief Returns existing IPv4 lease for specified client-id
///
/// There can be at most one lease for a given HW address in a single
@@ -480,10 +235,12 @@ public:
/// The assumption here is that there will not be site or link-local
/// addresses used, so there is no way of having address duplication.
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param addr address of the searched lease
///
/// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const isc::asiolink::IOAddress& addr) const = 0;
+ virtual Lease6Ptr getLease6(Lease::Type type,
+ const isc::asiolink::IOAddress& addr) const = 0;
/// @brief Returns existing IPv6 leases for a given DUID+IA combination
///
@@ -492,22 +249,54 @@ public:
/// can be more than one. Thus return type is a container, not a single
/// pointer.
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
///
- /// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Collection getLease6(const DUID& duid,
- uint32_t iaid) const = 0;
+ /// @return Lease collection (may be empty if no lease is found)
+ virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid,
+ uint32_t iaid) const = 0;
/// @brief Returns existing IPv6 lease for a given DUID+IA combination
///
+ /// There may be more than one address, temp. address or prefix
+ /// for specified duid/iaid/subnet-id tuple.
+ ///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
/// @param subnet_id subnet id of the subnet the lease belongs to
///
- /// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const DUID& duid, uint32_t iaid,
- SubnetID subnet_id) const = 0;
+ /// @return Lease collection (may be empty if no lease is found)
+ virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid,
+ uint32_t iaid, SubnetID subnet_id) const = 0;
+
+
+ /// @brief returns zero or one IPv6 lease for a given duid+iaid+subnet_id
+ ///
+ /// This function is mostly intended to be used in unit-tests during the
+ /// transition from single to multi address per IA. It may also be used
+ /// in other cases where at most one lease is expected in the database.
+ ///
+ /// It is a wrapper around getLease6(), which returns a collection of
+ /// leases. That collection can be converted into a single pointer if
+ /// there are no leases (NULL pointer) or one lease (use that single lease).
+ /// If there are more leases in the collection, the function will
+ /// throw MultipleRecords exception.
+ ///
+ /// Note: This method is not virtual on purpose. It is common for all
+ /// backends.
+ ///
+ /// @param type specifies lease type: (NA, TA or PD)
+ /// @param duid client DUID
+ /// @param iaid IA identifier
+ /// @param subnet_id subnet id of the subnet the lease belongs to
+ ///
+ /// @throw MultipleRecords if there is more than one lease matching
+ ///
+ /// @return Lease pointer (or NULL if none is found)
+ Lease6Ptr getLease6(Lease::Type type, const DUID& duid,
+ uint32_t iaid, SubnetID subnet_id) const;
/// @brief Updates IPv4 lease.
///
diff --git a/src/lib/dhcpsrv/libdhcpsrv.dox b/src/lib/dhcpsrv/libdhcpsrv.dox
index e3d3429..d56029a 100644
--- a/src/lib/dhcpsrv/libdhcpsrv.dox
+++ b/src/lib/dhcpsrv/libdhcpsrv.dox
@@ -61,19 +61,21 @@ separate module, called allocator. Its sole purpose is to pick an address from
a pool. Allocation engine will then check if the picked address is free and if
it is not, then will ask allocator to pick again.
-At lease 3 allocators will be implemented:
-
-- Iterative - it iterates over all addresses in available pools, one
-by one. The advantages of this approach are speed (typically it only needs to
-increase last address), the guarantee to cover all addresses and predictability.
-This allocator behaves very good in case of nearing depletion. Even when pools
-are almost completely allocated, it still will be able to allocate outstanding
-leases efficiently. Predictability can also be considered a serious flaw in
-some environments, as prediction of the next address is trivial and can be
-leveraged by an attacker. Another drawback of this allocator is that it does
-not attempt to give the same address to returning clients (clients that released
-or expired their leases and are requesting a new lease will likely to get a
-different lease). This allocator is implemented in \ref isc::dhcp::AllocEngine::IterativeAllocator.
+At least 3 allocators will be implemented:
+
+- Iterative - it iterates over all resources (addresses or prefixes) in
+available pools, one by one. The advantages of this approach are: speed
+(typically it only needs to increase address just one), the guarantee to cover
+all addresses and predictability. This allocator behaves reasonably good in
+case of nearing depletion. Even when pools are almost completely allocated, it
+still will be able to allocate outstanding leases efficiently. Predictability
+can also be considered a serious flaw in some environments, as prediction of the
+next address is trivial and can be leveraged by an attacker. Another drawback of
+this allocator is that it does not attempt to give the same address to returning
+clients (clients that released or expired their leases and are requesting a new
+lease will likely to get a different lease). This allocator is not suitable for
+temporary addresses, which must be randomized. This allocator is implemented
+in \ref isc::dhcp::AllocEngine::IterativeAllocator.
- Hashed - ISC-DHCP uses hash of the client-id or DUID to determine, which
address is tried first. If that address is not available, the result is hashed
@@ -81,13 +83,13 @@ again. That procedure is repeated until available address is found or there
are no more addresses left. The benefit of that approach is that it provides
a relative lease stability, so returning old clients are likely to get the same
address again. The drawbacks are increased computation cost, as each iteration
-requires use of a hashing function. That is especially difficult when the
+requires use of a hashing function. That is especially difficult when the
pools are almost depleted. It also may be difficult to guarantee that the
repeated hashing will iterate over all available addresses in all pools. Flawed
hash algorithm can go into cycles that iterate over only part of the addresses.
It is difficult to detect such issues as only some initial seed (client-id
or DUID) values may trigger short cycles. This allocator is currently not
-implemented.
+implemented. This will be the only allocator allowed for temporary addresses.
- Random - Another possible approach to address selection is randomization. This
allocator can pick an address randomly from the configured pool. The benefit
@@ -97,4 +99,26 @@ returning clients are almost guaranteed to get a different address. Another
drawback is that with almost depleted pools it is increasingly difficult to
"guess" an address that is free. This allocator is currently not implemented.
+ at subsection allocEngineTypes Different lease types support
+
+Allocation Engine has been extended to support different types of leases. Four
+types are supported: TYPE_V4 (IPv4 addresses), TYPE_NA (normal IPv6 addresses),
+TYPE_TA (temporary IPv6 addresses) and TYPE_PD (delegated prefixes). Support for
+TYPE_TA is partial. Some routines are able to handle it, while other are
+not. The major missing piece is the RandomAllocator, so there is no way to randomly
+generate an address. This defeats the purpose of using temporary addresses for now.
+
+ at subsection allocEnginePD Prefix Delegation support in AllocEngine
+
+The Allocation Engine supports allocation of the IPv6 addresses and prefixes.
+For a prefix pool, the iterative allocator "walks over"
+every available pool. It is similar to how it iterates over address pool,
+but instead of increasing address by just one, it walks over the whole delegated
+prefix length in one step. This is implemented in
+isc::dhcp::AllocEngine::IterativeAllocator::increasePrefix(). Functionally the
+increaseAddress(addr) call is equivalent to increasePrefix(addr, 128)
+(increasing by a /128 prefix, i.e. a single address). However, both methods are
+kept, because increaseAddress() is faster and this is a routine that may be
+called many hundred thousands times per second.
+
*/
diff --git a/src/lib/dhcpsrv/memfile_lease_mgr.cc b/src/lib/dhcpsrv/memfile_lease_mgr.cc
index 713d625..c6c9c9f 100644
--- a/src/lib/dhcpsrv/memfile_lease_mgr.cc
+++ b/src/lib/dhcpsrv/memfile_lease_mgr.cc
@@ -28,7 +28,8 @@ Memfile_LeaseMgr::Memfile_LeaseMgr(const ParameterMap& parameters)
Memfile_LeaseMgr::~Memfile_LeaseMgr() {
}
-bool Memfile_LeaseMgr::addLease(const Lease4Ptr& lease) {
+bool
+Memfile_LeaseMgr::addLease(const Lease4Ptr& lease) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_ADD_ADDR4).arg(lease->addr_.toText());
@@ -40,11 +41,12 @@ bool Memfile_LeaseMgr::addLease(const Lease4Ptr& lease) {
return (true);
}
-bool Memfile_LeaseMgr::addLease(const Lease6Ptr& lease) {
+bool
+Memfile_LeaseMgr::addLease(const Lease6Ptr& lease) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_ADD_ADDR6).arg(lease->addr_.toText());
- if (getLease6(lease->addr_)) {
+ if (getLease6(lease->type_, lease->addr_)) {
// there is a lease with specified address already
return (false);
}
@@ -52,7 +54,8 @@ bool Memfile_LeaseMgr::addLease(const Lease6Ptr& lease) {
return (true);
}
-Lease4Ptr Memfile_LeaseMgr::getLease4(const isc::asiolink::IOAddress& addr) const {
+Lease4Ptr
+Memfile_LeaseMgr::getLease4(const isc::asiolink::IOAddress& addr) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_ADDR4).arg(addr.toText());
@@ -62,19 +65,31 @@ Lease4Ptr Memfile_LeaseMgr::getLease4(const isc::asiolink::IOAddress& addr) cons
if (l == storage4_.end()) {
return (Lease4Ptr());
} else {
- return (*l);
+ return (Lease4Ptr(new Lease4(**l)));
}
}
-Lease4Collection Memfile_LeaseMgr::getLease4(const HWAddr& hwaddr) const {
+Lease4Collection
+Memfile_LeaseMgr::getLease4(const HWAddr& hwaddr) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_HWADDR).arg(hwaddr.toText());
+ typedef Lease4Storage::nth_index<0>::type SearchIndex;
+ Lease4Collection collection;
+ const SearchIndex& idx = storage4_.get<0>();
+ for(SearchIndex::const_iterator lease = idx.begin();
+ lease != idx.end(); ++lease) {
+
+ // Every Lease4 has a hardware address, so we can compare it
+ if ((*lease)->hwaddr_ == hwaddr.hwaddr_) {
+ collection.push_back((*lease));
+ }
+ }
- isc_throw(NotImplemented, "getLease4(HWaddr x) method not implemented yet");
+ return (collection);
}
-Lease4Ptr Memfile_LeaseMgr::getLease4(const HWAddr& hwaddr,
- SubnetID subnet_id) const {
+Lease4Ptr
+Memfile_LeaseMgr::getLease4(const HWAddr& hwaddr, SubnetID subnet_id) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_SUBID_HWADDR).arg(subnet_id)
.arg(hwaddr.toText());
@@ -90,21 +105,65 @@ Lease4Ptr Memfile_LeaseMgr::getLease4(const HWAddr& hwaddr,
idx.find(boost::make_tuple(hwaddr.hwaddr_, subnet_id));
// Lease was not found. Return empty pointer to the caller.
if (lease == idx.end()) {
- return Lease4Ptr();
+ return (Lease4Ptr());
}
// Lease was found. Return it to the caller.
- return (*lease);
+ return (Lease4Ptr(new Lease4(**lease)));
}
-Lease4Collection Memfile_LeaseMgr::getLease4(const ClientId& clientid) const {
+Lease4Collection
+Memfile_LeaseMgr::getLease4(const ClientId& client_id) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
- DHCPSRV_MEMFILE_GET_CLIENTID).arg(clientid.toText());
- isc_throw(NotImplemented, "getLease4(ClientId) not implemented");
+ DHCPSRV_MEMFILE_GET_CLIENTID).arg(client_id.toText());
+ typedef Memfile_LeaseMgr::Lease4Storage::nth_index<0>::type SearchIndex;
+ Lease4Collection collection;
+ const SearchIndex& idx = storage4_.get<0>();
+ for(SearchIndex::const_iterator lease = idx.begin();
+ lease != idx.end(); ++ lease) {
+
+ // client-id is not mandatory in DHCPv4. There can be a lease that does
+ // not have a client-id. Dereferencing null pointer would be a bad thing
+ if((*lease)->client_id_ && *(*lease)->client_id_ == client_id) {
+ collection.push_back((*lease));
+ }
+ }
+
+ return (collection);
+}
+
+Lease4Ptr
+Memfile_LeaseMgr::getLease4(const ClientId& client_id,
+ const HWAddr& hwaddr,
+ SubnetID subnet_id) const {
+ LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
+ DHCPSRV_MEMFILE_GET_CLIENTID_HWADDR_SUBID).arg(client_id.toText())
+ .arg(hwaddr.toText())
+ .arg(subnet_id);
+
+ // We are going to use index #3 of the multi index container.
+ // We define SearchIndex locally in this function because
+ // currently only this function uses this index.
+ typedef Lease4Storage::nth_index<3>::type SearchIndex;
+ // Get the index.
+ const SearchIndex& idx = storage4_.get<3>();
+ // Try to get the lease using client id, hardware address and subnet id.
+ SearchIndex::const_iterator lease =
+ idx.find(boost::make_tuple(client_id.getClientId(), hwaddr.hwaddr_,
+ subnet_id));
+
+ if (lease == idx.end()) {
+ // Lease was not found. Return empty pointer to the caller.
+ return (Lease4Ptr());
+ }
+
+ // Lease was found. Return it to the caller.
+ return (*lease);
}
-Lease4Ptr Memfile_LeaseMgr::getLease4(const ClientId& client_id,
- SubnetID subnet_id) const {
+Lease4Ptr
+Memfile_LeaseMgr::getLease4(const ClientId& client_id,
+ SubnetID subnet_id) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_SUBID_CLIENTID).arg(subnet_id)
.arg(client_id.toText());
@@ -120,14 +179,15 @@ Lease4Ptr Memfile_LeaseMgr::getLease4(const ClientId& client_id,
idx.find(boost::make_tuple(client_id.getClientId(), subnet_id));
// Lease was not found. Return empty pointer to the caller.
if (lease == idx.end()) {
- return Lease4Ptr();
+ return (Lease4Ptr());
}
// Lease was found. Return it to the caller.
- return (*lease);
+ return (Lease4Ptr(new Lease4(**lease)));
}
-Lease6Ptr Memfile_LeaseMgr::getLease6(
- const isc::asiolink::IOAddress& addr) const {
+Lease6Ptr
+Memfile_LeaseMgr::getLease6(Lease::Type /* not used yet */,
+ const isc::asiolink::IOAddress& addr) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_ADDR6).arg(addr.toText());
@@ -135,20 +195,25 @@ Lease6Ptr Memfile_LeaseMgr::getLease6(
if (l == storage6_.end()) {
return (Lease6Ptr());
} else {
- return (*l);
+ return (Lease6Ptr(new Lease6(**l)));
}
}
-Lease6Collection Memfile_LeaseMgr::getLease6(const DUID& duid,
- uint32_t iaid) const {
+Lease6Collection
+Memfile_LeaseMgr::getLeases6(Lease::Type /* not used yet */,
+ const DUID& duid, uint32_t iaid) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_IAID_DUID).arg(iaid).arg(duid.toText());
+ /// @todo Not implemented.
+
return (Lease6Collection());
}
-Lease6Ptr Memfile_LeaseMgr::getLease6(const DUID& duid, uint32_t iaid,
- SubnetID subnet_id) const {
+Lease6Collection
+Memfile_LeaseMgr::getLeases6(Lease::Type /* not used yet */,
+ const DUID& duid, uint32_t iaid,
+ SubnetID subnet_id) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_GET_IAID_SUBID_DUID)
.arg(iaid).arg(subnet_id).arg(duid.toText());
@@ -164,26 +229,44 @@ Lease6Ptr Memfile_LeaseMgr::getLease6(const DUID& duid, uint32_t iaid,
idx.find(boost::make_tuple(duid.getDuid(), iaid, subnet_id));
// Lease was not found. Return empty pointer.
if (lease == idx.end()) {
- return (Lease6Ptr());
+ return (Lease6Collection());
}
+
// Lease was found, return it to the caller.
- return (*lease);
+ /// @todo: allow multiple leases for a single duid+iaid+subnet_id tuple
+ Lease6Collection collection;
+ collection.push_back(Lease6Ptr(new Lease6(**lease)));
+ return (collection);
}
-void Memfile_LeaseMgr::updateLease4(const Lease4Ptr& lease) {
+void
+Memfile_LeaseMgr::updateLease4(const Lease4Ptr& lease) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_UPDATE_ADDR4).arg(lease->addr_.toText());
+ Lease4Storage::iterator lease_it = storage4_.find(lease->addr_);
+ if (lease_it == storage4_.end()) {
+ isc_throw(NoSuchLease, "failed to update the lease with address "
+ << lease->addr_ << " - no such lease");
+ }
+ **lease_it = *lease;
}
-void Memfile_LeaseMgr::updateLease6(const Lease6Ptr& lease) {
+void
+Memfile_LeaseMgr::updateLease6(const Lease6Ptr& lease) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_UPDATE_ADDR6).arg(lease->addr_.toText());
-
+ Lease6Storage::iterator lease_it = storage6_.find(lease->addr_);
+ if (lease_it == storage6_.end()) {
+ isc_throw(NoSuchLease, "failed to update the lease with address "
+ << lease->addr_ << " - no such lease");
+ }
+ **lease_it = *lease;
}
-bool Memfile_LeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) {
+bool
+Memfile_LeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MEMFILE_DELETE_ADDR).arg(addr.toText());
if (addr.isV4()) {
@@ -210,7 +293,8 @@ bool Memfile_LeaseMgr::deleteLease(const isc::asiolink::IOAddress& addr) {
}
}
-std::string Memfile_LeaseMgr::getDescription() const {
+std::string
+Memfile_LeaseMgr::getDescription() const {
return (std::string("This is a dummy memfile backend implementation.\n"
"It does not offer any useful lease management and its only\n"
"purpose is to test abstract lease manager API."));
diff --git a/src/lib/dhcpsrv/memfile_lease_mgr.h b/src/lib/dhcpsrv/memfile_lease_mgr.h
index 4ac7536..66548f6 100644
--- a/src/lib/dhcpsrv/memfile_lease_mgr.h
+++ b/src/lib/dhcpsrv/memfile_lease_mgr.h
@@ -16,7 +16,6 @@
#define MEMFILE_LEASE_MGR_H
#include <dhcp/hwaddr.h>
-#include <dhcpsrv/key_from_key.h>
#include <dhcpsrv/lease_mgr.h>
#include <boost/multi_index/indexed_by.hpp>
@@ -54,7 +53,6 @@ public:
/// @brief Adds an IPv4 lease.
///
- /// @todo Not implemented yet
/// @param lease lease to be added
virtual bool addLease(const Lease4Ptr& lease);
@@ -65,16 +63,16 @@ public:
/// @brief Returns existing IPv4 lease for specified IPv4 address.
///
- /// @todo Not implemented yet
- /// @param addr address of the searched lease
+ /// This function returns a copy of the lease. The modification in the
+ /// return lease does not affect the instance held in the lease storage.
+ ///
+ /// @param addr An address of the searched lease.
///
/// @return a collection of leases
virtual Lease4Ptr getLease4(const isc::asiolink::IOAddress& addr) const;
/// @brief Returns existing IPv4 leases for specified hardware address.
///
- /// @todo Not implemented yet
- ///
/// Although in the usual case there will be only one lease, for mobile
/// clients or clients with multiple static/fixed/reserved leases there
/// can be more than one. Thus return type is a container, not a single
@@ -85,10 +83,11 @@ public:
/// @return lease collection
virtual Lease4Collection getLease4(const isc::dhcp::HWAddr& hwaddr) const;
- /// @brief Returns existing IPv4 leases for specified hardware address
+ /// @brief Returns existing IPv4 lease for specified hardware address
/// and a subnet
///
- /// @todo Not implemented yet
+ /// This function returns a copy of the lease. The modification in the
+ /// return lease does not affect the instance held in the lease storage.
///
/// There can be at most one lease for a given HW address in a single
/// pool, so this method with either return a single lease or NULL.
@@ -102,18 +101,32 @@ public:
/// @brief Returns existing IPv4 lease for specified client-id
///
- /// @todo Not implemented yet
+ /// @param client_id client identifier
+ virtual Lease4Collection getLease4(const ClientId& client_id) const;
+
+ /// @brief Returns IPv4 lease for specified client-id/hwaddr/subnet-id tuple
+ ///
+ /// There can be at most one lease for a given client-id/hwaddr tuple
+ /// in a single pool, so this method with either return a single lease
+ /// or NULL.
///
/// @param clientid client identifier
- virtual Lease4Collection getLease4(const ClientId& clientid) const;
+ /// @param hwaddr hardware address of the client
+ /// @param subnet_id identifier of the subnet that lease must belong to
+ ///
+ /// @return a pointer to the lease (or NULL if a lease is not found)
+ virtual Lease4Ptr getLease4(const ClientId& clientid,
+ const HWAddr& hwaddr,
+ SubnetID subnet_id) const;
/// @brief Returns existing IPv4 lease for specified client-id
///
+ /// This function returns a copy of the lease. The modification in the
+ /// return lease does not affect the instance held in the lease storage.
+ ///
/// There can be at most one lease for a given HW address in a single
/// pool, so this method with either return a single lease or NULL.
///
- /// @todo Not implemented yet
- ///
/// @param clientid client identifier
/// @param subnet_id identifier of the subnet that lease must belong to
///
@@ -123,44 +136,56 @@ public:
/// @brief Returns existing IPv6 lease for a given IPv6 address.
///
- /// @param addr address of the searched lease
+ /// This function returns a copy of the lease. The modification in the
+ /// return lease does not affect the instance held in the lease storage.
+ ///
+ /// @param type specifies lease type: (NA, TA or PD)
+ /// @param addr An address of the searched lease.
///
/// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const isc::asiolink::IOAddress& addr) const;
+ virtual Lease6Ptr getLease6(Lease::Type type,
+ const isc::asiolink::IOAddress& addr) const;
/// @brief Returns existing IPv6 lease for a given DUID+IA combination
///
/// @todo Not implemented yet
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
///
/// @return collection of IPv6 leases
- virtual Lease6Collection getLease6(const DUID& duid, uint32_t iaid) const;
+ virtual Lease6Collection getLeases6(Lease::Type type,
+ const DUID& duid, uint32_t iaid) const;
- /// @brief Returns existing IPv6 lease for a given DUID+IA combination
+ /// @brief Returns existing IPv6 lease for a given DUID/IA/subnet-id tuple
///
- /// @todo Not implemented yet
+ /// This function returns a copy of the lease. The modification in the
+ /// return lease does not affect the instance held in the lease storage.
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
/// @param subnet_id identifier of the subnet the lease must belong to
///
- /// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const DUID& duid, uint32_t iaid, SubnetID subnet_id) const;
+ /// @return lease collection (may be empty if no lease is found)
+ virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid,
+ uint32_t iaid, SubnetID subnet_id) const;
/// @brief Updates IPv4 lease.
///
- /// @todo Not implemented yet
+ /// @warning This function does not validate the pointer to the lease.
+ /// It is caller's responsibility to pass the valid pointer.
///
/// @param lease4 The lease to be updated.
///
/// If no such lease is present, an exception will be thrown.
virtual void updateLease4(const Lease4Ptr& lease4);
- /// @brief Updates IPv4 lease.
+ /// @brief Updates IPv6 lease.
///
- /// @todo Not implemented yet
+ /// @warning This function does not validate the pointer to the lease.
+ /// It is caller's responsibility to pass the valid pointer.
///
/// @param lease6 The lease to be updated.
///
@@ -241,20 +266,10 @@ protected:
// the lease using three attributes: DUID, IAID, Subnet Id.
boost::multi_index::composite_key<
Lease6,
- // The DUID value can't be directly accessed from the Lease6
- // object because it is wrapped with the DUID object (actually
- // pointer to this object). Therefore we need to use
- // KeyFromKeyExtractor class to extract the DUID value from
- // this cascaded structure.
- KeyFromKeyExtractor<
- // The value of the DUID is accessed by the getDuid() method
- // from the DUID object.
- boost::multi_index::const_mem_fun<DUID, std::vector<uint8_t>,
- &DUID::getDuid>,
- // The DUID object is stored in the duid_ member of the
- // Lease6 object.
- boost::multi_index::member<Lease6, DuidPtr, &Lease6::duid_>
- >,
+ // The DUID can be retrieved from the Lease6 object using
+ // a getDuidVector const function.
+ boost::multi_index::const_mem_fun<Lease6, const std::vector<uint8_t>&,
+ &Lease6::getDuidVector>,
// The two other ingredients of this index are IAID and
// subnet id.
boost::multi_index::member<Lease6, uint32_t, &Lease6::iaid_>,
@@ -304,23 +319,32 @@ protected:
// lease: client id and subnet id.
boost::multi_index::composite_key<
Lease4,
- // The client id value is not directly accessible through the
- // Lease4 object as it is wrapped with the ClientIdPtr object.
- // Therefore we use the KeyFromKeyExtractor class to access
- // client id through this cascaded structure. The client id
- // is used as an index value.
- KeyFromKeyExtractor<
- // Specify that the vector holding client id value can be obtained
- // from the ClientId object.
- boost::multi_index::const_mem_fun<ClientId, std::vector<uint8_t>,
- &ClientId::getClientId>,
- // Specify that the ClientId object (actually pointer to it) can
- // be accessed by the client_id_ member of Lease4 class.
- boost::multi_index::member<Lease4, ClientIdPtr, &Lease4::client_id_>
- >,
+ // The client id can be retrieved from the Lease4 object by
+ // calling getClientIdVector const function.
+ boost::multi_index::const_mem_fun<Lease4, const std::vector<uint8_t>&,
+ &Lease4::getClientIdVector>,
// The subnet id is accessed through the subnet_id_ member.
boost::multi_index::member<Lease, uint32_t, &Lease::subnet_id_>
>
+ >,
+
+ // Specification of the fourth index starts here.
+ boost::multi_index::ordered_unique<
+ // This is a composite index that uses two values to search for a
+ // lease: client id and subnet id.
+ boost::multi_index::composite_key<
+ Lease4,
+ // The client id can be retrieved from the Lease4 object by
+ // calling getClientIdVector const function.
+ boost::multi_index::const_mem_fun<Lease4, const std::vector<uint8_t>&,
+ &Lease4::getClientIdVector>,
+ // The hardware address is held in the hwaddr_ member of the
+ // Lease4 object.
+ boost::multi_index::member<Lease4, std::vector<uint8_t>,
+ &Lease4::hwaddr_>,
+ // The subnet id is accessed through the subnet_id_ member.
+ boost::multi_index::member<Lease, SubnetID, &Lease::subnet_id_>
+ >
>
>
> Lease4Storage; // Specify the type name for this container.
diff --git a/src/lib/dhcpsrv/mysql_lease_mgr.cc b/src/lib/dhcpsrv/mysql_lease_mgr.cc
index 9828085..c006bbf 100644
--- a/src/lib/dhcpsrv/mysql_lease_mgr.cc
+++ b/src/lib/dhcpsrv/mysql_lease_mgr.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -104,6 +104,12 @@ const size_t ADDRESS6_TEXT_MAX_LEN = 39;
const my_bool MLM_FALSE = 0; ///< False value
const my_bool MLM_TRUE = 1; ///< True value
+/// @brief Maximum length of the hostname stored in DNS.
+///
+/// This length is restricted by the length of the domain-name carried
+/// in the Client FQDN %Option (see RFC4702 and RFC4704).
+const size_t HOSTNAME_MAX_LEN = 255;
+
///@}
/// @brief MySQL Selection Statements
@@ -123,68 +129,81 @@ TaggedStatement tagged_statements[] = {
"DELETE FROM lease6 WHERE address = ?"},
{MySqlLeaseMgr::GET_LEASE4_ADDR,
"SELECT address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id "
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease4 "
"WHERE address = ?"},
{MySqlLeaseMgr::GET_LEASE4_CLIENTID,
"SELECT address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id "
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease4 "
"WHERE client_id = ?"},
{MySqlLeaseMgr::GET_LEASE4_CLIENTID_SUBID,
"SELECT address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id "
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease4 "
"WHERE client_id = ? AND subnet_id = ?"},
{MySqlLeaseMgr::GET_LEASE4_HWADDR,
"SELECT address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id "
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease4 "
"WHERE hwaddr = ?"},
{MySqlLeaseMgr::GET_LEASE4_HWADDR_SUBID,
"SELECT address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id "
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease4 "
"WHERE hwaddr = ? AND subnet_id = ?"},
{MySqlLeaseMgr::GET_LEASE6_ADDR,
"SELECT address, duid, valid_lifetime, "
"expire, subnet_id, pref_lifetime, "
- "lease_type, iaid, prefix_len "
+ "lease_type, iaid, prefix_len, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease6 "
- "WHERE address = ?"},
+ "WHERE address = ? AND lease_type = ?"},
{MySqlLeaseMgr::GET_LEASE6_DUID_IAID,
"SELECT address, duid, valid_lifetime, "
"expire, subnet_id, pref_lifetime, "
- "lease_type, iaid, prefix_len "
+ "lease_type, iaid, prefix_len, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease6 "
- "WHERE duid = ? AND iaid = ?"},
+ "WHERE duid = ? AND iaid = ? AND lease_type = ?"},
{MySqlLeaseMgr::GET_LEASE6_DUID_IAID_SUBID,
"SELECT address, duid, valid_lifetime, "
"expire, subnet_id, pref_lifetime, "
- "lease_type, iaid, prefix_len "
+ "lease_type, iaid, prefix_len, "
+ "fqdn_fwd, fqdn_rev, hostname "
"FROM lease6 "
- "WHERE duid = ? AND iaid = ? AND subnet_id = ?"},
+ "WHERE duid = ? AND iaid = ? AND subnet_id = ? "
+ "AND lease_type = ?"},
{MySqlLeaseMgr::GET_VERSION,
"SELECT version, minor FROM schema_version"},
{MySqlLeaseMgr::INSERT_LEASE4,
"INSERT INTO lease4(address, hwaddr, client_id, "
- "valid_lifetime, expire, subnet_id) "
- "VALUES (?, ?, ?, ?, ?, ?)"},
+ "valid_lifetime, expire, subnet_id, "
+ "fqdn_fwd, fqdn_rev, hostname) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
{MySqlLeaseMgr::INSERT_LEASE6,
"INSERT INTO lease6(address, duid, valid_lifetime, "
"expire, subnet_id, pref_lifetime, "
- "lease_type, iaid, prefix_len) "
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"},
+ "lease_type, iaid, prefix_len, "
+ "fqdn_fwd, fqdn_rev, hostname) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"},
{MySqlLeaseMgr::UPDATE_LEASE4,
"UPDATE lease4 SET address = ?, hwaddr = ?, "
"client_id = ?, valid_lifetime = ?, expire = ?, "
- "subnet_id = ? "
+ "subnet_id = ?, fqdn_fwd = ?, fqdn_rev = ?, "
+ "hostname = ? "
"WHERE address = ?"},
{MySqlLeaseMgr::UPDATE_LEASE6,
"UPDATE lease6 SET address = ?, duid = ?, "
"valid_lifetime = ?, expire = ?, subnet_id = ?, "
"pref_lifetime = ?, lease_type = ?, iaid = ?, "
- "prefix_len = ? "
+ "prefix_len = ?, fqdn_fwd = ?, fqdn_rev = ?, "
+ "hostname = ? "
"WHERE address = ?"},
// End of list sentinel
{MySqlLeaseMgr::NUM_STATEMENTS, NULL}
@@ -275,16 +294,18 @@ public:
class MySqlLease4Exchange : public MySqlLeaseExchange {
/// @brief Set number of database columns for this lease structure
- static const size_t LEASE_COLUMNS = 6;
+ static const size_t LEASE_COLUMNS = 9;
public:
/// @brief Constructor
///
/// The initialization of the variables here is only to satisfy cppcheck -
/// all variables are initialized/set in the methods before they are used.
- MySqlLease4Exchange() : addr4_(0), hwaddr_length_(0), client_id_length_(0) {
+ MySqlLease4Exchange() : addr4_(0), hwaddr_length_(0), client_id_length_(0),
+ fqdn_fwd_(false), fqdn_rev_(false), hostname_length_(0) {
memset(hwaddr_buffer_, 0, sizeof(hwaddr_buffer_));
memset(client_id_buffer_, 0, sizeof(client_id_buffer_));
+ memset(hostname_buffer_, 0, sizeof(hostname_buffer_));
std::fill(&error_[0], &error_[LEASE_COLUMNS], MLM_FALSE);
// Set the column names (for error messages)
@@ -294,7 +315,10 @@ public:
columns_[3] = "valid_lifetime";
columns_[4] = "expire";
columns_[5] = "subnet_id";
- BOOST_STATIC_ASSERT(5 < LEASE_COLUMNS);
+ columns_[6] = "fqdn_fwd";
+ columns_[7] = "fqdn_rev";
+ columns_[8] = "hostname";
+ BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
}
/// @brief Create MYSQL_BIND objects for Lease4 Pointer
@@ -397,11 +421,32 @@ public:
// bind_[5].is_null = &MLM_FALSE; // commented out for performance
// reasons, see memset() above
+ // fqdn_fwd: boolean
+ bind_[6].buffer_type = MYSQL_TYPE_TINY;
+ bind_[6].buffer = reinterpret_cast<char*>(&lease_->fqdn_fwd_);
+ bind_[6].is_unsigned = MLM_TRUE;
+ // bind_[6].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // fqdn_rev: boolean
+ bind_[7].buffer_type = MYSQL_TYPE_TINY;
+ bind_[7].buffer = reinterpret_cast<char*>(&lease_->fqdn_rev_);
+ bind_[7].is_unsigned = MLM_TRUE;
+ // bind_[7].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // hostname: varchar(255)
+ bind_[8].buffer_type = MYSQL_TYPE_VARCHAR;
+ bind_[8].buffer = const_cast<char*>(lease_->hostname_.c_str());
+ bind_[8].buffer_length = lease_->hostname_.length();
+ // bind_[8].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
// Add the error flags
setErrorIndicators(bind_, error_, LEASE_COLUMNS);
// .. and check that we have the numbers correct at compile time.
- BOOST_STATIC_ASSERT(5 < LEASE_COLUMNS);
+ BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
// Add the data to the vector. Note the end element is one after the
// end of the array.
@@ -470,11 +515,34 @@ public:
// bind_[5].is_null = &MLM_FALSE; // commented out for performance
// reasons, see memset() above
+ // fqdn_fwd: boolean
+ bind_[6].buffer_type = MYSQL_TYPE_TINY;
+ bind_[6].buffer = reinterpret_cast<char*>(&fqdn_fwd_);
+ bind_[6].is_unsigned = MLM_TRUE;
+ // bind_[6].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // fqdn_rev: boolean
+ bind_[7].buffer_type = MYSQL_TYPE_TINY;
+ bind_[7].buffer = reinterpret_cast<char*>(&fqdn_rev_);
+ bind_[7].is_unsigned = MLM_TRUE;
+ // bind_[7].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // hostname: varchar(255)
+ hostname_length_ = sizeof(hostname_buffer_);
+ bind_[8].buffer_type = MYSQL_TYPE_STRING;
+ bind_[8].buffer = reinterpret_cast<char*>(hostname_buffer_);
+ bind_[8].buffer_length = hostname_length_;
+ bind_[8].length = &hostname_length_;
+ // bind_[8].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
// Add the error flags
setErrorIndicators(bind_, error_, LEASE_COLUMNS);
// .. and check that we have the numbers correct at compile time.
- BOOST_STATIC_ASSERT(5 < LEASE_COLUMNS);
+ BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
// Add the data to the vector. Note the end element is one after the
// end of the array.
@@ -500,10 +568,16 @@ public:
client_id_length_ = 0;
}
+ // Hostname is passed to Lease4 as a string object. We have to create
+ // it from the buffer holding hostname and the buffer length.
+ std::string hostname(hostname_buffer_,
+ hostname_buffer_ + hostname_length_);
+
// note that T1 and T2 are not stored
return (Lease4Ptr(new Lease4(addr4_, hwaddr_buffer_, hwaddr_length_,
client_id_buffer_, client_id_length_,
- valid_lifetime_, 0, 0, cltt, subnet_id_)));
+ valid_lifetime_, 0, 0, cltt, subnet_id_,
+ fqdn_fwd_, fqdn_rev_, hostname)));
}
/// @brief Return columns in error
@@ -543,6 +617,15 @@ private:
Lease4Ptr lease_; ///< Pointer to lease object
uint32_t subnet_id_; ///< Subnet identification
uint32_t valid_lifetime_; ///< Lease time
+
+ my_bool fqdn_fwd_; ///< Has forward DNS update been
+ ///< performed
+ my_bool fqdn_rev_; ///< Has reverse DNS update been
+ ///< performed
+ char hostname_buffer_[HOSTNAME_MAX_LEN];
+ ///< Client hostname
+ unsigned long hostname_length_; ///< Client hostname length
+
};
@@ -562,28 +645,34 @@ private:
class MySqlLease6Exchange : public MySqlLeaseExchange {
/// @brief Set number of database columns for this lease structure
- static const size_t LEASE_COLUMNS = 9;
+ static const size_t LEASE_COLUMNS = 12;
public:
/// @brief Constructor
///
/// The initialization of the variables here is nonly to satisfy cppcheck -
/// all variables are initialized/set in the methods before they are used.
- MySqlLease6Exchange() : addr6_length_(0), duid_length_(0) {
+ MySqlLease6Exchange() : addr6_length_(0), duid_length_(0),
+ fqdn_fwd_(false), fqdn_rev_(false),
+ hostname_length_(0) {
memset(addr6_buffer_, 0, sizeof(addr6_buffer_));
memset(duid_buffer_, 0, sizeof(duid_buffer_));
+ memset(hostname_buffer_, 0, sizeof(hostname_buffer_));
std::fill(&error_[0], &error_[LEASE_COLUMNS], MLM_FALSE);
// Set the column names (for error messages)
- columns_[0] = "address";
- columns_[1] = "duid";
- columns_[2] = "valid_lifetime";
- columns_[3] = "expire";
- columns_[4] = "subnet_id";
- columns_[5] = "pref_lifetime";
- columns_[6] = "lease_type";
- columns_[7] = "iaid";
- columns_[8] = "prefix_len";
+ columns_[0] = "address";
+ columns_[1] = "duid";
+ columns_[2] = "valid_lifetime";
+ columns_[3] = "expire";
+ columns_[4] = "subnet_id";
+ columns_[5] = "pref_lifetime";
+ columns_[6] = "lease_type";
+ columns_[7] = "iaid";
+ columns_[8] = "prefix_len";
+ columns_[9] = "fqdn_fwd";
+ columns_[10] = "fqdn_rev";
+ columns_[11] = "hostname";
BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
}
@@ -707,11 +796,32 @@ public:
// bind_[8].is_null = &MLM_FALSE; // commented out for performance
// reasons, see memset() above
+ // fqdn_fwd: boolean
+ bind_[9].buffer_type = MYSQL_TYPE_TINY;
+ bind_[9].buffer = reinterpret_cast<char*>(&lease_->fqdn_fwd_);
+ bind_[9].is_unsigned = MLM_TRUE;
+ // bind_[7].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // fqdn_rev: boolean
+ bind_[10].buffer_type = MYSQL_TYPE_TINY;
+ bind_[10].buffer = reinterpret_cast<char*>(&lease_->fqdn_rev_);
+ bind_[10].is_unsigned = MLM_TRUE;
+ // bind_[10].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // hostname: varchar(255)
+ bind_[11].buffer_type = MYSQL_TYPE_VARCHAR;
+ bind_[11].buffer = const_cast<char*>(lease_->hostname_.c_str());
+ bind_[11].buffer_length = lease_->hostname_.length();
+ // bind_[11].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
// Add the error flags
setErrorIndicators(bind_, error_, LEASE_COLUMNS);
// .. and check that we have the numbers correct at compile time.
- BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
+ BOOST_STATIC_ASSERT(11 < LEASE_COLUMNS);
// Add the data to the vector. Note the end element is one after the
// end of the array.
@@ -805,11 +915,34 @@ public:
// bind_[8].is_null = &MLM_FALSE; // commented out for performance
// reasons, see memset() above
+ // fqdn_fwd: boolean
+ bind_[9].buffer_type = MYSQL_TYPE_TINY;
+ bind_[9].buffer = reinterpret_cast<char*>(&fqdn_fwd_);
+ bind_[9].is_unsigned = MLM_TRUE;
+ // bind_[9].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // fqdn_rev: boolean
+ bind_[10].buffer_type = MYSQL_TYPE_TINY;
+ bind_[10].buffer = reinterpret_cast<char*>(&fqdn_rev_);
+ bind_[10].is_unsigned = MLM_TRUE;
+ // bind_[10].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
+ // hostname: varchar(255)
+ hostname_length_ = sizeof(hostname_buffer_);
+ bind_[11].buffer_type = MYSQL_TYPE_STRING;
+ bind_[11].buffer = reinterpret_cast<char*>(hostname_buffer_);
+ bind_[11].buffer_length = hostname_length_;
+ bind_[11].length = &hostname_length_;
+ // bind_[11].is_null = &MLM_FALSE; // commented out for performance
+ // reasons, see memset() above
+
// Add the error flags
setErrorIndicators(bind_, error_, LEASE_COLUMNS);
// .. and check that we have the numbers correct at compile time.
- BOOST_STATIC_ASSERT(8 < LEASE_COLUMNS);
+ BOOST_STATIC_ASSERT(11 < LEASE_COLUMNS);
// Add the data to the vector. Note the end element is one after the
// end of the array.
@@ -836,34 +969,41 @@ public:
// Set the lease type in a variable of the appropriate data type, which
// has been initialized with an arbitrary (but valid) value.
- Lease6::LeaseType type = Lease6::LEASE_IA_NA;
+ Lease::Type type = Lease::TYPE_NA;
switch (lease_type_) {
- case Lease6::LEASE_IA_NA:
- type = Lease6::LEASE_IA_NA;
+ case Lease::TYPE_NA:
+ type = Lease::TYPE_NA;
break;
- case Lease6::LEASE_IA_TA:
- type = Lease6::LEASE_IA_TA;
+ case Lease::TYPE_TA:
+ type = Lease::TYPE_TA;
break;
- case Lease6::LEASE_IA_PD:
- type = Lease6::LEASE_IA_PD;
+ case Lease::TYPE_PD:
+ type = Lease::TYPE_PD;
break;
default:
isc_throw(BadValue, "invalid lease type returned (" <<
- lease_type_ << ") for lease with address " <<
- address << ". Only 0, 1, or 2 are allowed.");
+ static_cast<int>(lease_type_) << ") for lease with "
+ << "address " << address << ". Only 0, 1, or 2 are "
+ << "allowed.");
}
// Set up DUID,
DuidPtr duid_ptr(new DUID(duid_buffer_, duid_length_));
+ // Hostname is passed to Lease6 as a string object, so we have to
+ // create it from the hostname buffer and length.
+ std::string hostname(hostname_buffer_,
+ hostname_buffer_ + hostname_length_);
+
// Create the lease and set the cltt (after converting from the
// expire time retrieved from the database).
Lease6Ptr result(new Lease6(type, addr, duid_ptr, iaid_,
pref_lifetime_, valid_lifetime_, 0, 0,
- subnet_id_, prefixlen_));
+ subnet_id_, fqdn_fwd_, fqdn_rev_,
+ hostname, prefixlen_));
time_t cltt = 0;
MySqlLeaseMgr::convertFromDatabaseTime(expire_, valid_lifetime_, cltt);
result->cltt_ = cltt;
@@ -907,6 +1047,14 @@ private:
uint32_t pref_lifetime_; ///< Preferred lifetime
uint32_t subnet_id_; ///< Subnet identification
uint32_t valid_lifetime_; ///< Lease time
+ my_bool fqdn_fwd_; ///< Has forward DNS update been
+ ///< performed
+ my_bool fqdn_rev_; ///< Has reverse DNS update been
+ ///< performed
+ char hostname_buffer_[HOSTNAME_MAX_LEN];
+ ///< Client hostname
+ unsigned long hostname_length_; ///< Client hostname length
+
};
@@ -1106,7 +1254,7 @@ MySqlLeaseMgr::openDatabase() {
mysql_error(mysql_));
}
- // Set SQL mode options for the connection: SQL mode governs how what
+ // Set SQL mode options for the connection: SQL mode governs how what
// constitutes insertable data for a given column, and how to handle
// invalid data. We want to ensure we get the strictest behavior and
// to reject invalid data with an error.
@@ -1227,7 +1375,8 @@ MySqlLeaseMgr::addLease(const Lease4Ptr& lease) {
bool
MySqlLeaseMgr::addLease(const Lease6Ptr& lease) {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
- DHCPSRV_MYSQL_ADD_ADDR6).arg(lease->addr_.toText());
+ DHCPSRV_MYSQL_ADD_ADDR6).arg(lease->addr_.toText())
+ .arg(lease->type_);
// Create the MYSQL_BIND array for the lease
std::vector<MYSQL_BIND> bind = exchange6_->createBindForSend(lease);
@@ -1473,6 +1622,15 @@ MySqlLeaseMgr::getLease4(const ClientId& clientid) const {
return (result);
}
+Lease4Ptr
+MySqlLeaseMgr::getLease4(const ClientId&, const HWAddr&, SubnetID) const {
+ /// This function is currently not implemented because allocation engine
+ /// searches for the lease using HW address or client identifier.
+ /// It never uses both parameters in the same time. We need to
+ /// consider if this function is needed at all.
+ isc_throw(NotImplemented, "The MySqlLeaseMgr::getLease4 function was"
+ " called, but it is not implemented");
+}
Lease4Ptr
MySqlLeaseMgr::getLease4(const ClientId& clientid, SubnetID subnet_id) const {
@@ -1504,12 +1662,14 @@ MySqlLeaseMgr::getLease4(const ClientId& clientid, SubnetID subnet_id) const {
Lease6Ptr
-MySqlLeaseMgr::getLease6(const isc::asiolink::IOAddress& addr) const {
+MySqlLeaseMgr::getLease6(Lease::Type lease_type,
+ const isc::asiolink::IOAddress& addr) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
- DHCPSRV_MYSQL_GET_ADDR6).arg(addr.toText());
+ DHCPSRV_MYSQL_GET_ADDR6).arg(addr.toText())
+ .arg(lease_type);
// Set up the WHERE clause value
- MYSQL_BIND inbind[1];
+ MYSQL_BIND inbind[2];
memset(inbind, 0, sizeof(inbind));
std::string addr6 = addr.toText();
@@ -1522,6 +1682,11 @@ MySqlLeaseMgr::getLease6(const isc::asiolink::IOAddress& addr) const {
inbind[0].buffer_length = addr6_length;
inbind[0].length = &addr6_length;
+ // LEASE_TYPE
+ inbind[1].buffer_type = MYSQL_TYPE_TINY;
+ inbind[1].buffer = reinterpret_cast<char*>(&lease_type);
+ inbind[1].is_unsigned = MLM_TRUE;
+
Lease6Ptr result;
getLease(GET_LEASE6_ADDR, inbind, result);
@@ -1530,12 +1695,14 @@ MySqlLeaseMgr::getLease6(const isc::asiolink::IOAddress& addr) const {
Lease6Collection
-MySqlLeaseMgr::getLease6(const DUID& duid, uint32_t iaid) const {
+MySqlLeaseMgr::getLeases6(Lease::Type lease_type,
+ const DUID& duid, uint32_t iaid) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
- DHCPSRV_MYSQL_GET_IAID_DUID).arg(iaid).arg(duid.toText());
+ DHCPSRV_MYSQL_GET_IAID_DUID).arg(iaid).arg(duid.toText())
+ .arg(lease_type);
// Set up the WHERE clause value
- MYSQL_BIND inbind[2];
+ MYSQL_BIND inbind[3];
memset(inbind, 0, sizeof(inbind));
// In the following statement, the DUID is being read. However, the
@@ -1563,6 +1730,11 @@ MySqlLeaseMgr::getLease6(const DUID& duid, uint32_t iaid) const {
inbind[1].buffer = reinterpret_cast<char*>(&iaid);
inbind[1].is_unsigned = MLM_TRUE;
+ // LEASE_TYPE
+ inbind[2].buffer_type = MYSQL_TYPE_TINY;
+ inbind[2].buffer = reinterpret_cast<char*>(&lease_type);
+ inbind[2].is_unsigned = MLM_TRUE;
+
// ... and get the data
Lease6Collection result;
getLeaseCollection(GET_LEASE6_DUID_IAID, inbind, result);
@@ -1570,16 +1742,17 @@ MySqlLeaseMgr::getLease6(const DUID& duid, uint32_t iaid) const {
return (result);
}
-
-Lease6Ptr
-MySqlLeaseMgr::getLease6(const DUID& duid, uint32_t iaid,
- SubnetID subnet_id) const {
+Lease6Collection
+MySqlLeaseMgr::getLeases6(Lease::Type lease_type,
+ const DUID& duid, uint32_t iaid,
+ SubnetID subnet_id) const {
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
DHCPSRV_MYSQL_GET_IAID_SUBID_DUID)
- .arg(iaid).arg(subnet_id).arg(duid.toText());
+ .arg(iaid).arg(subnet_id).arg(duid.toText())
+ .arg(lease_type);
// Set up the WHERE clause value
- MYSQL_BIND inbind[3];
+ MYSQL_BIND inbind[4];
memset(inbind, 0, sizeof(inbind));
// See the earlier description of the use of "const_cast" when accessing
@@ -1602,8 +1775,14 @@ MySqlLeaseMgr::getLease6(const DUID& duid, uint32_t iaid,
inbind[2].buffer = reinterpret_cast<char*>(&subnet_id);
inbind[2].is_unsigned = MLM_TRUE;
- Lease6Ptr result;
- getLease(GET_LEASE6_DUID_IAID_SUBID, inbind, result);
+ // LEASE_TYPE
+ inbind[3].buffer_type = MYSQL_TYPE_TINY;
+ inbind[3].buffer = reinterpret_cast<char*>(&lease_type);
+ inbind[3].is_unsigned = MLM_TRUE;
+
+ // ... and get the data
+ Lease6Collection result;
+ getLeaseCollection(GET_LEASE6_DUID_IAID_SUBID, inbind, result);
return (result);
}
@@ -1630,12 +1809,12 @@ MySqlLeaseMgr::updateLeaseCommon(StatementIndex stindex, MYSQL_BIND* bind,
int affected_rows = mysql_stmt_affected_rows(statements_[stindex]);
if (affected_rows == 0) {
isc_throw(NoSuchLease, "unable to update lease for address " <<
- lease->addr_.toText() << " as it does not exist");
+ lease->addr_ << " as it does not exist");
} else if (affected_rows > 1) {
// Should not happen - primary key constraint should only have selected
// one row.
isc_throw(DbOperationError, "apparently updated more than one lease "
- "that had the address " << lease->addr_.toText());
+ "that had the address " << lease->addr_);
}
}
@@ -1670,7 +1849,8 @@ MySqlLeaseMgr::updateLease6(const Lease6Ptr& lease) {
const StatementIndex stindex = UPDATE_LEASE6;
LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
- DHCPSRV_MYSQL_UPDATE_ADDR6).arg(lease->addr_.toText());
+ DHCPSRV_MYSQL_UPDATE_ADDR6).arg(lease->addr_.toText())
+ .arg(lease->type_);
// Create the MYSQL_BIND array for the data being updated
std::vector<MYSQL_BIND> bind = exchange6_->createBindForSend(lease);
diff --git a/src/lib/dhcpsrv/mysql_lease_mgr.h b/src/lib/dhcpsrv/mysql_lease_mgr.h
index 6d8eb8c..ef4dc47 100644
--- a/src/lib/dhcpsrv/mysql_lease_mgr.h
+++ b/src/lib/dhcpsrv/mysql_lease_mgr.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -220,6 +220,19 @@ public:
/// failed.
virtual Lease4Collection getLease4(const ClientId& clientid) const;
+ /// @brief Returns IPv4 lease for the specified client identifier, HW
+ /// address and subnet identifier.
+ ///
+ /// @param client_id A client identifier.
+ /// @param hwaddr Hardware address.
+ /// @param subnet_id A subnet identifier.
+ ///
+ /// @return A pointer to the lease or NULL if the lease is not found.
+ /// @throw isc::NotImplemented On every call as this function is currently
+ /// not implemented for the MySQL backend.
+ virtual Lease4Ptr getLease4(const ClientId& client_id, const HWAddr& hwaddr,
+ SubnetID subnet_id) const;
+
/// @brief Returns existing IPv4 lease for specified client-id
///
/// There can be at most one lease for a given HW address in a single
@@ -244,6 +257,7 @@ public:
/// The assumption here is that there will not be site or link-local
/// addresses used, so there is no way of having address duplication.
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param addr address of the searched lease
///
/// @return smart pointer to the lease (or NULL if a lease is not found)
@@ -255,7 +269,8 @@ public:
/// programming error.
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
- virtual Lease6Ptr getLease6(const isc::asiolink::IOAddress& addr) const;
+ virtual Lease6Ptr getLease6(Lease::Type type,
+ const isc::asiolink::IOAddress& addr) const;
/// @brief Returns existing IPv6 leases for a given DUID+IA combination
///
@@ -264,6 +279,7 @@ public:
/// can be more than one. Thus return type is a container, not a single
/// pointer.
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
///
@@ -276,16 +292,17 @@ public:
/// programming error.
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
- virtual Lease6Collection getLease6(const DUID& duid,
- uint32_t iaid) const;
+ virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid,
+ uint32_t iaid) const;
/// @brief Returns existing IPv6 lease for a given DUID+IA combination
///
+ /// @param type specifies lease type: (NA, TA or PD)
/// @param duid client DUID
/// @param iaid IA identifier
/// @param subnet_id subnet id of the subnet the lease belongs to
///
- /// @return smart pointer to the lease (or NULL if a lease is not found)
+ /// @return lease collection (may be empty if no lease is found)
///
/// @throw isc::BadValue record retrieved from database had an invalid
/// lease type field.
@@ -294,8 +311,8 @@ public:
/// programming error.
/// @throw isc::dhcp::DbOperationError An operation on the open database has
/// failed.
- virtual Lease6Ptr getLease6(const DUID& duid, uint32_t iaid,
- SubnetID subnet_id) const;
+ virtual Lease6Collection getLeases6(Lease::Type type, const DUID& duid,
+ uint32_t iaid, SubnetID subnet_id) const;
/// @brief Updates IPv4 lease.
///
diff --git a/src/lib/dhcpsrv/option_space_container.h b/src/lib/dhcpsrv/option_space_container.h
index ba16fbb..7d258d6 100644
--- a/src/lib/dhcpsrv/option_space_container.h
+++ b/src/lib/dhcpsrv/option_space_container.h
@@ -31,7 +31,8 @@ namespace dhcp {
/// @tparam ContainerType of the container holding items within
/// option space.
/// @tparam ItemType type of the item being held by the container.
-template<typename ContainerType, typename ItemType>
+/// @tparam Selector a string (for option spaces) or uint32_t (for vendor options)
+template<typename ContainerType, typename ItemType, typename Selector>
class OptionSpaceContainer {
public:
@@ -41,8 +42,8 @@ public:
/// @brief Adds a new item to the option_space.
///
/// @param item reference to the item being added.
- /// @param option_space name of the option space.
- void addItem(const ItemType& item, const std::string& option_space) {
+ /// @param option_space name or vendor-id of the option space
+ void addItem(const ItemType& item, const Selector& option_space) {
ItemsContainerPtr items = getItems(option_space);
items->push_back(item);
option_space_map_[option_space] = items;
@@ -54,10 +55,10 @@ public:
/// space an empty container is created and returned. However
/// this container is not added to the list of option spaces.
///
- /// @param option_space name of the option space.
+ /// @param option_space name or vendor-id of the option space.
///
/// @return pointer to the container holding items.
- ItemsContainerPtr getItems(const std::string& option_space) const {
+ ItemsContainerPtr getItems(const Selector& option_space) const {
const typename OptionSpaceMap::const_iterator& items =
option_space_map_.find(option_space);
if (items == option_space_map_.end()) {
@@ -73,8 +74,8 @@ public:
/// @todo This function is likely to be removed once
/// we create a structore of OptionSpaces defined
/// through the configuration manager.
- std::list<std::string> getOptionSpaceNames() {
- std::list<std::string> names;
+ std::list<Selector> getOptionSpaceNames() {
+ std::list<Selector> names;
for (typename OptionSpaceMap::const_iterator space =
option_space_map_.begin();
space != option_space_map_.end(); ++space) {
@@ -90,8 +91,8 @@ public:
private:
- /// A map holding container (option space name is the key).
- typedef std::map<std::string, ItemsContainerPtr> OptionSpaceMap;
+ /// A map holding container (option space name or vendor-id is the key).
+ typedef std::map<Selector, ItemsContainerPtr> OptionSpaceMap;
OptionSpaceMap option_space_map_;
};
diff --git a/src/lib/dhcpsrv/pool.cc b/src/lib/dhcpsrv/pool.cc
index 7104c61..d9c3da0 100644
--- a/src/lib/dhcpsrv/pool.cc
+++ b/src/lib/dhcpsrv/pool.cc
@@ -15,24 +15,33 @@
#include <asiolink/io_address.h>
#include <dhcpsrv/addr_utilities.h>
#include <dhcpsrv/pool.h>
+#include <sstream>
using namespace isc::asiolink;
namespace isc {
namespace dhcp {
-Pool::Pool(const isc::asiolink::IOAddress& first,
+Pool::Pool(Lease::Type type, const isc::asiolink::IOAddress& first,
const isc::asiolink::IOAddress& last)
- :id_(getNextID()), first_(first), last_(last) {
+ :id_(getNextID()), first_(first), last_(last), type_(type) {
}
bool Pool::inRange(const isc::asiolink::IOAddress& addr) const {
return (first_.smallerEqual(addr) && addr.smallerEqual(last_));
}
+std::string
+Pool::toText() const {
+ std::stringstream tmp;
+ tmp << "type=" << Lease::typeToText(type_) << ", " << first_
+ << "-" << last_;
+ return (tmp.str());
+}
+
Pool4::Pool4(const isc::asiolink::IOAddress& first,
const isc::asiolink::IOAddress& last)
- :Pool(first, last) {
+:Pool(Lease::TYPE_V4, first, last) {
// check if specified address boundaries are sane
if (!first.isV4() || !last.isV4()) {
isc_throw(BadValue, "Invalid Pool4 address boundaries: not IPv4");
@@ -43,9 +52,8 @@ Pool4::Pool4(const isc::asiolink::IOAddress& first,
}
}
-Pool4::Pool4(const isc::asiolink::IOAddress& prefix,
- uint8_t prefix_len)
- :Pool(prefix, IOAddress("0.0.0.0")) {
+Pool4::Pool4( const isc::asiolink::IOAddress& prefix, uint8_t prefix_len)
+:Pool(Lease::TYPE_V4, prefix, IOAddress("0.0.0.0")) {
// check if the prefix is sane
if (!prefix.isV4()) {
@@ -62,15 +70,21 @@ Pool4::Pool4(const isc::asiolink::IOAddress& prefix,
}
-Pool6::Pool6(Pool6Type type, const isc::asiolink::IOAddress& first,
+Pool6::Pool6(Lease::Type type, const isc::asiolink::IOAddress& first,
const isc::asiolink::IOAddress& last)
- :Pool(first, last), type_(type) {
+ :Pool(type, first, last), prefix_len_(128) {
// check if specified address boundaries are sane
if (!first.isV6() || !last.isV6()) {
isc_throw(BadValue, "Invalid Pool6 address boundaries: not IPv6");
}
+ if ( (type != Lease::TYPE_NA) && (type != Lease::TYPE_TA) &&
+ (type != Lease::TYPE_PD)) {
+ isc_throw(BadValue, "Invalid Pool6 type: " << static_cast<int>(type)
+ << ", must be TYPE_IA, TYPE_TA or TYPE_PD");
+ }
+
if (last < first) {
isc_throw(BadValue, "Upper boundary is smaller than lower boundary.");
// This check is a bit strict. If we decide that it is too strict,
@@ -87,24 +101,36 @@ Pool6::Pool6(Pool6Type type, const isc::asiolink::IOAddress& first,
// TYPE_PD is not supported by this constructor. first-last style
// parameters are for IA and TA only. There is another dedicated
// constructor for that (it uses prefix/length)
- if ((type != TYPE_IA) && (type != TYPE_TA)) {
- isc_throw(BadValue, "Invalid Pool6 type specified");
+ if ((type != Lease::TYPE_NA) && (type != Lease::TYPE_TA)) {
+ isc_throw(BadValue, "Invalid Pool6 type specified:"
+ << static_cast<int>(type));
}
}
-Pool6::Pool6(Pool6Type type, const isc::asiolink::IOAddress& prefix,
- uint8_t prefix_len)
- :Pool(prefix, IOAddress("::")),
- type_(type) {
+Pool6::Pool6(Lease::Type type, const isc::asiolink::IOAddress& prefix,
+ uint8_t prefix_len, uint8_t delegated_len /* = 128 */)
+ :Pool(type, prefix, IOAddress("::")), prefix_len_(delegated_len) {
// check if the prefix is sane
if (!prefix.isV6()) {
isc_throw(BadValue, "Invalid Pool6 address boundaries: not IPv6");
}
- // check if the prefix length is sane
+ // check if the prefix length is sane
if (prefix_len == 0 || prefix_len > 128) {
- isc_throw(BadValue, "Invalid prefix length");
+ isc_throw(BadValue, "Invalid prefix length: " << prefix_len);
+ }
+
+ if (prefix_len > delegated_len) {
+ isc_throw(BadValue, "Delegated length (" << static_cast<int>(delegated_len)
+ << ") must be longer than prefix length ("
+ << static_cast<int>(prefix_len) << ")");
+ }
+
+ if ( ( (type == Lease::TYPE_NA) || (type == Lease::TYPE_TA)) &&
+ (delegated_len != 128)) {
+ isc_throw(BadValue, "For IA or TA pools, delegated prefix length must "
+ << " be 128.");
}
/// @todo: We should probably implement checks against weird addresses
@@ -114,5 +140,15 @@ Pool6::Pool6(Pool6Type type, const isc::asiolink::IOAddress& prefix,
last_ = lastAddrInPrefix(prefix, prefix_len);
}
+std::string
+Pool6::toText() const {
+ std::stringstream tmp;
+ tmp << "type=" << Lease::typeToText(type_) << ", " << first_
+ << "-" << last_ << ", delegated_len="
+ << static_cast<int>(prefix_len_);
+ return (tmp.str());
+}
+
+
}; // end of isc::dhcp namespace
}; // end of isc namespace
diff --git a/src/lib/dhcpsrv/pool.h b/src/lib/dhcpsrv/pool.h
index e0a6f3c..ae8cb58 100644
--- a/src/lib/dhcpsrv/pool.h
+++ b/src/lib/dhcpsrv/pool.h
@@ -16,8 +16,8 @@
#define POOL_H
#include <asiolink/io_address.h>
-
#include <boost/shared_ptr.hpp>
+#include <dhcpsrv/lease.h>
#include <vector>
@@ -31,6 +31,8 @@ namespace dhcp {
class Pool {
public:
+ /// @note:
+ /// PoolType enum was removed. Please use Lease::Type instead
/// @brief returns Pool-id
///
@@ -58,6 +60,25 @@ public:
/// @return true, if the address is in pool
bool inRange(const isc::asiolink::IOAddress& addr) const;
+ /// @brief Returns pool type (v4, v6 non-temporary, v6 temp, v6 prefix)
+ /// @return returns pool type
+ Lease::Type getType() const {
+ return (type_);
+ }
+
+ /// @brief returns textual representation of the pool
+ ///
+ /// @return textual representation
+ virtual std::string toText() const;
+
+ /// @brief virtual destructor
+ ///
+ /// We need Pool to be a polymorphic class, so we could dynamic cast
+ /// from PoolPtr to Pool6Ptr if we need to. A class becomes polymorphic,
+ /// when there is at least one virtual method.
+ virtual ~Pool() {
+ }
+
protected:
/// @brief protected constructor
@@ -65,7 +86,12 @@ protected:
/// This constructor is protected to prevent anyone from instantiating
/// Pool class directly. Instances of Pool4 and Pool6 should be created
/// instead.
- Pool(const isc::asiolink::IOAddress& first,
+ ///
+ /// @param type type of lease that will be served from this pool
+ /// @param first first address of a range
+ /// @param last last address of a range
+ Pool(Lease::Type type,
+ const isc::asiolink::IOAddress& first,
const isc::asiolink::IOAddress& last);
/// @brief returns the next unique Pool-ID
@@ -91,6 +117,9 @@ protected:
///
/// @todo: This field is currently not used.
std::string comments_;
+
+ /// @brief defines a lease type that will be served from this pool
+ Lease::Type type_;
};
/// @brief Pool information for IPv4 addresses
@@ -117,9 +146,6 @@ public:
/// @brief a pointer an IPv4 Pool
typedef boost::shared_ptr<Pool4> Pool4Ptr;
-/// @brief a container for IPv4 Pools
-typedef std::vector<Pool4Ptr> Pool4Collection;
-
/// @brief Pool information for IPv6 addresses and prefixes
///
/// It holds information about pool6, i.e. a range of IPv6 address space that
@@ -127,55 +153,75 @@ typedef std::vector<Pool4Ptr> Pool4Collection;
class Pool6 : public Pool {
public:
- /// @brief specifies Pool type
- ///
- /// Currently there are 3 pool types defined in DHCPv6:
- /// - Non-temporary addresses (conveyed in IA_NA)
- /// - Temporary addresses (conveyed in IA_TA)
- /// - Delegated Prefixes (conveyed in IA_PD)
- /// There is a new one being worked on (IA_PA, see draft-ietf-dhc-host-gen-id), but
- /// support for it is not planned for now.
- typedef enum {
- TYPE_IA,
- TYPE_TA,
- TYPE_PD
- } Pool6Type;
-
/// @brief the constructor for Pool6 "min-max" style definition
///
- /// @param type type of the pool (IA, TA or PD)
+ /// @throw BadValue if PD is define (PD can be only prefix/len)
+ ///
+ /// @param type type of the pool (IA or TA)
/// @param first the first address in a pool
/// @param last the last address in a pool
- Pool6(Pool6Type type, const isc::asiolink::IOAddress& first,
+ Pool6(Lease::Type type, const isc::asiolink::IOAddress& first,
const isc::asiolink::IOAddress& last);
/// @brief the constructor for Pool6 "prefix/len" style definition
///
+ /// For addressed, this is just a prefix/len definition. For prefixes,
+ /// there is one extra additional parameter delegated_len. It specifies
+ /// a size of delegated prefixes that the pool will be split into. For
+ /// example pool 2001:db8::/56, delegated_len=64 means that there is a
+ /// pool 2001:db8::/56. It will be split into 256 prefixes of length /64,
+ /// e.g. 2001:db8:0:1::/64, 2001:db8:0:2::/64 etc.
+ ///
+ /// Naming convention:
+ /// A smaller prefix length yields a shorter prefix which describes a larger
+ /// set of addresses. A larger length yields a longer prefix which describes
+ /// a smaller set of addresses.
+ ///
+ /// Obviously, prefix_len must define shorter or equal prefix length than
+ /// delegated_len, so prefix_len <= delegated_len. Note that it is slightly
+ /// confusing: bigger (larger) prefix actually has smaller prefix length,
+ /// e.g. /56 is a bigger prefix than /64, but has shorter (smaller) prefix
+ /// length.
+ ///
+ /// @throw BadValue if delegated_len is defined for non-PD types or
+ /// when delegated_len < prefix_len
+ ///
/// @param type type of the pool (IA, TA or PD)
/// @param prefix specifies prefix of the pool
- /// @param prefix_len specifies length of the prefix of the pool
- Pool6(Pool6Type type, const isc::asiolink::IOAddress& prefix,
- uint8_t prefix_len);
+ /// @param prefix_len specifies prefix length of the pool
+ /// @param delegated_len specifies lenght of the delegated prefixes
+ Pool6(Lease::Type type, const isc::asiolink::IOAddress& prefix,
+ uint8_t prefix_len, uint8_t delegated_len = 128);
/// @brief returns pool type
///
/// @return pool type
- Pool6Type getType() const {
+ Lease::Type getType() const {
return (type_);
}
-private:
- /// @brief defines a pool type
- Pool6Type type_;
+ /// @brief returns delegated prefix length
+ ///
+ /// This may be useful for "prefix/len" style definition for
+ /// addresses, but is mostly useful for prefix pools.
+ /// @return prefix length (1-128)
+ uint8_t getLength() {
+ return (prefix_len_);
+ }
+ /// @brief returns textual representation of the pool
+ ///
+ /// @return textual representation
+ virtual std::string toText() const;
+
+private:
+ /// @brief Defines prefix length (for TYPE_PD only)
+ uint8_t prefix_len_;
};
/// @brief a pointer an IPv6 Pool
typedef boost::shared_ptr<Pool6> Pool6Ptr;
-/// @brief a container for IPv6 Pools
-typedef std::vector<Pool6Ptr> Pool6Collection;
-
/// @brief a pointer to either IPv4 or IPv6 Pool
typedef boost::shared_ptr<Pool> PoolPtr;
diff --git a/src/lib/dhcpsrv/subnet.cc b/src/lib/dhcpsrv/subnet.cc
index daf3f9e..0134d8c 100644
--- a/src/lib/dhcpsrv/subnet.cc
+++ b/src/lib/dhcpsrv/subnet.cc
@@ -24,20 +24,27 @@ using namespace isc::asiolink;
namespace isc {
namespace dhcp {
+// This is an initial value of subnet-id. See comments in subnet.h for details.
+SubnetID Subnet::static_id_ = 1;
+
Subnet::Subnet(const isc::asiolink::IOAddress& prefix, uint8_t len,
const Triplet<uint32_t>& t1,
const Triplet<uint32_t>& t2,
const Triplet<uint32_t>& valid_lifetime)
- :id_(getNextID()), prefix_(prefix), prefix_len_(len), t1_(t1),
+ :id_(generateNextID()), prefix_(prefix), prefix_len_(len), t1_(t1),
t2_(t2), valid_(valid_lifetime),
- last_allocated_(lastAddrInPrefix(prefix, len)) {
+ last_allocated_ia_(lastAddrInPrefix(prefix, len)),
+ last_allocated_ta_(lastAddrInPrefix(prefix, len)),
+ last_allocated_pd_(lastAddrInPrefix(prefix, len)) {
if ((prefix.isV6() && len > 128) ||
(prefix.isV4() && len > 32)) {
- isc_throw(BadValue, "Invalid prefix length specified for subnet: " << len);
+ isc_throw(BadValue,
+ "Invalid prefix length specified for subnet: " << len);
}
}
-bool Subnet::inRange(const isc::asiolink::IOAddress& addr) const {
+bool
+Subnet::inRange(const isc::asiolink::IOAddress& addr) const {
IOAddress first = firstAddrInPrefix(prefix_, prefix_len_);
IOAddress last = lastAddrInPrefix(prefix_, prefix_len_);
@@ -84,50 +91,168 @@ Subnet::getOptionDescriptor(const std::string& option_space,
return (*range.first);
}
-std::string Subnet::toText() const {
+void Subnet::addVendorOption(const OptionPtr& option, bool persistent,
+ uint32_t vendor_id){
+
+ validateOption(option);
+
+ vendor_option_spaces_.addItem(OptionDescriptor(option, persistent), vendor_id);
+}
+
+Subnet::OptionContainerPtr
+Subnet::getVendorOptionDescriptors(uint32_t vendor_id) const {
+ return (vendor_option_spaces_.getItems(vendor_id));
+}
+
+Subnet::OptionDescriptor
+Subnet::getVendorOptionDescriptor(uint32_t vendor_id, uint16_t option_code) {
+ OptionContainerPtr options = getVendorOptionDescriptors(vendor_id);
+ if (!options || options->empty()) {
+ return (OptionDescriptor(false));
+ }
+ const OptionContainerTypeIndex& idx = options->get<1>();
+ const OptionContainerTypeRange& range = idx.equal_range(option_code);
+ if (std::distance(range.first, range.second) == 0) {
+ return (OptionDescriptor(false));
+ }
+
+ return (*range.first);
+}
+
+void Subnet::delVendorOptions() {
+ vendor_option_spaces_.clearItems();
+}
+
+isc::asiolink::IOAddress Subnet::getLastAllocated(Lease::Type type) const {
+ // check if the type is valid (and throw if it isn't)
+ checkType(type);
+
+ switch (type) {
+ case Lease::TYPE_V4:
+ case Lease::TYPE_NA:
+ return last_allocated_ia_;
+ case Lease::TYPE_TA:
+ return last_allocated_ta_;
+ case Lease::TYPE_PD:
+ return last_allocated_pd_;
+ default:
+ isc_throw(BadValue, "Pool type " << type << " not supported");
+ }
+}
+
+void Subnet::setLastAllocated(Lease::Type type,
+ const isc::asiolink::IOAddress& addr) {
+
+ // check if the type is valid (and throw if it isn't)
+ checkType(type);
+
+ switch (type) {
+ case Lease::TYPE_V4:
+ case Lease::TYPE_NA:
+ last_allocated_ia_ = addr;
+ return;
+ case Lease::TYPE_TA:
+ last_allocated_ta_ = addr;
+ return;
+ case Lease::TYPE_PD:
+ last_allocated_pd_ = addr;
+ return;
+ default:
+ isc_throw(BadValue, "Pool type " << type << " not supported");
+ }
+}
+
+std::string
+Subnet::toText() const {
std::stringstream tmp;
- tmp << prefix_.toText() << "/" << static_cast<unsigned int>(prefix_len_);
+ tmp << prefix_ << "/" << static_cast<unsigned int>(prefix_len_);
return (tmp.str());
}
+void Subnet4::checkType(Lease::Type type) const {
+ if (type != Lease::TYPE_V4) {
+ isc_throw(BadValue, "Only TYPE_V4 is allowed for Subnet4");
+ }
+}
+
Subnet4::Subnet4(const isc::asiolink::IOAddress& prefix, uint8_t length,
const Triplet<uint32_t>& t1,
const Triplet<uint32_t>& t2,
const Triplet<uint32_t>& valid_lifetime)
- :Subnet(prefix, length, t1, t2, valid_lifetime) {
+ :Subnet(prefix, length, t1, t2, valid_lifetime),
+ siaddr_(IOAddress("0.0.0.0")) {
if (!prefix.isV4()) {
isc_throw(BadValue, "Non IPv4 prefix " << prefix.toText()
<< " specified in subnet4");
}
}
-void Subnet::addPool(const PoolPtr& pool) {
- IOAddress first_addr = pool->getFirstAddress();
- IOAddress last_addr = pool->getLastAddress();
-
- if (!inRange(first_addr) || !inRange(last_addr)) {
- isc_throw(BadValue, "Pool (" << first_addr.toText() << "-" << last_addr.toText()
- << " does not belong in this (" << prefix_.toText() << "/"
- << static_cast<int>(prefix_len_) << ") subnet4");
+void Subnet4::setSiaddr(const isc::asiolink::IOAddress& siaddr) {
+ if (!siaddr.isV4()) {
+ isc_throw(BadValue, "Can't set siaddr to non-IPv4 address "
+ << siaddr);
}
+ siaddr_ = siaddr;
+}
- /// @todo: Check that pools do not overlap
+isc::asiolink::IOAddress Subnet4::getSiaddr() const {
+ return (siaddr_);
+}
- pools_.push_back(pool);
+const PoolCollection& Subnet::getPools(Lease::Type type) const {
+ // check if the type is valid (and throw if it isn't)
+ checkType(type);
+
+ switch (type) {
+ case Lease::TYPE_V4:
+ case Lease::TYPE_NA:
+ return (pools_);
+ case Lease::TYPE_TA:
+ return (pools_ta_);
+ case Lease::TYPE_PD:
+ return (pools_pd_);
+ default:
+ isc_throw(BadValue, "Unsupported pool type: "
+ << static_cast<int>(type));
+ }
+}
+
+PoolCollection& Subnet::getPoolsWritable(Lease::Type type) {
+ // check if the type is valid (and throw if it isn't)
+ checkType(type);
+
+ switch (type) {
+ case Lease::TYPE_V4:
+ case Lease::TYPE_NA:
+ return (pools_);
+ case Lease::TYPE_TA:
+ return (pools_ta_);
+ case Lease::TYPE_PD:
+ return (pools_pd_);
+ default:
+ isc_throw(BadValue, "Invalid pool type specified: "
+ << static_cast<int>(type));
+ }
}
-PoolPtr Subnet::getPool(isc::asiolink::IOAddress hint) {
+const PoolPtr Subnet::getPool(Lease::Type type, const isc::asiolink::IOAddress& hint,
+ bool anypool /* true */) const {
+ // check if the type is valid (and throw if it isn't)
+ checkType(type);
+
+ const PoolCollection& pools = getPools(type);
PoolPtr candidate;
- for (PoolCollection::iterator pool = pools_.begin(); pool != pools_.end(); ++pool) {
+ for (PoolCollection::const_iterator pool = pools.begin();
+ pool != pools.end(); ++pool) {
// if we won't find anything better, then let's just use the first pool
- if (!candidate) {
+ if (anypool && !candidate) {
candidate = *pool;
}
- // if the client provided a pool and there's a pool that hint is valid in,
- // then let's use that pool
+ // if the client provided a pool and there's a pool that hint is valid
+ // in, then let's use that pool
if ((*pool)->inRange(hint)) {
return (*pool);
}
@@ -135,29 +260,69 @@ PoolPtr Subnet::getPool(isc::asiolink::IOAddress hint) {
return (candidate);
}
+void
+Subnet::addPool(const PoolPtr& pool) {
+ IOAddress first_addr = pool->getFirstAddress();
+ IOAddress last_addr = pool->getLastAddress();
+
+ if (!inRange(first_addr) || !inRange(last_addr)) {
+ isc_throw(BadValue, "Pool (" << first_addr << "-" << last_addr
+ << " does not belong in this (" << prefix_ << "/"
+ << static_cast<int>(prefix_len_) << ") subnet");
+ }
+
+ /// @todo: Check that pools do not overlap
+
+ // check if the type is valid (and throw if it isn't)
+ checkType(pool->getType());
+
+ // Add the pool to the appropriate pools collection
+ getPoolsWritable(pool->getType()).push_back(pool);
+}
+
+void
+Subnet::delPools(Lease::Type type) {
+ getPoolsWritable(type).clear();
+}
+
+void
+Subnet::setIface(const std::string& iface_name) {
+ iface_ = iface_name;
+}
+
+std::string
+Subnet::getIface() const {
+ return (iface_);
+}
void
Subnet4::validateOption(const OptionPtr& option) const {
if (!option) {
- isc_throw(isc::BadValue, "option configured for subnet must not be NULL");
+ isc_throw(isc::BadValue,
+ "option configured for subnet must not be NULL");
} else if (option->getUniverse() != Option::V4) {
- isc_throw(isc::BadValue, "expected V4 option to be added to the subnet");
+ isc_throw(isc::BadValue,
+ "expected V4 option to be added to the subnet");
}
}
-bool Subnet::inPool(const isc::asiolink::IOAddress& addr) const {
+bool
+Subnet::inPool(Lease::Type type, const isc::asiolink::IOAddress& addr) const {
// Let's start with checking if it even belongs to that subnet.
if (!inRange(addr)) {
return (false);
}
- for (PoolCollection::const_iterator pool = pools_.begin(); pool != pools_.end(); ++pool) {
+ const PoolCollection& pools = getPools(type);
+
+ for (PoolCollection::const_iterator pool = pools.begin();
+ pool != pools.end(); ++pool) {
if ((*pool)->inRange(addr)) {
return (true);
}
}
- // there's no pool that address belongs to
+ // There's no pool that address belongs to
return (false);
}
@@ -169,29 +334,30 @@ Subnet6::Subnet6(const isc::asiolink::IOAddress& prefix, uint8_t length,
:Subnet(prefix, length, t1, t2, valid_lifetime),
preferred_(preferred_lifetime){
if (!prefix.isV6()) {
- isc_throw(BadValue, "Non IPv6 prefix " << prefix.toText()
+ isc_throw(BadValue, "Non IPv6 prefix " << prefix
<< " specified in subnet6");
}
}
+void Subnet6::checkType(Lease::Type type) const {
+ if ( (type != Lease::TYPE_NA) && (type != Lease::TYPE_TA) &&
+ (type != Lease::TYPE_PD)) {
+ isc_throw(BadValue, "Invalid Pool type: " << Lease::typeToText(type)
+ << "(" << static_cast<int>(type)
+ << "), must be TYPE_NA, TYPE_TA or TYPE_PD for Subnet6");
+ }
+}
+
void
Subnet6::validateOption(const OptionPtr& option) const {
if (!option) {
- isc_throw(isc::BadValue, "option configured for subnet must not be NULL");
+ isc_throw(isc::BadValue,
+ "option configured for subnet must not be NULL");
} else if (option->getUniverse() != Option::V6) {
- isc_throw(isc::BadValue, "expected V6 option to be added to the subnet");
+ isc_throw(isc::BadValue,
+ "expected V6 option to be added to the subnet");
}
}
-
-void Subnet6::setIface(const std::string& iface_name) {
- iface_ = iface_name;
-}
-
-std::string Subnet6::getIface() const {
- return (iface_);
-}
-
-
} // end of isc::dhcp namespace
} // end of isc namespace
diff --git a/src/lib/dhcpsrv/subnet.h b/src/lib/dhcpsrv/subnet.h
index 4c7cbcc..ecac6c3 100644
--- a/src/lib/dhcpsrv/subnet.h
+++ b/src/lib/dhcpsrv/subnet.h
@@ -28,6 +28,7 @@
#include <dhcpsrv/option_space_container.h>
#include <dhcpsrv/pool.h>
#include <dhcpsrv/triplet.h>
+#include <dhcpsrv/lease.h>
namespace isc {
namespace dhcp {
@@ -46,7 +47,6 @@ namespace dhcp {
///
/// @todo: Implement support for options here
-
/// @brief Unique identifier for a subnet (both v4 and v6)
typedef uint32_t SubnetID;
@@ -180,9 +180,22 @@ public:
void addOption(const OptionPtr& option, bool persistent,
const std::string& option_space);
+
+ /// @brief Adds new vendor option instance to the collection.
+ ///
+ /// @param option option instance.
+ /// @param persistent if true, send an option regardless if client
+ /// requested it or not.
+ /// @param vendor_id enterprise id of the vendor space to add an option to.
+ void addVendorOption(const OptionPtr& option, bool persistent,
+ uint32_t vendor_id);
+
/// @brief Delete all options configured for the subnet.
void delOptions();
+ /// @brief Deletes all vendor options configured for the subnet.
+ void delVendorOptions();
+
/// @brief checks if the specified address is in pools
///
/// Note the difference between inSubnet() and inPool(). For a given
@@ -192,22 +205,23 @@ public:
/// is not always true. For the given example, 2001::1234:abcd would return
/// true for inSubnet(), but false for inPool() check.
///
+ /// @param type type of pools to iterate over
/// @param addr this address will be checked if it belongs to any pools in
/// that subnet
/// @return true if the address is in any of the pools
- bool inPool(const isc::asiolink::IOAddress& addr) const;
+ bool inPool(Lease::Type type, const isc::asiolink::IOAddress& addr) const;
- /// @brief return valid-lifetime for addresses in that prefix
+ /// @brief Return valid-lifetime for addresses in that prefix
Triplet<uint32_t> getValid() const {
return (valid_);
}
- /// @brief returns T1 (renew timer), expressed in seconds
+ /// @brief Returns T1 (renew timer), expressed in seconds
Triplet<uint32_t> getT1() const {
return (t1_);
}
- /// @brief returns T2 (rebind timer), expressed in seconds
+ /// @brief Returns T2 (rebind timer), expressed in seconds
Triplet<uint32_t> getT2() const {
return (t2_);
}
@@ -220,6 +234,14 @@ public:
OptionContainerPtr
getOptionDescriptors(const std::string& option_space) const;
+ /// @brief Return a collection of vendor option descriptors.
+ ///
+ /// @param vendor_id enterprise id of the option space.
+ ///
+ /// @return pointer to collection of options configured for a subnet.
+ OptionContainerPtr
+ getVendorOptionDescriptors(uint32_t vendor_id) const;
+
/// @brief Return single option descriptor.
///
/// @param option_space name of the option space.
@@ -231,6 +253,16 @@ public:
getOptionDescriptor(const std::string& option_space,
const uint16_t option_code);
+ /// @brief Return single vendor option descriptor.
+ ///
+ /// @param vendor_id enterprise id of the option space.
+ /// @param option_code code of the option to be returned.
+ ///
+ /// @return option descriptor found for the specified option space
+ /// and option code.
+ OptionDescriptor
+ getVendorOptionDescriptor(uint32_t vendor_id, uint16_t option_code);
+
/// @brief returns the last address that was tried from this pool
///
/// This method returns the last address that was attempted to be allocated
@@ -240,10 +272,9 @@ public:
/// @todo: Define map<SubnetID, IOAddress> somewhere in the
/// AllocEngine::IterativeAllocator and keep the data there
///
- /// @return address that was last tried from this pool
- isc::asiolink::IOAddress getLastAllocated() const {
- return (last_allocated_);
- }
+ /// @param type lease type to be returned
+ /// @return address/prefix that was last tried from this pool
+ isc::asiolink::IOAddress getLastAllocated(Lease::Type type) const;
/// @brief sets the last address that was tried from this pool
///
@@ -253,15 +284,16 @@ public:
///
/// @todo: Define map<SubnetID, IOAddress> somewhere in the
/// AllocEngine::IterativeAllocator and keep the data there
- void setLastAllocated(const isc::asiolink::IOAddress& addr) {
- last_allocated_ = addr;
- }
+ /// @param addr address/prefix to that was tried last
+ /// @param type lease type to be set
+ void setLastAllocated(Lease::Type type,
+ const isc::asiolink::IOAddress& addr);
- /// @brief returns unique ID for that subnet
+ /// @brief Returns unique ID for that subnet
/// @return unique ID for that subnet
SubnetID getID() const { return (id_); }
- /// @brief returns subnet parameters (prefix and prefix length)
+ /// @brief Returns subnet parameters (prefix and prefix length)
///
/// @return (prefix, prefix length) pair
std::pair<isc::asiolink::IOAddress, uint8_t> get() const {
@@ -272,16 +304,36 @@ public:
/// @param pool pool to be added
void addPool(const PoolPtr& pool);
+
+ /// @brief Deletes all pools of specified type
+ ///
+ /// This method is used for testing purposes only
+ /// @param type type of pools to be deleted
+ void delPools(Lease::Type type);
+
/// @brief Returns a pool that specified address belongs to
///
+ /// If there is no pool that the address belongs to (hint is invalid), other
+ /// pool of specified type will be returned.
+ ///
+ /// With anypool set to true, this is means give me a pool, preferably
+ /// the one that addr belongs to. With anypool set to false, it means
+ /// give me a pool that addr belongs to (or NULL if here is no such pool)
+ ///
+ /// @param type pool type that the pool is looked for
/// @param addr address that the returned pool should cover (optional)
- /// @return Pointer to found Pool4 or Pool6 (or NULL)
- PoolPtr getPool(isc::asiolink::IOAddress addr);
+ /// @param anypool other pool may be returned as well, not only the one
+ /// that addr belongs to
+ /// @return found pool (or NULL)
+ const PoolPtr getPool(Lease::Type type, const isc::asiolink::IOAddress& addr,
+ bool anypool = true) const;
/// @brief Returns a pool without any address specified
+ ///
+ /// @param type pool type that the pool is looked for
/// @return returns one of the pools defined
- PoolPtr getPool() {
- return (getPool(default_pool()));
+ PoolPtr getAnyPool(Lease::Type type) {
+ return (getPool(type, default_pool()));
}
/// @brief Returns the default address that will be used for pool selection
@@ -290,25 +342,57 @@ public:
/// and 0.0.0.0 for Subnet4)
virtual isc::asiolink::IOAddress default_pool() const = 0;
- /// @brief returns all pools
+ /// @brief Returns all pools (const variant)
///
/// The reference is only valid as long as the object that returned it.
///
+ /// @param type lease type to be set
/// @return a collection of all pools
- const PoolCollection& getPools() const {
- return pools_;
- }
+ const PoolCollection& getPools(Lease::Type type) const;
+
+ /// @brief Sets name of the network interface for directly attached networks
+ ///
+ /// @param iface_name name of the interface
+ void setIface(const std::string& iface_name);
+
+ /// @brief Network interface name used to reach subnet (or "" for remote
+ /// subnets)
+ /// @return network interface name for directly attached subnets or ""
+ std::string getIface() const;
- /// @brief returns textual representation of the subnet (e.g. "2001:db8::/64")
+ /// @brief Returns textual representation of the subnet (e.g.
+ /// "2001:db8::/64")
///
/// @return textual representation
virtual std::string toText() const;
+ /// @brief Resets subnet-id counter to its initial value (1)
+ ///
+ /// This should be called during reconfiguration, before any new
+ /// subnet objects are created. It will ensure that the subnet_id will
+ /// be consistent between reconfigures.
+ static void resetSubnetID() {
+ static_id_ = 1;
+ }
+
protected:
- /// @brief protected constructor
+ /// @brief Returns all pools (non-const variant)
+ ///
+ /// The reference is only valid as long as the object that returned it.
+ ///
+ /// @param type lease type to be set
+ /// @return a collection of all pools
+ PoolCollection& getPoolsWritable(Lease::Type type);
+
+ /// @brief Protected constructor
//
/// By making the constructor protected, we make sure that noone will
- /// ever instantiate that class. Pool4 and Pool6 should be used instead.
+ /// ever instantiate that class. Subnet4 and Subnet6 should be used instead.
+ ///
+ /// This constructor assigns a new subnet-id (see @ref generateNextID).
+ /// This subnet-id has unique value that is strictly monotonously increasing
+ /// for each subnet, until it is explicitly reset back to 1 during
+ /// reconfiguration process.
Subnet(const isc::asiolink::IOAddress& prefix, uint8_t len,
const Triplet<uint32_t>& t1,
const Triplet<uint32_t>& t2,
@@ -320,14 +404,36 @@ protected:
/// derive from this class.
virtual ~Subnet() { };
+ /// @brief keeps the subnet-id value
+ ///
+ /// It is inreased every time a new Subnet object is created.
+ /// It is reset (@ref resetSubnetId) every time reconfiguration occurs.
+ ///
+ /// Static value initialized in subnet.cc.
+ static SubnetID static_id_;
+
/// @brief returns the next unique Subnet-ID
///
+ /// This method generates and returns the next unique subnet-id.
+ /// It is a strictly monotonously increasing value (1,2,3,...) for
+ /// each new Subnet object created. It can be explicitly reset
+ /// back to 1 during reconfiguration (@ref resetSubnetID).
+ ///
/// @return the next unique Subnet-ID
- static SubnetID getNextID() {
- static SubnetID id = 0;
- return (id++);
+ static SubnetID generateNextID() {
+ return (static_id_++);
}
+ /// @brief Checks if used pool type is valid
+ ///
+ /// Allowed type for Subnet4 is Pool::TYPE_V4.
+ /// Allowed types for Subnet6 are Pool::TYPE_{IA,TA,PD}.
+ /// This method is implemented in derived classes.
+ ///
+ /// @param type type to be checked
+ /// @throw BadValue if invalid value is used
+ virtual void checkType(Lease::Type type) const = 0;
+
/// @brief Check if option is valid and can be added to a subnet.
///
/// @param option option to be validated.
@@ -339,9 +445,15 @@ protected:
/// a Subnet4 or Subnet6.
SubnetID id_;
- /// @brief collection of pools in that list
+ /// @brief collection of IPv4 or non-temporary IPv6 pools in that subnet
PoolCollection pools_;
+ /// @brief collection of IPv6 temporary address pools in that subnet
+ PoolCollection pools_ta_;
+
+ /// @brief collection of IPv6 prefix pools in that subnet
+ PoolCollection pools_pd_;
+
/// @brief a prefix of the subnet
isc::asiolink::IOAddress prefix_;
@@ -366,7 +478,17 @@ protected:
/// removing a pool, restarting or changing allocation algorithms. For
/// that purpose it should be only considered a help that should not be
/// fully trusted.
- isc::asiolink::IOAddress last_allocated_;
+ isc::asiolink::IOAddress last_allocated_ia_;
+
+ /// @brief last allocated temporary address
+ ///
+ /// See @ref last_allocated_ia_ for details.
+ isc::asiolink::IOAddress last_allocated_ta_;
+
+ /// @brief last allocated IPv6 prefix
+ ///
+ /// See @ref last_allocated_ia_ for details.
+ isc::asiolink::IOAddress last_allocated_pd_;
/// @brief Name of the network interface (if connected directly)
std::string iface_;
@@ -375,9 +497,17 @@ private:
/// A collection of option spaces grouping option descriptors.
typedef OptionSpaceContainer<OptionContainer,
- OptionDescriptor> OptionSpaceCollection;
+ OptionDescriptor, std::string> OptionSpaceCollection;
+
+ /// A collection of vendor space option descriptors.
+ typedef OptionSpaceContainer<OptionContainer,
+ OptionDescriptor, uint32_t> VendorOptionSpaceCollection;
+
+ /// Regular options are kept here
OptionSpaceCollection option_spaces_;
+ /// Vendor options are kept here
+ VendorOptionSpaceCollection vendor_option_spaces_;
};
/// @brief A generic pointer to either Subnet4 or Subnet6 object
@@ -391,6 +521,8 @@ public:
/// @brief Constructor with all parameters
///
+ /// This constructor calls Subnet::Subnet, where subnet-id is generated.
+ ///
/// @param prefix Subnet4 prefix
/// @param length prefix length
/// @param t1 renewal timer (in seconds)
@@ -401,6 +533,18 @@ public:
const Triplet<uint32_t>& t2,
const Triplet<uint32_t>& valid_lifetime);
+ /// @brief Sets siaddr for the Subnet4
+ ///
+ /// Will be used for siaddr field (the next server) that typically is used
+ /// as TFTP server. If not specified, the default value of 0.0.0.0 is
+ /// used.
+ void setSiaddr(const isc::asiolink::IOAddress& siaddr);
+
+ /// @brief Returns siaddr for this subnet
+ ///
+ /// @return siaddr value
+ isc::asiolink::IOAddress getSiaddr() const;
+
protected:
/// @brief Check if option is valid and can be added to a subnet.
@@ -415,6 +559,17 @@ protected:
virtual isc::asiolink::IOAddress default_pool() const {
return (isc::asiolink::IOAddress("0.0.0.0"));
}
+
+ /// @brief Checks if used pool type is valid
+ ///
+ /// Allowed type for Subnet4 is Pool::TYPE_V4.
+ ///
+ /// @param type type to be checked
+ /// @throw BadValue if invalid value is used
+ virtual void checkType(Lease::Type type) const;
+
+ /// @brief siaddr value for this subnet
+ isc::asiolink::IOAddress siaddr_;
};
/// @brief A pointer to a Subnet4 object
@@ -432,6 +587,8 @@ public:
/// @brief Constructor with all parameters
///
+ /// This constructor calls Subnet::Subnet, where subnet-id is generated.
+ ///
/// @param prefix Subnet6 prefix
/// @param length prefix length
/// @param t1 renewal timer (in seconds)
@@ -451,17 +608,18 @@ public:
return (preferred_);
}
- /// @brief sets name of the network interface for directly attached networks
+ /// @brief sets interface-id option (if defined)
///
- /// A subnet may be reachable directly (not via relays). In DHCPv6 it is not
- /// possible to decide that based on addresses assigned to network interfaces,
- /// as DHCPv6 operates on link-local (and site local) addresses.
- /// @param iface_name name of the interface
- void setIface(const std::string& iface_name);
+ /// @param ifaceid pointer to interface-id option
+ void setInterfaceId(const OptionPtr& ifaceid) {
+ interface_id_ = ifaceid;
+ }
- /// @brief network interface name used to reach subnet (or "" for remote subnets)
- /// @return network interface name for directly attached subnets or ""
- std::string getIface() const;
+ /// @brief returns interface-id value (if specified)
+ /// @return interface-id option (if defined)
+ OptionPtr getInterfaceId() const {
+ return interface_id_;
+ }
protected:
@@ -478,8 +636,16 @@ protected:
return (isc::asiolink::IOAddress("::"));
}
- /// @brief collection of pools in that list
- Pool6Collection pools_;
+ /// @brief Checks if used pool type is valid
+ ///
+ /// allowed types for Subnet6 are Pool::TYPE_{IA,TA,PD}.
+ ///
+ /// @param type type to be checked
+ /// @throw BadValue if invalid value is used
+ virtual void checkType(Lease::Type type) const;
+
+ /// @brief specifies optional interface-id
+ OptionPtr interface_id_;
/// @brief a triplet with preferred lifetime (in seconds)
Triplet<uint32_t> preferred_;
diff --git a/src/lib/dhcpsrv/tests/.gitignore b/src/lib/dhcpsrv/tests/.gitignore
index 7add7fb..33ac8d9 100644
--- a/src/lib/dhcpsrv/tests/.gitignore
+++ b/src/lib/dhcpsrv/tests/.gitignore
@@ -1 +1,2 @@
/libdhcpsrv_unittests
+/test_libraries.h
diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am
index e19fd87..ff57254 100644
--- a/src/lib/dhcpsrv/tests/Makefile.am
+++ b/src/lib/dhcpsrv/tests/Makefile.am
@@ -13,10 +13,6 @@ AM_CXXFLAGS = $(B10_CXXFLAGS)
# But older GCC compilers don't have the flag.
AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
-if USE_STATIC_LINK
-AM_LDFLAGS = -static
-endif
-
CLEANFILES = *.gcno *.gcda
TESTS_ENVIRONMENT = \
@@ -24,22 +20,52 @@ TESTS_ENVIRONMENT = \
TESTS =
if HAVE_GTEST
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+lib_LTLIBRARIES = libco1.la libco2.la
+
+libco1_la_SOURCES = callout_library.cc
+libco1_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco1_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libco1_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+libco2_la_SOURCES = callout_library.cc
+libco2_la_CXXFLAGS = $(AM_CXXFLAGS)
+libco2_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libco2_la_LDFLAGS = -avoid-version -export-dynamic -module
+
TESTS += libdhcpsrv_unittests
libdhcpsrv_unittests_SOURCES = run_unittests.cc
libdhcpsrv_unittests_SOURCES += addr_utilities_unittest.cc
libdhcpsrv_unittests_SOURCES += alloc_engine_unittest.cc
+libdhcpsrv_unittests_SOURCES += callout_handle_store_unittest.cc
libdhcpsrv_unittests_SOURCES += cfgmgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += d2_client_unittest.cc
libdhcpsrv_unittests_SOURCES += dbaccess_parser_unittest.cc
+libdhcpsrv_unittests_SOURCES += lease_unittest.cc
libdhcpsrv_unittests_SOURCES += lease_mgr_factory_unittest.cc
libdhcpsrv_unittests_SOURCES += lease_mgr_unittest.cc
libdhcpsrv_unittests_SOURCES += memfile_lease_mgr_unittest.cc
+libdhcpsrv_unittests_SOURCES += dhcp_parsers_unittest.cc
if HAVE_MYSQL
libdhcpsrv_unittests_SOURCES += mysql_lease_mgr_unittest.cc
endif
libdhcpsrv_unittests_SOURCES += pool_unittest.cc
libdhcpsrv_unittests_SOURCES += schema_copy.h
libdhcpsrv_unittests_SOURCES += subnet_unittest.cc
+libdhcpsrv_unittests_SOURCES += test_get_callout_handle.cc test_get_callout_handle.h
libdhcpsrv_unittests_SOURCES += triplet_unittest.cc
libdhcpsrv_unittests_SOURCES += test_utils.cc test_utils.h
@@ -63,11 +89,13 @@ endif
libdhcpsrv_unittests_LDADD = $(top_builddir)/src/lib/dhcpsrv/libb10-dhcpsrv.la
libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libb10-dhcp++.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/dhcp_ddns/libb10-dhcp_ddns.la
libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/config/libb10-cfgclient.la
libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/cc/libb10-cc.la
libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
-libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/hooks/libb10-hooks.la
libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
+libdhcpsrv_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
libdhcpsrv_unittests_LDADD += $(GTEST_LDADD)
endif
diff --git a/src/lib/dhcpsrv/tests/alloc_engine_unittest.cc b/src/lib/dhcpsrv/tests/alloc_engine_unittest.cc
index 05f3741..8a37915 100644
--- a/src/lib/dhcpsrv/tests/alloc_engine_unittest.cc
+++ b/src/lib/dhcpsrv/tests/alloc_engine_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -25,18 +25,24 @@
#include <dhcpsrv/tests/test_utils.h>
+#include <hooks/server_hooks.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_manager.h>
+
#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>
#include <iostream>
#include <sstream>
+#include <algorithm>
#include <set>
#include <time.h>
using namespace std;
using namespace isc;
using namespace isc::asiolink;
+using namespace isc::hooks;
using namespace isc::dhcp;
using namespace isc::dhcp::test;
@@ -49,13 +55,30 @@ public:
/// @brief the sole constructor
/// @param engine_type specifies engine type (e.g. iterative)
/// @param attempts number of lease selection attempts before giving up
- NakedAllocEngine(AllocEngine::AllocType engine_type, unsigned int attempts)
- :AllocEngine(engine_type, attempts) {
+ /// @param ipv6 specifies if the engine is IPv6 or IPv4
+ NakedAllocEngine(AllocEngine::AllocType engine_type,
+ unsigned int attempts, bool ipv6 = true)
+ :AllocEngine(engine_type, attempts, ipv6) {
}
// Expose internal classes for testing purposes
using AllocEngine::Allocator;
using AllocEngine::IterativeAllocator;
+ using AllocEngine::getAllocator;
+
+ /// @brief IterativeAllocator with internal methods exposed
+ class NakedIterativeAllocator: public AllocEngine::IterativeAllocator {
+ public:
+
+ /// @brief constructor
+ /// @param type pool types that will be interated
+ NakedIterativeAllocator(Lease::Type type)
+ :IterativeAllocator(type) {
+ }
+
+ using AllocEngine::IterativeAllocator::increaseAddress;
+ using AllocEngine::IterativeAllocator::increasePrefix;
+ };
};
/// @brief Used in Allocation Engine tests for IPv6
@@ -74,45 +97,262 @@ public:
// instantiate cfg_mgr
CfgMgr& cfg_mgr = CfgMgr::instance();
+ // Configure normal address pool
subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1::10"),
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::10"),
IOAddress("2001:db8:1::20")));
subnet_->addPool(pool_);
+
+ // Configure PD pool
+ pd_pool_ = Pool6Ptr(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 56, 64));
+ subnet_->addPool(pd_pool_);
+
cfg_mgr.addSubnet6(subnet_);
factory_.create("type=memfile");
}
+ /// @brief attempts to convert leases collection to a single lease
+ ///
+ /// This operation makes sense if there is at most one lease in the
+ /// collection. Otherwise it will throw.
+ ///
+ /// @param col collection of leases (zero or one leases allowed)
+ /// @throw MultipleRecords if there is more than one lease
+ /// @return Lease6 pointer (or NULL if collection was empty)
+ Lease6Ptr expectOneLease(const Lease6Collection& col) {
+ if (col.size() > 1) {
+ isc_throw(MultipleRecords, "More than one lease found in collection");
+ }
+ if (col.empty()) {
+ return (Lease6Ptr());
+ }
+ return (*col.begin());
+ }
+
/// @brief checks if Lease6 matches expected configuration
///
/// @param lease lease to be checked
- void checkLease6(const Lease6Ptr& lease) {
+ /// @param exp_type expected lease type
+ /// @param exp_pd_len expected prefix length
+ void checkLease6(const Lease6Ptr& lease, Lease::Type exp_type,
+ uint8_t exp_pd_len = 128) {
+
// that is belongs to the right subnet
EXPECT_EQ(lease->subnet_id_, subnet_->getID());
EXPECT_TRUE(subnet_->inRange(lease->addr_));
- EXPECT_TRUE(subnet_->inPool(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(exp_type, lease->addr_));
// that it have proper parameters
+ EXPECT_EQ(exp_type, lease->type_);
EXPECT_EQ(iaid_, lease->iaid_);
EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
EXPECT_EQ(subnet_->getPreferred(), lease->preferred_lft_);
EXPECT_EQ(subnet_->getT1(), lease->t1_);
EXPECT_EQ(subnet_->getT2(), lease->t2_);
- EXPECT_EQ(0, lease->prefixlen_); // this is IA_NA, not IA_PD
+ EXPECT_EQ(exp_pd_len, lease->prefixlen_);
EXPECT_TRUE(false == lease->fqdn_fwd_);
EXPECT_TRUE(false == lease->fqdn_rev_);
EXPECT_TRUE(*lease->duid_ == *duid_);
- // @todo: check cltt
- }
+ /// @todo: check cltt
+ }
+
+ /// @brief Checks if specified address is increased properly
+ ///
+ /// Method uses gtest macros to mark check failure.
+ ///
+ /// @param alloc IterativeAllocator that is tested
+ /// @param input address to be increased
+ /// @param exp_output expected address after increase
+ void
+ checkAddrIncrease(NakedAllocEngine::NakedIterativeAllocator& alloc,
+ std::string input, std::string exp_output) {
+ EXPECT_EQ(exp_output, alloc.increaseAddress(IOAddress(input)).toText());
+ }
- ~AllocEngine6Test() {
+ /// @brief Checks if increasePrefix() works as expected
+ ///
+ /// Method uses gtest macros to mark check failure.
+ ///
+ /// @param alloc allocator to be tested
+ /// @param input IPv6 prefix (as a string)
+ /// @param prefix_len prefix len
+ /// @param exp_output expected output (string)
+ void
+ checkPrefixIncrease(NakedAllocEngine::NakedIterativeAllocator& alloc,
+ std::string input, uint8_t prefix_len,
+ std::string exp_output) {
+ EXPECT_EQ(exp_output, alloc.increasePrefix(IOAddress(input), prefix_len)
+ .toText());
+ }
+
+ /// @brief Checks if the simple allocation can succeed
+ ///
+ /// The type of lease is determined by pool type (pool->getType()
+ ///
+ /// @param pool pool from which the lease will be allocated from
+ /// @param hint address to be used as a hint
+ /// @param fake true - this is fake allocation (SOLICIT)
+ /// @return allocated lease (or NULL)
+ Lease6Ptr simpleAlloc6Test(const Pool6Ptr& pool, const IOAddress& hint,
+ bool fake) {
+ Lease::Type type = pool->getType();
+ uint8_t expected_len = pool->getLength();
+
+ boost::scoped_ptr<AllocEngine> engine;
+ EXPECT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100)));
+ // We can't use ASSERT macros in non-void methods
+ EXPECT_TRUE(engine);
+ if (!engine) {
+ return (Lease6Ptr());
+ }
+
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, hint, type, false, false,
+ "", fake, CalloutHandlePtr())));
+
+ // Check that we got a lease
+ EXPECT_TRUE(lease);
+ if (!lease) {
+ return (Lease6Ptr());
+ }
+
+ // Do all checks on the lease
+ checkLease6(lease, type, expected_len);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(type,
+ lease->addr_);
+ if (!fake) {
+ // This is a real (REQUEST) allocation, the lease must be in the DB
+ EXPECT_TRUE(from_mgr);
+ if (!from_mgr) {
+ return (Lease6Ptr());
+ }
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+ } else {
+ // This is a fake (SOLICIT) allocation, the lease must not be in DB
+ EXPECT_FALSE(from_mgr);
+ if (from_mgr) {
+ return (Lease6Ptr());
+ }
+ }
+
+ return (lease);
+ }
+
+ /// @brief Checks if the address allocation with a hint that is in range,
+ /// in pool, but is currently used, can succeed
+ ///
+ /// Method uses gtest macros to mark check failure.
+ ///
+ /// @param type lease type
+ /// @param used_addr address should be preallocated (simulates prior
+ /// allocation by some other user)
+ /// @param requested address requested by the client
+ /// @param expected_pd_len expected PD len (128 for addresses)
+ void allocWithUsedHintTest(Lease::Type type, IOAddress used_addr,
+ IOAddress requested, uint8_t expected_pd_len) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100)));
+ ASSERT_TRUE(engine);
+
+ // Let's create a lease and put it in the LeaseMgr
+ DuidPtr duid2 = boost::shared_ptr<DUID>(new DUID(vector<uint8_t>(8, 0xff)));
+ time_t now = time(NULL);
+ Lease6Ptr used(new Lease6(type, used_addr,
+ duid2, 1, 2, 3, 4, now, subnet_->getID()));
+ ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
+
+ // Another client comes in and request an address that is in pool, but
+ // unfortunately it is used already. The same address must not be allocated
+ // twice.
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, requested, type, false, false, "", false,
+ CalloutHandlePtr())));
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Allocated address must be different
+ EXPECT_NE(used_addr, lease->addr_);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE(requested, lease->addr_);
+
+ // Do all checks on the lease
+ checkLease6(lease, type, expected_pd_len);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+ }
+
+ /// @brief checks if bogus hint can be ignored and the allocation succeeds
+ ///
+ /// This test checks if the allocation with a hing that is out of the blue
+ /// can succeed. The invalid hint should be ingored completely.
+ ///
+ /// @param type Lease type
+ /// @param hint hint (as send by a client)
+ /// @param expectd_pd_len (used in validation)
+ void allocBogusHint6(Lease::Type type, IOAddress hint,
+ uint8_t expected_pd_len) {
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100)));
+ ASSERT_TRUE(engine);
+
+ // Client would like to get a 3000::abc lease, which does not belong to any
+ // supported lease. Allocation engine should ignore it and carry on
+ // with the normal allocation
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, hint, type, false,
+ false, "", false, CalloutHandlePtr())));
+
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // We should NOT get what we asked for, because it is used already
+ EXPECT_NE(hint, lease->addr_);
+
+ // Do all checks on the lease
+ checkLease6(lease, type, expected_pd_len);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ detailCompareLease(lease, from_mgr);
+
+
+
+
+ }
+
+
+ virtual ~AllocEngine6Test() {
factory_.destroy();
}
DuidPtr duid_; ///< client-identifier (value used in tests)
uint32_t iaid_; ///< IA identifier (value used in tests)
Subnet6Ptr subnet_; ///< subnet6 (used in tests)
- Pool6Ptr pool_; ///< pool belonging to subnet_
+ Pool6Ptr pool_; ///< NA pool belonging to subnet_
+ Pool6Ptr pd_pool_; ///< PD pool belonging to subnet_
LeaseMgrFactory factory_; ///< pointer to LeaseMgr factory
};
@@ -152,14 +392,12 @@ public:
// Check that is belongs to the right subnet
EXPECT_EQ(lease->subnet_id_, subnet_->getID());
EXPECT_TRUE(subnet_->inRange(lease->addr_));
- EXPECT_TRUE(subnet_->inPool(lease->addr_));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, lease->addr_));
// Check that it has proper parameters
EXPECT_EQ(subnet_->getValid(), lease->valid_lft_);
EXPECT_EQ(subnet_->getT1(), lease->t1_);
EXPECT_EQ(subnet_->getT2(), lease->t2_);
- EXPECT_TRUE(false == lease->fqdn_fwd_);
- EXPECT_TRUE(false == lease->fqdn_rev_);
if (lease->client_id_ && !clientid_) {
ADD_FAILURE() << "Lease4 has a client-id, while it should have none.";
} else
@@ -170,10 +408,10 @@ public:
EXPECT_TRUE(*lease->client_id_ == *clientid_);
}
EXPECT_TRUE(lease->hwaddr_ == hwaddr_->hwaddr_);
- // @todo: check cltt
+ /// @todo: check cltt
}
- ~AllocEngine4Test() {
+ virtual ~AllocEngine4Test() {
factory_.destroy();
}
@@ -182,10 +420,11 @@ public:
Subnet4Ptr subnet_; ///< Subnet4 (used in tests)
Pool4Ptr pool_; ///< Pool belonging to subnet_
LeaseMgrFactory factory_; ///< Pointer to LeaseMgr factory
+ Lease4Ptr old_lease_; ///< Holds previous instance of the lease.
};
-// This test checks if the Allocation Engine can be instantiated and that it
-// parses parameters string properly.
+// This test checks if the v6 Allocation Engine can be instantiated, parses
+// parameters string and allocators are created.
TEST_F(AllocEngine6Test, constructor) {
boost::scoped_ptr<AllocEngine> x;
@@ -193,148 +432,83 @@ TEST_F(AllocEngine6Test, constructor) {
ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_HASHED, 5)), NotImplemented);
ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_RANDOM, 5)), NotImplemented);
- ASSERT_NO_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
-}
+ ASSERT_NO_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100, true)));
-// This test checks if the simple allocation can succeed
-TEST_F(AllocEngine6Test, simpleAlloc6) {
- boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
- ASSERT_TRUE(engine);
+ // Check that allocator for normal addresses is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_NA));
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
- false);
+ // Check that allocator for temporary address is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_TA));
- // Check that we got a lease
- ASSERT_TRUE(lease);
+ // Check that allocator for prefixes is created
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_PD));
- // Do all checks on the lease
- checkLease6(lease);
+ // There should be no V4 allocator
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_V4), BadValue);
+}
- // Check that the lease is indeed in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
- ASSERT_TRUE(from_mgr);
+// This test checks if the simple allocation (REQUEST) can succeed
+TEST_F(AllocEngine6Test, simpleAlloc6) {
+ simpleAlloc6Test(pool_, IOAddress("::"), false);
+}
- // Now check that the lease in LeaseMgr has the same parameters
- detailCompareLease(lease, from_mgr);
+// This test checks if the simple PD allocation (REQUEST) can succeed
+TEST_F(AllocEngine6Test, pdSimpleAlloc6) {
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), false);
}
// This test checks if the fake allocation (for SOLICIT) can succeed
TEST_F(AllocEngine6Test, fakeAlloc6) {
- boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
- ASSERT_TRUE(engine);
-
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
- true);
- // Check that we got a lease
- ASSERT_TRUE(lease);
-
- // Do all checks on the lease
- checkLease6(lease);
-
- // Check that the lease is NOT in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
- ASSERT_FALSE(from_mgr);
+ simpleAlloc6Test(pool_, IOAddress("::"), true);
}
+// This test checks if the fake PD allocation (for SOLICIT) can succeed
+TEST_F(AllocEngine6Test, pdFakeAlloc6) {
+ simpleAlloc6Test(pd_pool_, IOAddress("::"), true);
+};
+
// This test checks if the allocation with a hint that is valid (in range,
// in pool and free) can succeed
TEST_F(AllocEngine6Test, allocWithValidHint6) {
- boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
- ASSERT_TRUE(engine);
-
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
- IOAddress("2001:db8:1::15"),
- false);
- // Check that we got a lease
- ASSERT_TRUE(lease);
+ Lease6Ptr lease = simpleAlloc6Test(pool_, IOAddress("2001:db8:1::15"),
+ false);
// We should get what we asked for
- EXPECT_EQ(lease->addr_.toText(), "2001:db8:1::15");
-
- // Do all checks on the lease
- checkLease6(lease);
-
- // Check that the lease is indeed in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
- ASSERT_TRUE(from_mgr);
-
- // Now check that the lease in LeaseMgr has the same parameters
- detailCompareLease(lease, from_mgr);
+ EXPECT_EQ("2001:db8:1::15", lease->addr_.toText());
}
-// This test checks if the allocation with a hint that is in range,
-// in pool, but is currently used) can succeed
+// This test checks if the address allocation with a hint that is in range,
+// in pool, but is currently used, can succeed
TEST_F(AllocEngine6Test, allocWithUsedHint6) {
- boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
- ASSERT_TRUE(engine);
-
- // Let's create a lease and put it in the LeaseMgr
- DuidPtr duid2 = boost::shared_ptr<DUID>(new DUID(vector<uint8_t>(8, 0xff)));
- time_t now = time(NULL);
- Lease6Ptr used(new Lease6(Lease6::LEASE_IA_NA, IOAddress("2001:db8:1::1f"),
- duid2, 1, 2, 3, 4, now, subnet_->getID()));
- ASSERT_TRUE(LeaseMgrFactory::instance().addLease(used));
-
- // Another client comes in and request an address that is in pool, but
- // unfortunately it is used already. The same address must not be allocated
- // twice.
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
- IOAddress("2001:db8:1::1f"),
- false);
- // Check that we got a lease
- ASSERT_TRUE(lease);
-
- // Allocated address must be different
- EXPECT_TRUE(used->addr_.toText() != lease->addr_.toText());
-
- // We should NOT get what we asked for, because it is used already
- EXPECT_TRUE(lease->addr_.toText() != "2001:db8:1::1f");
-
- // Do all checks on the lease
- checkLease6(lease);
-
- // Check that the lease is indeed in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
- ASSERT_TRUE(from_mgr);
+ allocWithUsedHintTest(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::1f"), // allocate this as used
+ IOAddress("2001:db8:1::1f"), // request this addr
+ 128);
+}
- // Now check that the lease in LeaseMgr has the same parameters
- detailCompareLease(lease, from_mgr);
+// This test checks if the PD allocation with a hint that is in range,
+// in pool, but is currently used, can succeed
+TEST_F(AllocEngine6Test, pdAllocWithUsedHint6) {
+ allocWithUsedHintTest(Lease::TYPE_PD,
+ IOAddress("2001:db8:1::"), // allocate this prefix as used
+ IOAddress("2001:db8:1::"), // request this prefix
+ 64);
}
// This test checks if the allocation with a hint that is out the blue
// can succeed. The invalid hint should be ignored completely.
TEST_F(AllocEngine6Test, allocBogusHint6) {
- boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
- ASSERT_TRUE(engine);
-
- // Client would like to get a 3000::abc lease, which does not belong to any
- // supported lease. Allocation engine should ignore it and carry on
- // with the normal allocation
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_,
- IOAddress("3000::abc"),
- false);
- // Check that we got a lease
- ASSERT_TRUE(lease);
- // We should NOT get what we asked for, because it is used already
- EXPECT_TRUE(lease->addr_.toText() != "3000::abc");
-
- // Do all checks on the lease
- checkLease6(lease);
+ allocBogusHint6(Lease::TYPE_NA, IOAddress("3000::abc"), 128);
+}
- // Check that the lease is indeed in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
- ASSERT_TRUE(from_mgr);
+// This test checks if the allocation with a hint that is out the blue
+// can succeed. The invalid hint should be ignored completely.
+TEST_F(AllocEngine6Test, pdAllocBogusHint6) {
- // Now check that the lease in LeaseMgr has the same parameters
- detailCompareLease(lease, from_mgr);
+ allocBogusHint6(Lease::TYPE_PD, IOAddress("3000::abc"), 64);
}
// This test checks that NULL values are handled properly
@@ -344,13 +518,16 @@ TEST_F(AllocEngine6Test, allocateAddress6Nulls) {
ASSERT_TRUE(engine);
// Allocations without subnet are not allowed
- Lease6Ptr lease = engine->allocateAddress6(Subnet6Ptr(), duid_, iaid_,
- IOAddress("::"), false);
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(
+ Subnet6Ptr(), duid_, iaid_, IOAddress("::"), Lease::TYPE_NA,
+ false, false, "", false, CalloutHandlePtr())));
ASSERT_FALSE(lease);
// Allocations without DUID are not allowed either
- lease = engine->allocateAddress6(subnet_, DuidPtr(), iaid_,
- IOAddress("::"), false);
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ DuidPtr(), iaid_, IOAddress("::"), Lease::TYPE_NA, false,
+ false, "", false, CalloutHandlePtr())));
ASSERT_FALSE(lease);
}
@@ -359,20 +536,179 @@ TEST_F(AllocEngine6Test, allocateAddress6Nulls) {
// pool
TEST_F(AllocEngine6Test, IterativeAllocator) {
boost::scoped_ptr<NakedAllocEngine::Allocator>
- alloc(new NakedAllocEngine::IterativeAllocator());
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_NA));
for (int i = 0; i < 1000; ++i) {
IOAddress candidate = alloc->pickAddress(subnet_, duid_, IOAddress("::"));
- EXPECT_TRUE(subnet_->inPool(candidate));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate));
}
}
+TEST_F(AllocEngine6Test, IterativeAllocatorAddrStep) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ subnet_->delPools(Lease::TYPE_NA); // Get rid of default pool
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::5")));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::100"),
+ IOAddress("2001:db8:1::100")));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::105"),
+ IOAddress("2001:db8:1::106")));
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // Let's check the first pool (5 addresses here)
+ EXPECT_EQ("2001:db8:1::1", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::3", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::4", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::5", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // The second pool is easy - only one address here
+ EXPECT_EQ("2001:db8:1::100", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // This is the third and last pool, with 2 addresses in it
+ EXPECT_EQ("2001:db8:1::105", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::106", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // We iterated over all addresses and reached to the end of the last pool.
+ // Let's wrap around and start from the beginning
+ EXPECT_EQ("2001:db8:1::1", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:1::2", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+}
+
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixStep) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ subnet_.reset(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
+
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8::"), 56, 60));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 48, 48));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:2::"), 56, 64));
+ subnet_->addPool(pool1);
+ subnet_->addPool(pool2);
+ subnet_->addPool(pool3);
+
+ // We have a 2001:db8::/48 subnet that has 3 pools defined in it:
+ // 2001:db8::/56 split into /60 prefixes (16 leases) (or 2001:db8:0:X0::)
+ // 2001:db8:1::/48 split into a single /48 prefix (just 1 lease)
+ // 2001:db8:2::/56 split into /64 prefixes (256 leases) (or 2001:db8:2:XX::)
+
+ // First pool check (Let's check over all 16 leases)
+ EXPECT_EQ("2001:db8::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:20::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:30::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:40::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:50::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:60::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:70::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:80::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:90::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:a0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:b0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:c0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:d0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:e0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:f0::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // Second pool (just one lease here)
+ EXPECT_EQ("2001:db8:1::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // Third pool (256 leases, let's check first and last explictly and the
+ // rest over in a pool
+ EXPECT_EQ("2001:db8:2::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ for (int i = 1; i < 255; i++) {
+ stringstream exp;
+ exp << "2001:db8:2:" << hex << i << dec << "::";
+ EXPECT_EQ(exp.str(), alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ }
+ EXPECT_EQ("2001:db8:2:ff::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+
+ // Ok, we've iterated over all prefixes in all pools. We now wrap around.
+ // We're looping over now (iterating over first pool again)
+ EXPECT_EQ("2001:db8::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+ EXPECT_EQ("2001:db8:0:10::", alloc.pickAddress(subnet_, duid_, IOAddress("::")).toText());
+}
+
+// This test verifies that the iterative allocator can step over addresses
+TEST_F(AllocEngine6Test, IterativeAllocatorAddressIncrease) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_NA);
+
+ // Let's pick the first address
+ IOAddress addr1 = alloc.pickAddress(subnet_, duid_, IOAddress("2001:db8:1::10"));
+
+ // Check that we can indeed pick the first address from the pool
+ EXPECT_EQ("2001:db8:1::10", addr1.toText());
+
+ // Check that addresses can be increased properly
+ checkAddrIncrease(alloc, "2001:db8::9", "2001:db8::a");
+ checkAddrIncrease(alloc, "2001:db8::f", "2001:db8::10");
+ checkAddrIncrease(alloc, "2001:db8::10", "2001:db8::11");
+ checkAddrIncrease(alloc, "2001:db8::ff", "2001:db8::100");
+ checkAddrIncrease(alloc, "2001:db8::ffff", "2001:db8::1:0");
+ checkAddrIncrease(alloc, "::", "::1");
+ checkAddrIncrease(alloc, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "::");
+}
+
+// This test verifies that the allocator can step over prefixes
+TEST_F(AllocEngine6Test, IterativeAllocatorPrefixIncrease) {
+ NakedAllocEngine::NakedIterativeAllocator alloc(Lease::TYPE_PD);
+
+ // For /128 prefix, increasePrefix should work the same as addressIncrease
+ checkPrefixIncrease(alloc, "2001:db8::9", 128, "2001:db8::a");
+ checkPrefixIncrease(alloc, "2001:db8::f", 128, "2001:db8::10");
+ checkPrefixIncrease(alloc, "2001:db8::10", 128, "2001:db8::11");
+ checkPrefixIncrease(alloc, "2001:db8::ff", 128, "2001:db8::100");
+ checkPrefixIncrease(alloc, "2001:db8::ffff", 128, "2001:db8::1:0");
+ checkPrefixIncrease(alloc, "::", 128, "::1");
+ checkPrefixIncrease(alloc, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, "::");
+
+ // Check that /64 prefixes can be generated
+ checkPrefixIncrease(alloc, "2001:db8::", 64, "2001:db8:0:1::");
+
+ // Check that prefix length not divisible by 8 are working
+ checkPrefixIncrease(alloc, "2001:db8::", 128, "2001:db8::1");
+ checkPrefixIncrease(alloc, "2001:db8::", 127, "2001:db8::2");
+ checkPrefixIncrease(alloc, "2001:db8::", 126, "2001:db8::4");
+ checkPrefixIncrease(alloc, "2001:db8::", 125, "2001:db8::8");
+ checkPrefixIncrease(alloc, "2001:db8::", 124, "2001:db8::10");
+ checkPrefixIncrease(alloc, "2001:db8::", 123, "2001:db8::20");
+ checkPrefixIncrease(alloc, "2001:db8::", 122, "2001:db8::40");
+ checkPrefixIncrease(alloc, "2001:db8::", 121, "2001:db8::80");
+ checkPrefixIncrease(alloc, "2001:db8::", 120, "2001:db8::100");
+
+ // These are not really useful cases, because there are bits set
+ // int the last (128 - prefix_len) bits. Nevertheless, it shows
+ // that the algorithm is working even in such cases
+ checkPrefixIncrease(alloc, "2001:db8::1", 128, "2001:db8::2");
+ checkPrefixIncrease(alloc, "2001:db8::1", 127, "2001:db8::3");
+ checkPrefixIncrease(alloc, "2001:db8::1", 126, "2001:db8::5");
+ checkPrefixIncrease(alloc, "2001:db8::1", 125, "2001:db8::9");
+ checkPrefixIncrease(alloc, "2001:db8::1", 124, "2001:db8::11");
+ checkPrefixIncrease(alloc, "2001:db8::1", 123, "2001:db8::21");
+ checkPrefixIncrease(alloc, "2001:db8::1", 122, "2001:db8::41");
+ checkPrefixIncrease(alloc, "2001:db8::1", 121, "2001:db8::81");
+ checkPrefixIncrease(alloc, "2001:db8::1", 120, "2001:db8::101");
+
+ // Let's try out couple real life scenarios
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 64, "2001:db8:1:abce::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 60, "2001:db8:1:abdd::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 56, "2001:db8:1:accd::");
+ checkPrefixIncrease(alloc, "2001:db8:1:abcd::", 52, "2001:db8:1:bbcd::");
+
+ // And now let's try something over the top
+ checkPrefixIncrease(alloc, "::", 1, "8000::");
+}
// This test verifies that the iterative allocator really walks over all addresses
// in all pools in specified subnet. It also must not pick the same address twice
// unless it runs out of pool space and must start over.
TEST_F(AllocEngine6Test, IterativeAllocator_manyPools6) {
- NakedAllocEngine::IterativeAllocator alloc;
+ NakedAllocEngine::IterativeAllocator alloc(Lease::TYPE_NA);
// let's start from 2, as there is 2001:db8:1::10 - 2001:db8:1::20 pool already.
for (int i = 2; i < 10; ++i) {
@@ -381,7 +717,7 @@ TEST_F(AllocEngine6Test, IterativeAllocator_manyPools6) {
min << "2001:db8:1::" << hex << i*16 + 1;
max << "2001:db8:1::" << hex << i*16 + 9;
- Pool6Ptr pool(new Pool6(Pool6::TYPE_IA, IOAddress(min.str()),
+ Pool6Ptr pool(new Pool6(Lease::TYPE_NA, IOAddress(min.str()),
IOAddress(max.str())));
subnet_->addPool(pool);
}
@@ -394,11 +730,11 @@ TEST_F(AllocEngine6Test, IterativeAllocator_manyPools6) {
int cnt = 0;
while (++cnt) {
IOAddress candidate = alloc.pickAddress(subnet_, duid_, IOAddress("::"));
- EXPECT_TRUE(subnet_->inPool(candidate));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_NA, candidate));
// One way to easily verify that the iterative allocator really works is
// to uncomment the following line and observe its output that it
- // covers all defined subnets.
+ // covers all defined pools.
// cout << candidate.toText() << endl;
if (generated_addrs.find(candidate) == generated_addrs.end()) {
@@ -434,12 +770,14 @@ TEST_F(AllocEngine6Test, smallPool6) {
// Create configuration similar to other tests, but with a single address pool
subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, addr, addr)); // just a single address
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
subnet_->addPool(pool_);
cfg_mgr.addSubnet6(subnet_);
- Lease6Ptr lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
- false);
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, IOAddress("::"), Lease::TYPE_NA, false, false,
+ "", false, CalloutHandlePtr())));
// Check that we got that single lease
ASSERT_TRUE(lease);
@@ -447,10 +785,11 @@ TEST_F(AllocEngine6Test, smallPool6) {
EXPECT_EQ("2001:db8:1::ad", lease->addr_.toText());
// Do all checks on the lease
- checkLease6(lease);
+ checkLease6(lease, Lease::TYPE_NA, 128);
// Check that the lease is indeed in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->addr_);
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
ASSERT_TRUE(from_mgr);
// Now check that the lease in LeaseMgr has the same parameters
@@ -470,22 +809,24 @@ TEST_F(AllocEngine6Test, outOfAddresses6) {
// Create configuration similar to other tests, but with a single address pool
subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, addr, addr)); // just a single address
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
subnet_->addPool(pool_);
cfg_mgr.addSubnet6(subnet_);
// Just a different duid
DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
const uint32_t other_iaid = 3568;
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, other_duid, other_iaid,
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
501, 502, 503, 504, subnet_->getID(), 0));
lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// There is just a single address in the pool and allocated it to someone
// else, so the allocation should fail
- Lease6Ptr lease2 = engine->allocateAddress6(subnet_, duid_, iaid_,
- IOAddress("::"), false);
+ Lease6Ptr lease2;
+ EXPECT_NO_THROW(lease2 = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, IOAddress("::"), Lease::TYPE_NA, false, false,
+ "", false, CalloutHandlePtr())));
EXPECT_FALSE(lease2);
}
@@ -501,14 +842,14 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
// Create configuration similar to other tests, but with a single address pool
subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, addr, addr)); // just a single address
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
subnet_->addPool(pool_);
cfg_mgr.addSubnet6(subnet_);
// Just a different duid
DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
const uint32_t other_iaid = 3568;
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, other_duid, other_iaid,
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
501, 502, 503, 504, subnet_->getID(), 0));
lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
lease->valid_lft_ = 495; // Lease was valid for 495 seconds
@@ -518,21 +859,24 @@ TEST_F(AllocEngine6Test, solicitReuseExpiredLease6) {
ASSERT_TRUE(lease->expired());
// CASE 1: Asking for any address
- lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress("::"),
- true);
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, IOAddress("::"), Lease::TYPE_NA, false, false, "", true,
+ CalloutHandlePtr())));
// Check that we got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
// Do all checks on the lease (if subnet-id, preferred/valid times are ok etc.)
- checkLease6(lease);
+ checkLease6(lease, Lease::TYPE_NA, 128);
// CASE 2: Asking specifically for this address
- lease = engine->allocateAddress6(subnet_, duid_, iaid_, IOAddress(addr.toText()),
- true);
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, addr, Lease::TYPE_NA, false, false, "",
+ true, CalloutHandlePtr())));
+
// Check that we got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
}
// This test checks if an expired lease can be reused in REQUEST (actual allocation)
@@ -547,7 +891,7 @@ TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
// Create configuration similar to other tests, but with a single address pool
subnet_ = Subnet6Ptr(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- pool_ = Pool6Ptr(new Pool6(Pool6::TYPE_IA, addr, addr)); // just a single address
+ pool_ = Pool6Ptr(new Pool6(Lease::TYPE_NA, addr, addr)); // just a single address
subnet_->addPool(pool_);
cfg_mgr.addSubnet6(subnet_);
@@ -555,22 +899,24 @@ TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
DuidPtr other_duid = DuidPtr(new DUID(vector<uint8_t>(12, 0xff)));
const uint32_t other_iaid = 3568;
const SubnetID other_subnetid = 999;
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr, other_duid, other_iaid,
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr, other_duid, other_iaid,
501, 502, 503, 504, other_subnetid, 0));
lease->cltt_ = time(NULL) - 500; // Allocated 500 seconds ago
lease->valid_lft_ = 495; // Lease was valid for 495 seconds
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// A client comes along, asking specifically for this address
- lease = engine->allocateAddress6(subnet_, duid_, iaid_,
- IOAddress(addr.toText()), false);
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, addr, Lease::TYPE_NA, false, false, "",
+ false, CalloutHandlePtr())));
// Check that he got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
// Check that the lease is indeed updated in LeaseMgr
- Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(addr);
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(Lease::TYPE_NA,
+ addr);
ASSERT_TRUE(from_mgr);
// Now check that the lease in LeaseMgr has the same parameters
@@ -579,14 +925,47 @@ TEST_F(AllocEngine6Test, requestReuseExpiredLease6) {
// --- IPv4 ---
+// This test checks if the v4 Allocation Engine can be instantiated, parses
+// parameters string and allocators are created.
+TEST_F(AllocEngine4Test, constructor) {
+ boost::scoped_ptr<AllocEngine> x;
+
+ // Hashed and random allocators are not supported yet
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_HASHED, 5, false)),
+ NotImplemented);
+ ASSERT_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_RANDOM, 5, false)),
+ NotImplemented);
+
+ // Create V4 (ipv6=false) Allocation Engine that will try at most
+ // 100 attempts to pick up a lease
+ ASSERT_NO_THROW(x.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100,
+ false)));
+
+ // There should be V4 allocator
+ ASSERT_TRUE(x->getAllocator(Lease::TYPE_V4));
+
+ // Check that allocators for V6 stuff are not created
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_NA), BadValue);
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW(x->getAllocator(Lease::TYPE_PD), BadValue);
+}
+
+
// This test checks if the simple IPv4 allocation can succeed
TEST_F(AllocEngine4Test, simpleAlloc4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
- IOAddress("0.0.0.0"), false);
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, true,
+ "somehost.example.com.",
+ false, CalloutHandlePtr(),
+ old_lease_);
+ // The new lease has been allocated, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
// Check that we got a lease
ASSERT_TRUE(lease);
@@ -605,11 +984,18 @@ TEST_F(AllocEngine4Test, simpleAlloc4) {
// This test checks if the fake allocation (for DISCOVER) can succeed
TEST_F(AllocEngine4Test, fakeAlloc4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
- IOAddress("0.0.0.0"), true);
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, true, "host.example.com.",
+ true, CalloutHandlePtr(),
+ old_lease_);
+
+ // The new lease has been allocated, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
// Check that we got a lease
ASSERT_TRUE(lease);
@@ -627,16 +1013,21 @@ TEST_F(AllocEngine4Test, fakeAlloc4) {
// in pool and free) can succeed
TEST_F(AllocEngine4Test, allocWithValidHint4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
IOAddress("192.0.2.105"),
- false);
-
+ true, true, "host.example.com.",
+ false, CalloutHandlePtr(),
+ old_lease_);
// Check that we got a lease
ASSERT_TRUE(lease);
+ // We have allocated the new lease, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
+
// We should get what we asked for
EXPECT_EQ(lease->addr_.toText(), "192.0.2.105");
@@ -656,7 +1047,8 @@ TEST_F(AllocEngine4Test, allocWithValidHint4) {
// in pool, but is currently used) can succeed
TEST_F(AllocEngine4Test, allocWithUsedHint4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
// Let's create a lease and put it in the LeaseMgr
@@ -670,17 +1062,23 @@ TEST_F(AllocEngine4Test, allocWithUsedHint4) {
// Another client comes in and request an address that is in pool, but
// unfortunately it is used already. The same address must not be allocated
// twice.
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
IOAddress("192.0.2.106"),
- false);
+ false, false, "",
+ false, CalloutHandlePtr(),
+ old_lease_);
+
+ // New lease has been allocated, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
+
// Check that we got a lease
ASSERT_TRUE(lease);
// Allocated address must be different
- EXPECT_TRUE(used->addr_.toText() != lease->addr_.toText());
+ EXPECT_NE(used->addr_, lease->addr_);
// We should NOT get what we asked for, because it is used already
- EXPECT_TRUE(lease->addr_.toText() != "192.0.2.106");
+ EXPECT_NE("192.0.2.106", lease->addr_.toText());
// Do all checks on the lease
checkLease4(lease);
@@ -698,20 +1096,26 @@ TEST_F(AllocEngine4Test, allocWithUsedHint4) {
// can succeed. The invalid hint should be ignored completely.
TEST_F(AllocEngine4Test, allocBogusHint4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
// Client would like to get a 3000::abc lease, which does not belong to any
// supported lease. Allocation engine should ignore it and carry on
// with the normal allocation
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
IOAddress("10.1.1.1"),
- false);
+ false, false, "",
+ false, CalloutHandlePtr(),
+ old_lease_);
// Check that we got a lease
ASSERT_TRUE(lease);
+ // We have allocated a new lease, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
+
// We should NOT get what we asked for, because it is used already
- EXPECT_TRUE(lease->addr_.toText() != "10.1.1.1");
+ EXPECT_NE("10.1.1.1", lease->addr_.toText());
// Do all checks on the lease
checkLease4(lease);
@@ -726,27 +1130,40 @@ TEST_F(AllocEngine4Test, allocBogusHint4) {
// This test checks that NULL values are handled properly
-TEST_F(AllocEngine4Test, allocateAddress4Nulls) {
+TEST_F(AllocEngine4Test, allocateLease4Nulls) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
// Allocations without subnet are not allowed
- Lease4Ptr lease = engine->allocateAddress4(SubnetPtr(), clientid_, hwaddr_,
- IOAddress("0.0.0.0"), false);
+ Lease4Ptr lease = engine->allocateLease4(SubnetPtr(), clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ false, CalloutHandlePtr(),
+ old_lease_);
EXPECT_FALSE(lease);
// Allocations without HW address are not allowed
- lease = engine->allocateAddress4(subnet_, clientid_, HWAddrPtr(),
- IOAddress("0.0.0.0"), false);
+ lease = engine->allocateLease4(subnet_, clientid_, HWAddrPtr(),
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ false, CalloutHandlePtr(),
+ old_lease_);
EXPECT_FALSE(lease);
+ EXPECT_FALSE(old_lease_);
// Allocations without client-id are allowed
clientid_ = ClientIdPtr();
- lease = engine->allocateAddress4(subnet_, ClientIdPtr(), hwaddr_,
- IOAddress("0.0.0.0"), false);
+ lease = engine->allocateLease4(subnet_, ClientIdPtr(), hwaddr_,
+ IOAddress("0.0.0.0"),
+ true, true, "myhost.example.com.",
+ false, CalloutHandlePtr(),
+ old_lease_);
// Check that we got a lease
ASSERT_TRUE(lease);
+ // New lease has been allocated, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
// Do all checks on the lease
checkLease4(lease);
@@ -765,12 +1182,12 @@ TEST_F(AllocEngine4Test, allocateAddress4Nulls) {
// pool
TEST_F(AllocEngine4Test, IterativeAllocator) {
boost::scoped_ptr<NakedAllocEngine::Allocator>
- alloc(new NakedAllocEngine::IterativeAllocator());
+ alloc(new NakedAllocEngine::IterativeAllocator(Lease::TYPE_V4));
for (int i = 0; i < 1000; ++i) {
IOAddress candidate = alloc->pickAddress(subnet_, clientid_,
IOAddress("0.0.0.0"));
- EXPECT_TRUE(subnet_->inPool(candidate));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate));
}
}
@@ -779,7 +1196,7 @@ TEST_F(AllocEngine4Test, IterativeAllocator) {
// in all pools in specified subnet. It also must not pick the same address twice
// unless it runs out of pool space and must start over.
TEST_F(AllocEngine4Test, IterativeAllocator_manyPools4) {
- NakedAllocEngine::IterativeAllocator alloc;
+ NakedAllocEngine::IterativeAllocator alloc(Lease::TYPE_V4);
// Let's start from 2, as there is 2001:db8:1::10 - 2001:db8:1::20 pool already.
for (int i = 2; i < 10; ++i) {
@@ -802,7 +1219,7 @@ TEST_F(AllocEngine4Test, IterativeAllocator_manyPools4) {
int cnt = 0;
while (++cnt) {
IOAddress candidate = alloc.pickAddress(subnet_, clientid_, IOAddress("0.0.0.0"));
- EXPECT_TRUE(subnet_->inPool(candidate));
+ EXPECT_TRUE(subnet_->inPool(Lease::TYPE_V4, candidate));
// One way to easily verify that the iterative allocator really works is
// to uncomment the following line and observe its output that it
@@ -834,7 +1251,8 @@ TEST_F(AllocEngine4Test, IterativeAllocator_manyPools4) {
// This test checks if really small pools are working
TEST_F(AllocEngine4Test, smallPool4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
IOAddress addr("192.0.2.17");
@@ -847,12 +1265,18 @@ TEST_F(AllocEngine4Test, smallPool4) {
subnet_->addPool(pool_);
cfg_mgr.addSubnet4(subnet_);
- Lease4Ptr lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
- false);
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ true, true, "host.example.com.",
+ false, CalloutHandlePtr(),
+ old_lease_);
// Check that we got that single lease
ASSERT_TRUE(lease);
+ // We have allocated new lease, so the old lease should not exist.
+ EXPECT_FALSE(old_lease_);
+
EXPECT_EQ("192.0.2.17", lease->addr_.toText());
// Do all checks on the lease
@@ -870,7 +1294,8 @@ TEST_F(AllocEngine4Test, smallPool4) {
// to find out a new lease fails.
TEST_F(AllocEngine4Test, outOfAddresses4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
IOAddress addr("192.0.2.17");
@@ -887,23 +1312,29 @@ TEST_F(AllocEngine4Test, outOfAddresses4) {
uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
time_t now = time(NULL);
- Lease4Ptr lease(new Lease4(addr, hwaddr2, sizeof(hwaddr2), clientid2, sizeof(clientid2),
- 501, 502, 503, now, subnet_->getID()));
+ Lease4Ptr lease(new Lease4(addr, hwaddr2, sizeof(hwaddr2), clientid2,
+ sizeof(clientid2), 501, 502, 503, now,
+ subnet_->getID()));
lease->cltt_ = time(NULL) - 10; // Allocated 10 seconds ago
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// There is just a single address in the pool and allocated it to someone
// else, so the allocation should fail
- Lease4Ptr lease2 = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
- IOAddress("0.0.0.0"), false);
+ Lease4Ptr lease2 = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ false, CalloutHandlePtr(),
+ old_lease_);
EXPECT_FALSE(lease2);
+ EXPECT_FALSE(old_lease_);
}
// This test checks if an expired lease can be reused in DISCOVER (fake allocation)
TEST_F(AllocEngine4Test, discoverReuseExpiredLease4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
IOAddress addr("192.0.2.15");
@@ -920,35 +1351,58 @@ TEST_F(AllocEngine4Test, discoverReuseExpiredLease4) {
uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
time_t now = time(NULL) - 500; // Allocated 500 seconds ago
- Lease4Ptr lease(new Lease4(addr, clientid2, sizeof(clientid2), hwaddr2, sizeof(hwaddr2),
+ Lease4Ptr lease(new Lease4(addr, clientid2, sizeof(clientid2),
+ hwaddr2, sizeof(hwaddr2),
495, 100, 200, now, subnet_->getID()));
+ // Copy the lease, so as it can be compared with the old lease returned
+ // by the allocation engine.
+ Lease4 original_lease(*lease);
+
// Lease was assigned 500 seconds ago, but its valid lifetime is 495, so it
// is expired already
ASSERT_TRUE(lease->expired());
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// CASE 1: Asking for any address
- lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress("0.0.0.0"),
- true);
+ lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ true, CalloutHandlePtr(),
+ old_lease_);
// Check that we got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
+
+ // We are reusing expired lease, the old (expired) instance should be
+ // returned. The returned instance should be the same as the original
+ // lease.
+ ASSERT_TRUE(old_lease_);
+ EXPECT_TRUE(original_lease == *old_lease_);
// Do all checks on the lease (if subnet-id, preferred/valid times are ok etc.)
checkLease4(lease);
// CASE 2: Asking specifically for this address
- lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_, IOAddress(addr.toText()),
- true);
+ lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress(addr),
+ false, false, "",
+ true, CalloutHandlePtr(),
+ old_lease_);
// Check that we got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
+
+ // We are updating expired lease. The copy of the old lease should be
+ // returned and it should be equal to the original lease.
+ ASSERT_TRUE(old_lease_);
+ EXPECT_TRUE(*old_lease_ == original_lease);
}
// This test checks if an expired lease can be reused in REQUEST (actual allocation)
TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
IOAddress addr("192.0.2.105");
@@ -957,20 +1411,28 @@ TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
uint8_t hwaddr2[] = { 0, 0xfe, 0xfe, 0xfe, 0xfe, 0xfe};
uint8_t clientid2[] = { 8, 7, 6, 5, 4, 3, 2, 1 };
time_t now = time(NULL) - 500; // Allocated 500 seconds ago
- Lease4Ptr lease(new Lease4(addr, clientid2, sizeof(clientid2), hwaddr2, sizeof(hwaddr2),
- 495, 100, 200, now, subnet_->getID()));
+ Lease4Ptr lease(new Lease4(addr, clientid2, sizeof(clientid2), hwaddr2,
+ sizeof(hwaddr2), 495, 100, 200, now,
+ subnet_->getID()));
+ // Make a copy of the lease, so as we can comapre that with the old lease
+ // instance returned by the allocation engine.
+ Lease4 original_lease(*lease);
+
// Lease was assigned 500 seconds ago, but its valid lifetime is 495, so it
// is expired already
ASSERT_TRUE(lease->expired());
ASSERT_TRUE(LeaseMgrFactory::instance().addLease(lease));
// A client comes along, asking specifically for this address
- lease = engine->allocateAddress4(subnet_, clientid_, hwaddr_,
- IOAddress(addr.toText()), false);
+ lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress(addr),
+ false, true, "host.example.com.",
+ false, CalloutHandlePtr(),
+ old_lease_);
// Check that he got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
// Check that the lease is indeed updated in LeaseMgr
Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(addr);
@@ -978,13 +1440,23 @@ TEST_F(AllocEngine4Test, requestReuseExpiredLease4) {
// Now check that the lease in LeaseMgr has the same parameters
detailCompareLease(lease, from_mgr);
+
+ // The allocation engine should return a copy of the old lease. This
+ // lease should be equal to the original lease.
+ ASSERT_TRUE(old_lease_);
+ EXPECT_TRUE(*old_lease_ == original_lease);
}
+/// @todo write renewLease6
+
// This test checks if a lease is really renewed when renewLease4 method is
// called
TEST_F(AllocEngine4Test, renewLease4) {
boost::scoped_ptr<AllocEngine> engine;
- ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
ASSERT_TRUE(engine);
IOAddress addr("192.0.2.102");
@@ -1004,10 +1476,12 @@ TEST_F(AllocEngine4Test, renewLease4) {
// Lease was assigned 45 seconds ago and is valid for 100 seconds. Let's
// renew it.
ASSERT_FALSE(lease->expired());
- lease = engine->renewLease4(subnet_, clientid_, hwaddr_, lease, false);
+ lease = engine->renewLease4(subnet_, clientid_, hwaddr_, true,
+ true, "host.example.com.", lease,
+ callout_handle, false);
// Check that he got that single lease
ASSERT_TRUE(lease);
- EXPECT_EQ(addr.toText(), lease->addr_.toText());
+ EXPECT_EQ(addr, lease->addr_);
// Check that the lease matches subnet_, hwaddr_,clientid_ parameters
checkLease4(lease);
@@ -1020,4 +1494,449 @@ TEST_F(AllocEngine4Test, renewLease4) {
detailCompareLease(lease, from_mgr);
}
+/// @brief helper class used in Hooks testing in AllocEngine6
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+class HookAllocEngine6Test : public AllocEngine6Test {
+public:
+ HookAllocEngine6Test() {
+ resetCalloutBuffers();
+ }
+
+ virtual ~HookAllocEngine6Test() {
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease6_select");
+ }
+
+ /// @brief clears out buffers, so callouts can store received arguments
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_subnet6_.reset();
+ callback_fake_allocation_ = false;
+ callback_lease6_.reset();
+ callback_argument_names_.clear();
+ callback_addr_original_ = IOAddress("::");
+ callback_addr_updated_ = IOAddress("::");
+ }
+
+ /// callback that stores received callout name and received values
+ static int
+ lease6_select_callout(CalloutHandle& callout_handle) {
+
+ callback_name_ = string("lease6_select");
+
+ callout_handle.getArgument("subnet6", callback_subnet6_);
+ callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+ callout_handle.getArgument("lease6", callback_lease6_);
+
+ callback_addr_original_ = callback_lease6_->addr_;
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// callback that overrides the lease with different values
+ static int
+ lease6_select_different_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease6_select_callout(callout_handle);
+
+ // Now we need to tweak the least a bit
+ Lease6Ptr lease;
+ callout_handle.getArgument("lease6", lease);
+ callback_addr_updated_ = addr_override_;
+ lease->addr_ = callback_addr_updated_;
+ lease->t1_ = t1_override_;
+ lease->t2_ = t2_override_;
+ lease->preferred_lft_ = pref_override_;
+ lease->valid_lft_ = valid_override_;
+
+ return (0);
+ }
+
+ // Values to be used in callout to override lease6 content
+ static const IOAddress addr_override_;
+ static const uint32_t t1_override_;
+ static const uint32_t t2_override_;
+ static const uint32_t pref_override_;
+ static const uint32_t valid_override_;
+
+ // Callback will store original and overridden values here
+ static IOAddress callback_addr_original_;
+ static IOAddress callback_addr_updated_;
+
+ // Buffers (callback will store received values here)
+ static string callback_name_;
+ static Subnet6Ptr callback_subnet6_;
+ static Lease6Ptr callback_lease6_;
+ static bool callback_fake_allocation_;
+ static vector<string> callback_argument_names_;
+};
+
+// For some reason intialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine6Test::addr_override_("2001:db8::abcd");
+const uint32_t HookAllocEngine6Test::t1_override_ = 6000;
+const uint32_t HookAllocEngine6Test::t2_override_ = 7000;
+const uint32_t HookAllocEngine6Test::pref_override_ = 8000;
+const uint32_t HookAllocEngine6Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine6Test::callback_addr_original_("::");
+IOAddress HookAllocEngine6Test::callback_addr_updated_("::");
+
+string HookAllocEngine6Test::callback_name_;
+Subnet6Ptr HookAllocEngine6Test::callback_subnet6_;
+Lease6Ptr HookAllocEngine6Test::callback_lease6_;
+bool HookAllocEngine6Test::callback_fake_allocation_;
+vector<string> HookAllocEngine6Test::callback_argument_names_;
+
+// This test checks if the lease6_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine6Test, lease6_select) {
+
+ // Note: The following order is working as expected:
+ // 1. create AllocEngine (that register hook points)
+ // 2. call loadLibraries()
+ //
+ // This order, however, causes segfault in HooksManager
+ // 1. call loadLibraries()
+ // 2. create AllocEngine (that register hook points)
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ vector<string> libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install pkt6_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_select", lease6_select_callout));
+
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, IOAddress("::"), Lease::TYPE_NA, false, false,
+ "", false, callout_handle)));
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease6(lease, Lease::TYPE_NA, 128);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check that callouts were indeed called
+ EXPECT_EQ("lease6_select", callback_name_);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ ASSERT_TRUE(callback_lease6_);
+ detailCompareLease(callback_lease6_, from_mgr);
+
+ ASSERT_TRUE(callback_subnet6_);
+ EXPECT_EQ(subnet_->toText(), callback_subnet6_->toText());
+
+ EXPECT_FALSE(callback_fake_allocation_);
+
+ // Check if all expected parameters are reported. It's a bit tricky, because
+ // order may be different. If the test starts failing, because someone tweaked
+ // hooks engine, we'll have to implement proper vector matching (ignoring order)
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("fake_allocation");
+ expected_argument_names.push_back("lease6");
+ expected_argument_names.push_back("subnet6");
+
+ sort(callback_argument_names_.begin(), callback_argument_names_.end());
+ sort(expected_argument_names.begin(), expected_argument_names.end());
+
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test checks if lease6_select callout is able to override the values
+// in a lease6.
+TEST_F(HookAllocEngine6Test, change_lease6_select) {
+
+ // Make sure that the overridden values are different than the ones from
+ // subnet originally used to create the lease
+ ASSERT_NE(t1_override_, subnet_->getT1());
+ ASSERT_NE(t2_override_, subnet_->getT2());
+ ASSERT_NE(pref_override_, subnet_->getPreferred());
+ ASSERT_NE(valid_override_, subnet_->getValid());
+ ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE, 100)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ vector<string> libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease6_select", lease6_select_different_callout));
+
+ // Normally, dhcpv6_srv would passed the handle when calling allocateLeases6,
+ // but in tests we need to create it on our own.
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ // Call allocateLeases6. Callouts should be triggered here.
+ Lease6Ptr lease;
+ EXPECT_NO_THROW(lease = expectOneLease(engine->allocateLeases6(subnet_,
+ duid_, iaid_, IOAddress("::"), Lease::TYPE_NA, false, false,
+ "", false, callout_handle)));
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // See if the values overridden by callout are there
+ EXPECT_TRUE(lease->addr_.equals(addr_override_));
+ EXPECT_EQ(t1_override_, lease->t1_);
+ EXPECT_EQ(t2_override_, lease->t2_);
+ EXPECT_EQ(pref_override_, lease->preferred_lft_);
+ EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+ // Now check if the lease is in the database
+ Lease6Ptr from_mgr = LeaseMgrFactory::instance().getLease6(lease->type_,
+ lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check if values in the database are overridden
+ EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+ EXPECT_EQ(t1_override_, from_mgr->t1_);
+ EXPECT_EQ(t2_override_, from_mgr->t2_);
+ EXPECT_EQ(pref_override_, from_mgr->preferred_lft_);
+ EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+}
+
+
+/// @brief helper class used in Hooks testing in AllocEngine4
+///
+/// It features a couple of callout functions and buffers to store
+/// the data that is accessible via callouts.
+///
+/// Note: lease4_renew callout is tested from DHCPv4 server.
+/// See HooksDhcpv4SrvTest.basic_lease4_renew in
+/// src/bin/dhcp4/tests/dhcp4_srv_unittest.cc
+class HookAllocEngine4Test : public AllocEngine4Test {
+public:
+ HookAllocEngine4Test() {
+ resetCalloutBuffers();
+ }
+
+ virtual ~HookAllocEngine4Test() {
+ HooksManager::preCalloutsLibraryHandle().deregisterAllCallouts(
+ "lease4_select");
+ }
+
+ /// @brief clears out buffers, so callouts can store received arguments
+ void resetCalloutBuffers() {
+ callback_name_ = string("");
+ callback_subnet4_.reset();
+ callback_fake_allocation_ = false;
+ callback_lease4_.reset();
+ callback_argument_names_.clear();
+ callback_addr_original_ = IOAddress("::");
+ callback_addr_updated_ = IOAddress("::");
+ }
+
+ /// callback that stores received callout name and received values
+ static int
+ lease4_select_callout(CalloutHandle& callout_handle) {
+
+ callback_name_ = string("lease4_select");
+
+ callout_handle.getArgument("subnet4", callback_subnet4_);
+ callout_handle.getArgument("fake_allocation", callback_fake_allocation_);
+ callout_handle.getArgument("lease4", callback_lease4_);
+
+ callback_addr_original_ = callback_lease4_->addr_;
+
+ callback_argument_names_ = callout_handle.getArgumentNames();
+ return (0);
+ }
+
+ /// callback that overrides the lease with different values
+ static int
+ lease4_select_different_callout(CalloutHandle& callout_handle) {
+
+ // Let's call the basic callout, so it can record all parameters
+ lease4_select_callout(callout_handle);
+
+ // Now we need to tweak the least a bit
+ Lease4Ptr lease;
+ callout_handle.getArgument("lease4", lease);
+ callback_addr_updated_ = addr_override_;
+ lease->addr_ = callback_addr_updated_;
+ lease->t1_ = t1_override_;
+ lease->t2_ = t2_override_;
+ lease->valid_lft_ = valid_override_;
+
+ return (0);
+ }
+
+ // Values to be used in callout to override lease4 content
+ static const IOAddress addr_override_;
+ static const uint32_t t1_override_;
+ static const uint32_t t2_override_;
+ static const uint32_t valid_override_;
+
+ // Callback will store original and overridden values here
+ static IOAddress callback_addr_original_;
+ static IOAddress callback_addr_updated_;
+
+ // Buffers (callback will store received values here)
+ static string callback_name_;
+ static Subnet4Ptr callback_subnet4_;
+ static Lease4Ptr callback_lease4_;
+ static bool callback_fake_allocation_;
+ static vector<string> callback_argument_names_;
+};
+
+// For some reason intialization within a class makes the linker confused.
+// linker complains about undefined references if they are defined within
+// the class declaration.
+const IOAddress HookAllocEngine4Test::addr_override_("192.0.3.1");
+const uint32_t HookAllocEngine4Test::t1_override_ = 4000;
+const uint32_t HookAllocEngine4Test::t2_override_ = 7000;
+const uint32_t HookAllocEngine4Test::valid_override_ = 9000;
+
+IOAddress HookAllocEngine4Test::callback_addr_original_("::");
+IOAddress HookAllocEngine4Test::callback_addr_updated_("::");
+
+string HookAllocEngine4Test::callback_name_;
+Subnet4Ptr HookAllocEngine4Test::callback_subnet4_;
+Lease4Ptr HookAllocEngine4Test::callback_lease4_;
+bool HookAllocEngine4Test::callback_fake_allocation_;
+vector<string> HookAllocEngine4Test::callback_argument_names_;
+
+// This test checks if the lease4_select callout is executed and expected
+// parameters as passed.
+TEST_F(HookAllocEngine4Test, lease4_select) {
+
+ // Note: The following order is working as expected:
+ // 1. create AllocEngine (that register hook points)
+ // 2. call loadLibraries()
+ //
+ // This order, however, causes segfault in HooksManager
+ // 1. call loadLibraries()
+ // 2. create AllocEngine (that register hook points)
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ vector<string> libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install pkt4_receive_callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_select", lease4_select_callout));
+
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ false, callout_handle,
+ old_lease_);
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // Do all checks on the lease
+ checkLease4(lease);
+
+ // Check that the lease is indeed in LeaseMgr
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check that callouts were indeed called
+ EXPECT_EQ("lease4_select", callback_name_);
+
+ // Now check that the lease in LeaseMgr has the same parameters
+ ASSERT_TRUE(callback_lease4_);
+ detailCompareLease(callback_lease4_, from_mgr);
+
+ ASSERT_TRUE(callback_subnet4_);
+ EXPECT_EQ(subnet_->toText(), callback_subnet4_->toText());
+
+ EXPECT_EQ(callback_fake_allocation_, false);
+
+ // Check if all expected parameters are reported. It's a bit tricky, because
+ // order may be different. If the test starts failing, because someone tweaked
+ // hooks engine, we'll have to implement proper vector matching (ignoring order)
+ vector<string> expected_argument_names;
+ expected_argument_names.push_back("fake_allocation");
+ expected_argument_names.push_back("lease4");
+ expected_argument_names.push_back("subnet4");
+ EXPECT_TRUE(callback_argument_names_ == expected_argument_names);
+}
+
+// This test checks if lease4_select callout is able to override the values
+// in a lease4.
+TEST_F(HookAllocEngine4Test, change_lease4_select) {
+
+ // Make sure that the overridden values are different than the ones from
+ // subnet originally used to create the lease
+ ASSERT_NE(t1_override_, subnet_->getT1());
+ ASSERT_NE(t2_override_, subnet_->getT2());
+ ASSERT_NE(valid_override_, subnet_->getValid());
+ ASSERT_FALSE(subnet_->inRange(addr_override_));
+
+ // Create allocation engine (hook names are registered in its ctor)
+ boost::scoped_ptr<AllocEngine> engine;
+ ASSERT_NO_THROW(engine.reset(new AllocEngine(AllocEngine::ALLOC_ITERATIVE,
+ 100, false)));
+ ASSERT_TRUE(engine);
+
+ // Initialize Hooks Manager
+ vector<string> libraries; // no libraries at this time
+ HooksManager::loadLibraries(libraries);
+
+ // Install a callout
+ EXPECT_NO_THROW(HooksManager::preCalloutsLibraryHandle().registerCallout(
+ "lease4_select", lease4_select_different_callout));
+
+ // Normally, dhcpv4_srv would passed the handle when calling allocateLease4,
+ // but in tests we need to create it on our own.
+ CalloutHandlePtr callout_handle = HooksManager::createCalloutHandle();
+
+ // Call allocateLease4. Callouts should be triggered here.
+ Lease4Ptr lease = engine->allocateLease4(subnet_, clientid_, hwaddr_,
+ IOAddress("0.0.0.0"),
+ false, false, "",
+ false, callout_handle,
+ old_lease_);
+ // Check that we got a lease
+ ASSERT_TRUE(lease);
+
+ // See if the values overridden by callout are there
+ EXPECT_TRUE(lease->addr_.equals(addr_override_));
+ EXPECT_EQ(t1_override_, lease->t1_);
+ EXPECT_EQ(t2_override_, lease->t2_);
+ EXPECT_EQ(valid_override_, lease->valid_lft_);
+
+ // Now check if the lease is in the database
+ Lease4Ptr from_mgr = LeaseMgrFactory::instance().getLease4(lease->addr_);
+ ASSERT_TRUE(from_mgr);
+
+ // Check if values in the database are overridden
+ EXPECT_TRUE(from_mgr->addr_.equals(addr_override_));
+ EXPECT_EQ(t1_override_, from_mgr->t1_);
+ EXPECT_EQ(t2_override_, from_mgr->t2_);
+ EXPECT_EQ(valid_override_, from_mgr->valid_lft_);
+}
+
}; // End of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc b/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc
new file mode 100644
index 0000000..71157c3
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/callout_handle_store_unittest.cc
@@ -0,0 +1,122 @@
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+
+#include <dhcp/dhcp4.h>
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt4.h>
+#include <dhcpsrv/callout_handle_store.h>
+#include "test_get_callout_handle.h"
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+
+namespace {
+
+TEST(CalloutHandleStoreTest, StoreRetrieve) {
+
+ // Create two DHCP4 packets during tests. The constructor arguments are
+ // arbitrary.
+ Pkt4Ptr pktptr_1(new Pkt4(DHCPDISCOVER, 1234));
+ Pkt4Ptr pktptr_2(new Pkt4(DHCPDISCOVER, 5678));
+
+ // Check that the pointers point to objects that are different, and that
+ // the pointers are the only pointers pointing to the packets.
+ ASSERT_TRUE(pktptr_1);
+ ASSERT_TRUE(pktptr_2);
+
+ ASSERT_TRUE(pktptr_1 != pktptr_2);
+ EXPECT_EQ(1, pktptr_1.use_count());
+ EXPECT_EQ(1, pktptr_2.use_count());
+
+ // Get the CalloutHandle for the first packet.
+ CalloutHandlePtr chptr_1 = getCalloutHandle(pktptr_1);
+ ASSERT_TRUE(chptr_1);
+
+ // Reference counts on both the callout handle and the packet should have
+ // been incremented because of the stored data. The reference count on the
+ // other Pkt4 object should not have changed.
+ EXPECT_EQ(2, chptr_1.use_count());
+ EXPECT_EQ(2, pktptr_1.use_count());
+ EXPECT_EQ(1, pktptr_2.use_count());
+
+ // Try getting another pointer for the same packet. Use a different
+ // pointer object to check that the function returns a handle based on the
+ // pointed-to data and not the pointer. (Clear the temporary pointer after
+ // use to avoid complicating reference counts.)
+ Pkt4Ptr pktptr_temp = pktptr_1;
+ CalloutHandlePtr chptr_2 = getCalloutHandle(pktptr_temp);
+ pktptr_temp.reset();
+
+ ASSERT_TRUE(chptr_2);
+ EXPECT_TRUE(chptr_1 == chptr_2);
+
+ // Reference count is now 3 on the callout handle - two for pointers here,
+ // one for the static pointer in the function. The count is 2 for the]
+ // object pointed to by pktptr_1 - one for that pointer and one for the
+ // pointer in the function.
+ EXPECT_EQ(3, chptr_1.use_count());
+ EXPECT_EQ(3, chptr_2.use_count());
+ EXPECT_EQ(2, pktptr_1.use_count());
+ EXPECT_EQ(1, pktptr_2.use_count());
+
+ // Now ask for a CalloutHandle for a different object. This should return
+ // a different CalloutHandle.
+ chptr_2 = getCalloutHandle(pktptr_2);
+ EXPECT_FALSE(chptr_1 == chptr_2);
+
+ // Check reference counts. The getCalloutHandle function should be storing
+ // pointers to the objects poiunted to by chptr_2 and pktptr_2.
+ EXPECT_EQ(1, chptr_1.use_count());
+ EXPECT_EQ(1, pktptr_1.use_count());
+ EXPECT_EQ(2, chptr_2.use_count());
+ EXPECT_EQ(2, pktptr_2.use_count());
+
+ // Now try clearing the stored pointers.
+ Pkt4Ptr pktptr_empty;
+ ASSERT_FALSE(pktptr_empty);
+
+ CalloutHandlePtr chptr_empty = getCalloutHandle(pktptr_empty);
+ EXPECT_FALSE(chptr_empty);
+
+ // Reference counts should be back to 1 for the CalloutHandles and the
+ // Packet pointers.
+ EXPECT_EQ(1, chptr_1.use_count());
+ EXPECT_EQ(1, pktptr_1.use_count());
+ EXPECT_EQ(1, chptr_2.use_count());
+ EXPECT_EQ(1, pktptr_2.use_count());
+}
+
+// The followings is a trival test to check that if the template function
+// is referred to in a separate compilation unit, only one copy of the static
+// objects stored in it are returned. (For a change, we'll use a Pkt6 as the
+// packet object.)
+
+TEST(CalloutHandleStoreTest, SeparateCompilationUnit) {
+
+ // Access the template function here.
+ Pkt6Ptr pktptr_1(new Pkt6(DHCPV6_ADVERTISE, 4321));
+ CalloutHandlePtr chptr_1 = getCalloutHandle(pktptr_1);
+ ASSERT_TRUE(chptr_1);
+
+ // Access it from within another compilation unit.
+ CalloutHandlePtr chptr_2 = isc::dhcp::test::testGetCalloutHandle(pktptr_1);
+ EXPECT_TRUE(chptr_1 == chptr_2);
+}
+
+} // Anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/callout_library.cc b/src/lib/dhcpsrv/tests/callout_library.cc
new file mode 100644
index 0000000..09da3cd
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/callout_library.cc
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Callout Library
+///
+/// This is the source of a test library for the basic DHCP parser
+/// tests. It just has to load - nothing else.
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+// Framework functions
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+};
diff --git a/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc b/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
index be31bab..8c3d2ea 100644
--- a/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
+++ b/src/lib/dhcpsrv/tests/cfgmgr_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -15,8 +15,9 @@
#include <config.h>
#include <dhcpsrv/cfgmgr.h>
-#include <dhcpsrv/dhcp_config_parser.h>
+#include <dhcpsrv/dhcp_parsers.h>
#include <exceptions/exceptions.h>
+#include <dhcp/dhcp6.h>
#include <gtest/gtest.h>
@@ -37,7 +38,7 @@ using boost::scoped_ptr;
namespace {
-// This test verifies that BooleanStorage functions properly.
+// This test verifies that BooleanStorage functions properly.
TEST(ValueStorageTest, BooleanTesting) {
BooleanStorage testStore;
@@ -48,7 +49,7 @@ TEST(ValueStorageTest, BooleanTesting) {
EXPECT_FALSE(testStore.getParam("firstBool"));
EXPECT_TRUE(testStore.getParam("secondBool"));
- // Verify that we can update paramaters.
+ // Verify that we can update parameters.
testStore.setParam("firstBool", true);
testStore.setParam("secondBool", false);
@@ -65,7 +66,7 @@ TEST(ValueStorageTest, BooleanTesting) {
// Verify that looking for a parameter that never existed throws.
ASSERT_THROW(testStore.getParam("bogusBool"), isc::dhcp::DhcpConfigError);
- // Verify that attempting to delete a parameter that never existed does not throw.
+ // Verify that attempting to delete a parameter that never existed does not throw.
EXPECT_NO_THROW(testStore.delParam("bogusBool"));
// Verify that we can empty the list.
@@ -74,21 +75,21 @@ TEST(ValueStorageTest, BooleanTesting) {
}
-// This test verifies that Uint32Storage functions properly.
+// This test verifies that Uint32Storage functions properly.
TEST(ValueStorageTest, Uint32Testing) {
Uint32Storage testStore;
uint32_t intOne = 77;
uint32_t intTwo = 33;
- // Verify that we can add and retrieve parameters.
+ // Verify that we can add and retrieve parameters.
testStore.setParam("firstInt", intOne);
testStore.setParam("secondInt", intTwo);
EXPECT_EQ(testStore.getParam("firstInt"), intOne);
EXPECT_EQ(testStore.getParam("secondInt"), intTwo);
- // Verify that we can update parameters.
+ // Verify that we can update parameters.
testStore.setParam("firstInt", --intOne);
testStore.setParam("secondInt", ++intTwo);
@@ -105,7 +106,7 @@ TEST(ValueStorageTest, Uint32Testing) {
// Verify that looking for a parameter that never existed throws.
ASSERT_THROW(testStore.getParam("bogusInt"), isc::dhcp::DhcpConfigError);
- // Verify that attempting to delete a parameter that never existed does not throw.
+ // Verify that attempting to delete a parameter that never existed does not throw.
EXPECT_NO_THROW(testStore.delParam("bogusInt"));
// Verify that we can empty the list.
@@ -113,7 +114,7 @@ TEST(ValueStorageTest, Uint32Testing) {
EXPECT_THROW(testStore.getParam("secondInt"), isc::dhcp::DhcpConfigError);
}
-// This test verifies that StringStorage functions properly.
+// This test verifies that StringStorage functions properly.
TEST(ValueStorageTest, StringTesting) {
StringStorage testStore;
@@ -127,7 +128,7 @@ TEST(ValueStorageTest, StringTesting) {
EXPECT_EQ(testStore.getParam("firstString"), stringOne);
EXPECT_EQ(testStore.getParam("secondString"), stringTwo);
- // Verify that we can update parameters.
+ // Verify that we can update parameters.
stringOne.append("-boo");
stringTwo.append("-boo");
@@ -147,7 +148,7 @@ TEST(ValueStorageTest, StringTesting) {
// Verify that looking for a parameter that never existed throws.
ASSERT_THROW(testStore.getParam("bogusString"), isc::dhcp::DhcpConfigError);
- // Verify that attempting to delete a parameter that never existed does not throw.
+ // Verify that attempting to delete a parameter that never existed does not throw.
EXPECT_NO_THROW(testStore.delParam("bogusString"));
// Verify that we can empty the list.
@@ -163,6 +164,18 @@ public:
// make sure we start with a clean configuration
CfgMgr::instance().deleteSubnets4();
CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().deleteOptionDefs();
+ CfgMgr::instance().deleteActiveIfaces();
+ }
+
+ /// @brief generates interface-id option based on provided text
+ ///
+ /// @param text content of the option to be created
+ ///
+ /// @return pointer to the option object created
+ OptionPtr generateInterfaceId(const string& text) {
+ OptionBuffer buffer(text.begin(), text.end());
+ return OptionPtr(new Option(Option::V6, D6O_INTERFACE_ID, buffer));
}
~CfgMgrTest() {
@@ -406,6 +419,95 @@ TEST_F(CfgMgrTest, subnet6) {
EXPECT_FALSE(cfg_mgr.getSubnet6(IOAddress("4000::123")));
}
+// This test verifies if the configuration manager is able to hold, select
+// and return valid subnets, based on interface names.
+TEST_F(CfgMgrTest, subnet6Interface) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+ subnet1->setIface("foo");
+ subnet2->setIface("bar");
+ subnet3->setIface("foobar");
+
+ // There shouldn't be any subnet configured at this stage
+ EXPECT_FALSE(cfg_mgr.getSubnet6("foo"));
+
+ cfg_mgr.addSubnet6(subnet1);
+
+ // Now we have only one subnet, any request will be served from it
+ EXPECT_EQ(subnet1, cfg_mgr.getSubnet6("foo"));
+
+ // Check that the interface name is checked even when there is
+ // only one subnet defined.
+ EXPECT_FALSE(cfg_mgr.getSubnet6("bar"));
+
+ // If we have only a single subnet and the request came from a local
+ // address, let's use that subnet
+ EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(IOAddress("fe80::dead:beef")));
+
+ cfg_mgr.addSubnet6(subnet2);
+ cfg_mgr.addSubnet6(subnet3);
+
+ EXPECT_EQ(subnet3, cfg_mgr.getSubnet6("foobar"));
+ EXPECT_EQ(subnet2, cfg_mgr.getSubnet6("bar"));
+ EXPECT_FALSE(cfg_mgr.getSubnet6("xyzzy")); // no such interface
+
+ // Check that deletion of the subnets works.
+ cfg_mgr.deleteSubnets6();
+ EXPECT_FALSE(cfg_mgr.getSubnet6("foo"));
+ EXPECT_FALSE(cfg_mgr.getSubnet6("bar"));
+ EXPECT_FALSE(cfg_mgr.getSubnet6("foobar"));
+}
+
+// This test verifies if the configuration manager is able to hold, select
+// and return valid leases, based on interface-id option values
+TEST_F(CfgMgrTest, subnet6InterfaceId) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ Subnet6Ptr subnet1(new Subnet6(IOAddress("2000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet2(new Subnet6(IOAddress("3000::"), 48, 1, 2, 3, 4));
+ Subnet6Ptr subnet3(new Subnet6(IOAddress("4000::"), 48, 1, 2, 3, 4));
+
+ // interface-id options used in subnets 1,2, and 3
+ OptionPtr ifaceid1 = generateInterfaceId("relay1.eth0");
+ OptionPtr ifaceid2 = generateInterfaceId("VL32");
+ // That's a strange interface-id, but this is a real life example
+ OptionPtr ifaceid3 = generateInterfaceId("ISAM144|299|ipv6|nt:vp:1:110");
+
+ // bogus interface-id
+ OptionPtr ifaceid_bogus = generateInterfaceId("non-existent");
+
+ subnet1->setInterfaceId(ifaceid1);
+ subnet2->setInterfaceId(ifaceid2);
+ subnet3->setInterfaceId(ifaceid3);
+
+ // There shouldn't be any subnet configured at this stage
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid1));
+
+ cfg_mgr.addSubnet6(subnet1);
+
+ // If we have only a single subnet and the request came from a local
+ // address, let's use that subnet
+ EXPECT_EQ(subnet1, cfg_mgr.getSubnet6(ifaceid1));
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid2));
+
+ cfg_mgr.addSubnet6(subnet2);
+ cfg_mgr.addSubnet6(subnet3);
+
+ EXPECT_EQ(subnet3, cfg_mgr.getSubnet6(ifaceid3));
+ EXPECT_EQ(subnet2, cfg_mgr.getSubnet6(ifaceid2));
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid_bogus));
+
+ // Check that deletion of the subnets works.
+ cfg_mgr.deleteSubnets6();
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid1));
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid2));
+ EXPECT_FALSE(cfg_mgr.getSubnet6(ifaceid3));
+}
+
+
// This test verifies that new DHCPv4 option spaces can be added to
// the configuration manager and that duplicated option space is
// rejected.
@@ -436,7 +538,7 @@ TEST_F(CfgMgrTest, optionSpace4) {
cfg_mgr.addOptionSpace4(space3), isc::dhcp::InvalidOptionSpace
);
- // @todo decode if a duplicate vendor space is allowed.
+ /// @todo decode if a duplicate vendor space is allowed.
}
// This test verifies that new DHCPv6 option spaces can be added to
@@ -469,9 +571,166 @@ TEST_F(CfgMgrTest, optionSpace6) {
cfg_mgr.addOptionSpace6(space3), isc::dhcp::InvalidOptionSpace
);
- // @todo decide if a duplicate vendor space is allowed.
+ /// @todo decide if a duplicate vendor space is allowed.
}
+// This test verifies that it is possible to specify interfaces that server
+// should listen on.
+TEST_F(CfgMgrTest, addActiveIface) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth0"));
+ EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth1"));
+
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+ EXPECT_NO_THROW(cfg_mgr.deleteActiveIfaces());
+
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+}
+
+
+// This test verifies that it is possible to specify interfaces that server
+// should listen on.
+TEST_F(CfgMgrTest, addUnicastAddresses) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth1/2001:db8::1"));
+ EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth2/2001:db8::2"));
+ EXPECT_NO_THROW(cfg_mgr.addActiveIface("eth3"));
+
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth3"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth4"));
+
+ ASSERT_TRUE(cfg_mgr.getUnicast("eth1"));
+ EXPECT_EQ("2001:db8::1", cfg_mgr.getUnicast("eth1")->toText());
+ EXPECT_EQ("2001:db8::2", cfg_mgr.getUnicast("eth2")->toText());
+ EXPECT_FALSE(cfg_mgr.getUnicast("eth3"));
+ EXPECT_FALSE(cfg_mgr.getUnicast("eth4"));
+
+ EXPECT_NO_THROW(cfg_mgr.deleteActiveIfaces());
+
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth3"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth4"));
+
+ ASSERT_FALSE(cfg_mgr.getUnicast("eth1"));
+ ASSERT_FALSE(cfg_mgr.getUnicast("eth2"));
+ EXPECT_FALSE(cfg_mgr.getUnicast("eth3"));
+ EXPECT_FALSE(cfg_mgr.getUnicast("eth4"));
+}
+
+
+// This test verifies that it is possible to set the flag which configures the
+// server to listen on all interfaces.
+TEST_F(CfgMgrTest, activateAllIfaces) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ cfg_mgr.addActiveIface("eth0");
+ cfg_mgr.addActiveIface("eth1");
+
+ ASSERT_TRUE(cfg_mgr.isActiveIface("eth0"));
+ ASSERT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ ASSERT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+ cfg_mgr.activateAllIfaces();
+
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
+
+ cfg_mgr.deleteActiveIfaces();
+
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+}
+
+// This test verifies that RFC6842 (echo client-id) compatibility may be
+// configured.
+TEST_F(CfgMgrTest, echoClientId) {
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+
+ // Check that the default is true
+ EXPECT_TRUE(cfg_mgr.echoClientId());
+
+ // Check that it can be modified to false
+ cfg_mgr.echoClientId(false);
+ EXPECT_FALSE(cfg_mgr.echoClientId());
+
+ // Check that the default value can be restored
+ cfg_mgr.echoClientId(true);
+ EXPECT_TRUE(cfg_mgr.echoClientId());
+}
+
+// This test checks the D2ClientMgr wrapper methods.
+TEST_F(CfgMgrTest, d2ClientConfig) {
+ // After CfgMgr construction, D2 configuration should be disabled.
+ // Fetch it and verify this is the case.
+ D2ClientConfigPtr original_config = CfgMgr::instance().getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+ EXPECT_FALSE(original_config->getEnableUpdates());
+
+ // Make sure convenience method agrees.
+ EXPECT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ // Verify that we cannot set the configuration to an empty pointer.
+ D2ClientConfigPtr new_cfg;
+ ASSERT_THROW(CfgMgr::instance().setD2ClientConfig(new_cfg), D2ClientError);
+
+ // Create a new, enabled configuration.
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ isc::asiolink::IOAddress("127.0.0.1"), 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+
+ // Verify that we can assign a new, non-empty configuration.
+ ASSERT_NO_THROW(CfgMgr::instance().setD2ClientConfig(new_cfg));
+
+ // Verify that we can fetch the newly assigned configuration.
+ D2ClientConfigPtr updated_config = CfgMgr::instance().getD2ClientConfig();
+ ASSERT_TRUE(updated_config);
+ EXPECT_TRUE(updated_config->getEnableUpdates());
+
+ // Make sure convenience method agrees with updated configuration.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+
+ // Make sure the configuration we fetched is the one we assigned,
+ // and not the original configuration.
+ EXPECT_EQ(*new_cfg, *updated_config);
+ EXPECT_NE(*original_config, *updated_config);
+}
+
+
+/// @todo Add unit-tests for testing:
+/// - addActiveIface() with invalid interface name
+/// - addActiveIface() with the same interface twice
+/// - addActiveIface() with a bogus address
+///
+/// This is somewhat tricky. Care should be taken here, because it is rather
+/// difficult to decide if interface name is valid or not. Some servers, e.g.
+/// dibbler, allow to specify interface names that are not currently present in
+/// the system. The server accepts them, but upon discovering that they are
+/// yet available (for different definitions of not being available), adds
+/// the to to-be-activated list.
+///
+/// Cases covered by dibbler are:
+/// - missing interface (e.g. PPP connection that is not established yet)
+/// - downed interface (no link local address, no way to open sockets)
+/// - up, but not running interface (wifi up, but not associated)
+/// - tentative addresses (interface up and running, but DAD procedure is
+/// still in progress)
+/// - weird interfaces without link-local addresses (don't ask, 6rd tunnels
+/// look weird to me as well)
+
// No specific tests for getSubnet6. That method (2 overloaded versions) is tested
// in Dhcpv6SrvTest.selectSubnetAddr and Dhcpv6SrvTest.selectSubnetIface
// (see src/bin/dhcp6/tests/dhcp6_srv_unittest.cc)
diff --git a/src/lib/dhcpsrv/tests/d2_client_unittest.cc b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
new file mode 100644
index 0000000..bd8f061
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
@@ -0,0 +1,294 @@
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcpsrv/d2_client.h>
+#include <exceptions/exceptions.h>
+
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc::asiolink;
+using namespace isc::dhcp;
+using namespace isc::util;
+using namespace isc;
+
+namespace {
+
+/// @brief Checks constructors and accessors of D2ClientConfig.
+TEST(D2ClientConfigTest, constructorsAndAccessors) {
+ D2ClientConfigPtr d2_client_config;
+
+ // Verify default constructor creates a disabled instance.
+ ASSERT_NO_THROW(d2_client_config.reset(new D2ClientConfig()));
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+
+ d2_client_config.reset();
+
+ bool enable_updates = true;
+ isc::asiolink::IOAddress server_ip("127.0.0.1");
+ size_t server_port = 477;
+ dhcp_ddns::NameChangeProtocol ncr_protocol = dhcp_ddns::NCR_UDP;
+ dhcp_ddns::NameChangeFormat ncr_format = dhcp_ddns::FMT_JSON;
+ bool remove_on_renew = true;
+ bool always_include_fqdn = true;
+ bool override_no_update = true;
+ bool override_client_update = true;
+ bool replace_client_name = true;
+ std::string generated_prefix = "the_prefix";
+ std::string qualifying_suffix = "the.suffix.";
+
+ // Verify that we can construct a valid, enabled instance.
+ ASSERT_NO_THROW(d2_client_config.reset(new
+ D2ClientConfig(enable_updates,
+ server_ip,
+ server_port,
+ ncr_protocol,
+ ncr_format,
+ remove_on_renew,
+ always_include_fqdn,
+ override_no_update,
+ override_client_update,
+ replace_client_name,
+ generated_prefix,
+ qualifying_suffix)));
+
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the accessors return the expected values.
+ EXPECT_EQ(d2_client_config->getEnableUpdates(), enable_updates);
+
+ EXPECT_EQ(d2_client_config->getServerIp(), server_ip);
+ EXPECT_EQ(d2_client_config->getServerPort(), server_port);
+ EXPECT_EQ(d2_client_config->getNcrProtocol(), ncr_protocol);
+ EXPECT_EQ(d2_client_config->getNcrFormat(), ncr_format);
+ EXPECT_EQ(d2_client_config->getRemoveOnRenew(), remove_on_renew);
+ EXPECT_EQ(d2_client_config->getAlwaysIncludeFqdn(), always_include_fqdn);
+ EXPECT_EQ(d2_client_config->getOverrideNoUpdate(), override_no_update);
+ EXPECT_EQ(d2_client_config->getOverrideClientUpdate(),
+ override_client_update);
+ EXPECT_EQ(d2_client_config->getReplaceClientName(), replace_client_name);
+ EXPECT_EQ(d2_client_config->getGeneratedPrefix(), generated_prefix);
+ EXPECT_EQ(d2_client_config->getQualifyingSuffix(), qualifying_suffix);
+
+ // Verify that toText called by << operator doesn't bomb.
+ ASSERT_NO_THROW(std::cout << "toText test:" << std::endl <<
+ *d2_client_config << std::endl);
+
+ // Verify that constructor does not allow use of NCR_TCP.
+ /// @todo obviously this becomes invalid once TCP is supported.
+ ASSERT_THROW(d2_client_config.reset(new
+ D2ClientConfig(enable_updates,
+ server_ip,
+ server_port,
+ dhcp_ddns::NCR_TCP,
+ ncr_format,
+ remove_on_renew,
+ always_include_fqdn,
+ override_no_update,
+ override_client_update,
+ replace_client_name,
+ generated_prefix,
+ qualifying_suffix)),
+ D2ClientError);
+
+ /// @todo if additional validation is added to ctor, this test needs to
+ /// expand accordingly.
+}
+
+/// @brief Tests the equality and inequality operators of D2ClientConfig.
+TEST(D2ClientConfigTest, equalityOperator) {
+ D2ClientConfigPtr ref_config;
+ D2ClientConfigPtr test_config;
+
+ isc::asiolink::IOAddress ref_address("127.0.0.1");
+ isc::asiolink::IOAddress test_address("127.0.0.2");
+
+ // Create an instance to use as a reference.
+ ASSERT_NO_THROW(ref_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(ref_config);
+
+ // Check a configuration that is identical to reference configuration.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_TRUE(*ref_config == *test_config);
+ EXPECT_FALSE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by enable flag.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(false,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by server ip.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ test_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by server port.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 333,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by remove_on_renew.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ false, true, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by always_include_fqdn.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, false, true, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by override_no_update.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, false, true, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by override_client_update.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, false, true,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by replace_client_name.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, false,
+ "pre-fix", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by generated_prefix.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "bogus", "suf-fix")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+
+ // Check a configuration that differs only by qualifying_suffix.
+ ASSERT_NO_THROW(test_config.reset(new D2ClientConfig(true,
+ ref_address, 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "bogus")));
+ ASSERT_TRUE(test_config);
+ EXPECT_FALSE(*ref_config == *test_config);
+ EXPECT_TRUE(*ref_config != *test_config);
+}
+
+/// @brief Checks the D2ClientMgr constructor.
+TEST(D2ClientMgr, constructor) {
+ D2ClientMgrPtr d2_client_mgr;
+
+ // Verify we can construct with the default constructor.
+ ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr()));
+
+ // After construction, D2 configuration should be disabled.
+ // Fetch it and verify this is the case.
+ D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+ EXPECT_FALSE(original_config->getEnableUpdates());
+
+ // Make sure convenience method agrees.
+ EXPECT_FALSE(d2_client_mgr->ddnsEnabled());
+}
+
+/// @brief Checks passing the D2ClientMgr a valid D2 client configuration.
+/// @todo Once NameChangeSender is integrated, this test needs to expand, and
+/// additional scenario tests will need to be written.
+TEST(D2ClientMgr, validConfig) {
+ D2ClientMgrPtr d2_client_mgr;
+
+ // Construct the manager and fetch its initial configuration.
+ ASSERT_NO_THROW(d2_client_mgr.reset(new D2ClientMgr()));
+ D2ClientConfigPtr original_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(original_config);
+
+ // Verify that we cannot set the config to an empty pointer.
+ D2ClientConfigPtr new_cfg;
+ ASSERT_THROW(d2_client_mgr->setD2ClientConfig(new_cfg), D2ClientError);
+
+ // Create a new, enabled config.
+ ASSERT_NO_THROW(new_cfg.reset(new D2ClientConfig(true,
+ isc::asiolink::IOAddress("127.0.0.1"), 477,
+ dhcp_ddns::NCR_UDP, dhcp_ddns::FMT_JSON,
+ true, true, true, true, true,
+ "pre-fix", "suf-fix")));
+
+ // Verify that we can assign a new, non-empty configuration.
+ ASSERT_NO_THROW(d2_client_mgr->setD2ClientConfig(new_cfg));
+
+ // Verify that we can fetch the newly assigned configuration.
+ D2ClientConfigPtr updated_config = d2_client_mgr->getD2ClientConfig();
+ ASSERT_TRUE(updated_config);
+ EXPECT_TRUE(updated_config->getEnableUpdates());
+
+ // Make sure convenience method agrees with the updated configuration.
+ EXPECT_TRUE(d2_client_mgr->ddnsEnabled());
+
+ // Make sure the configuration we fetched is the one we assigned,
+ // and not the original configuration.
+ EXPECT_EQ(*new_cfg, *updated_config);
+ EXPECT_NE(*original_config, *updated_config);
+}
+
+
+} // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
new file mode 100644
index 0000000..2e6adb9
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
@@ -0,0 +1,1269 @@
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <config/ccsession.h>
+#include <dhcp/option.h>
+#include <dhcp/option_custom.h>
+#include <dhcp/option_int.h>
+#include <dhcpsrv/cfgmgr.h>
+#include <dhcpsrv/subnet.h>
+#include <dhcpsrv/dhcp_parsers.h>
+#include <dhcpsrv/tests/test_libraries.h>
+#include <exceptions/exceptions.h>
+#include <hooks/hooks_manager.h>
+
+#include <gtest/gtest.h>
+#include <boost/foreach.hpp>
+#include <boost/pointer_cast.hpp>
+
+#include <map>
+#include <string>
+
+using namespace std;
+using namespace isc;
+using namespace isc::config;
+using namespace isc::data;
+using namespace isc::dhcp;
+using namespace isc::hooks;
+
+namespace {
+
+/// @brief DHCP Parser test fixture class
+class DhcpParserTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ///
+ DhcpParserTest() {
+ CfgMgr::instance().deleteActiveIfaces();
+ }
+};
+
+
+/// @brief Check BooleanParser basic functionality.
+///
+/// Verifies that the parser:
+/// 1. Does not allow empty for storage.
+/// 2. Rejects a non-boolean element.
+/// 3. Builds with a valid true value.
+/// 4. Bbuils with a valid false value.
+/// 5. Updates storage upon commit.
+TEST_F(DhcpParserTest, booleanParserTest) {
+
+ const std::string name = "boolParm";
+
+ // Verify that parser does not allow empty for storage.
+ BooleanStoragePtr bs;
+ EXPECT_THROW(BooleanParser(name, bs), isc::dhcp::DhcpConfigError);
+
+ // Construct parser for testing.
+ BooleanStoragePtr storage(new BooleanStorage());
+ BooleanParser parser(name, storage);
+
+ // Verify that parser with rejects a non-boolean element.
+ ElementPtr wrong_element = Element::create("I am a string");
+ EXPECT_THROW(parser.build(wrong_element), isc::BadValue);
+
+ // Verify that parser will build with a valid true value.
+ bool test_value = true;
+ ElementPtr element = Element::create(test_value);
+ ASSERT_NO_THROW(parser.build(element));
+
+ // Verify that commit updates storage.
+ bool actual_value = !test_value;
+ parser.commit();
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ(test_value, actual_value);
+
+ // Verify that parser will build with a valid false value.
+ test_value = false;
+ element->setValue(test_value);
+ EXPECT_NO_THROW(parser.build(element));
+
+ // Verify that commit updates storage.
+ actual_value = ~test_value;
+ parser.commit();
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ(test_value, actual_value);
+}
+
+/// @brief Check StringParser basic functionality
+///
+/// Verifies that the parser:
+/// 1. Does not allow empty for storage.
+/// 2. Builds with a nont string value.
+/// 3. Builds with a string value.
+/// 4. Updates storage upon commit.
+TEST_F(DhcpParserTest, stringParserTest) {
+
+ const std::string name = "strParm";
+
+ // Verify that parser does not allow empty for storage.
+ StringStoragePtr bs;
+ EXPECT_THROW(StringParser(name, bs), isc::dhcp::DhcpConfigError);
+
+ // Construct parser for testing.
+ StringStoragePtr storage(new StringStorage());
+ StringParser parser(name, storage);
+
+ // Verify that parser with accepts a non-string element.
+ ElementPtr element = Element::create(9999);
+ EXPECT_NO_THROW(parser.build(element));
+
+ // Verify that commit updates storage.
+ parser.commit();
+ std::string actual_value;
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ("9999", actual_value);
+
+ // Verify that parser will build with a string value.
+ const std::string test_value = "test value";
+ element = Element::create(test_value);
+ ASSERT_NO_THROW(parser.build(element));
+
+ // Verify that commit updates storage.
+ parser.commit();
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ(test_value, actual_value);
+}
+
+/// @brief Check Uint32Parser basic functionality
+///
+/// Verifies that the parser:
+/// 1. Does not allow empty for storage.
+/// 2. Rejects a non-integer element.
+/// 3. Rejects a negative value.
+/// 4. Rejects too large a value.
+/// 5. Builds with value of zero.
+/// 6. Builds with a value greater than zero.
+/// 7. Updates storage upon commit.
+TEST_F(DhcpParserTest, uint32ParserTest) {
+
+ const std::string name = "intParm";
+
+ // Verify that parser does not allow empty for storage.
+ Uint32StoragePtr bs;
+ EXPECT_THROW(Uint32Parser(name, bs), isc::dhcp::DhcpConfigError);
+
+ // Construct parser for testing.
+ Uint32StoragePtr storage(new Uint32Storage());
+ Uint32Parser parser(name, storage);
+
+ // Verify that parser with rejects a non-interger element.
+ ElementPtr wrong_element = Element::create("I am a string");
+ EXPECT_THROW(parser.build(wrong_element), isc::BadValue);
+
+ // Verify that parser with rejects a negative value.
+ ElementPtr int_element = Element::create(-1);
+ EXPECT_THROW(parser.build(int_element), isc::BadValue);
+
+ // Verify that parser with rejects too large a value provided we are on
+ // 64-bit platform.
+ if (sizeof(long) > sizeof(uint32_t)) {
+ long max = (long)(std::numeric_limits<uint32_t>::max()) + 1;
+ int_element->setValue(max);
+ EXPECT_THROW(parser.build(int_element), isc::BadValue);
+ }
+
+ // Verify that parser will build with value of zero.
+ int test_value = 0;
+ int_element->setValue((long)test_value);
+ ASSERT_NO_THROW(parser.build(int_element));
+
+ // Verify that commit updates storage.
+ parser.commit();
+ uint32_t actual_value = 0;
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ(test_value, actual_value);
+
+ // Verify that parser will build with a valid positive value.
+ test_value = 77;
+ int_element->setValue((long)test_value);
+ ASSERT_NO_THROW(parser.build(int_element));
+
+ // Verify that commit updates storage.
+ parser.commit();
+ EXPECT_NO_THROW((actual_value = storage->getParam(name)));
+ EXPECT_EQ(test_value, actual_value);
+}
+
+/// @brief Check InterfaceListParser basic functionality
+///
+/// Verifies that the parser:
+/// 1. Does not allow empty for storage.
+/// 2. Does not allow name other than "interfaces"
+/// 3. Parses list of interfaces and adds them to CfgMgr
+/// 4. Parses wildcard interface name and sets a CfgMgr flag which indicates
+/// that server will listen on all interfaces.
+TEST_F(DhcpParserTest, interfaceListParserTest) {
+
+ const std::string name = "interfaces";
+
+ // Verify that parser constructor fails if parameter name isn't "interface"
+ EXPECT_THROW(InterfaceListConfigParser("bogus_name"), isc::BadValue);
+
+ boost::scoped_ptr<InterfaceListConfigParser>
+ parser(new InterfaceListConfigParser(name));
+ ElementPtr list_element = Element::createList();
+ list_element->add(Element::create("eth0"));
+ list_element->add(Element::create("eth1"));
+
+ // Make sure there are no interfaces added yet.
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth0"));
+ ASSERT_FALSE(CfgMgr::instance().isActiveIface("eth1"));
+
+ // This should parse the configuration and add eth0 and eth1 to the list
+ // of interfaces that server should listen on.
+ parser->build(list_element);
+ parser->commit();
+
+ // Use CfgMgr instance to check if eth0 and eth1 was added, and that
+ // eth2 was not added.
+ CfgMgr& cfg_mgr = CfgMgr::instance();
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_FALSE(cfg_mgr.isActiveIface("eth2"));
+
+ // Add keyword all to the configuration. This should activate all
+ // interfaces, including eth2, even though it has not been explicitly
+ // added.
+ list_element->add(Element::create("*"));
+
+ // Reset parser's state.
+ parser.reset(new InterfaceListConfigParser(name));
+ parser->build(list_element);
+ parser->commit();
+
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth0"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth1"));
+ EXPECT_TRUE(cfg_mgr.isActiveIface("eth2"));
+}
+
+// Checks whether option space can be detected as vendor-id
+TEST_F(DhcpParserTest, vendorOptionSpace) {
+ EXPECT_EQ(0, SubnetConfigParser::optionSpaceToVendorId(""));
+ EXPECT_EQ(0, SubnetConfigParser::optionSpaceToVendorId("dhcp4"));
+ EXPECT_EQ(0, SubnetConfigParser::optionSpaceToVendorId("vendor-"));
+ EXPECT_EQ(1, SubnetConfigParser::optionSpaceToVendorId("vendor-1"));
+ EXPECT_EQ(4491, SubnetConfigParser::optionSpaceToVendorId("vendor-4491"));
+ EXPECT_EQ(12345678, SubnetConfigParser::optionSpaceToVendorId("vendor-12345678"));
+}
+
+/// @brief Test Implementation of abstract OptionDataParser class. Allows
+/// testing basic option parsing.
+class UtestOptionDataParser : public OptionDataParser {
+public:
+
+ UtestOptionDataParser(const std::string&,
+ OptionStoragePtr options, ParserContextPtr global_context)
+ :OptionDataParser("", options, global_context) {
+ }
+
+ static OptionDataParser* factory(const std::string& param_name,
+ OptionStoragePtr options, ParserContextPtr global_context) {
+ return (new UtestOptionDataParser(param_name, options, global_context));
+ }
+
+protected:
+ // Dummy out last two params since test derivation doesn't use them.
+ virtual OptionDefinitionPtr findServerSpaceOptionDefinition (
+ std::string&, uint32_t) {
+ OptionDefinitionPtr def;
+ // always return empty
+ return (def);
+ }
+};
+
+/// @brief Test Fixture class which provides basic structure for testing
+/// configuration parsing. This is essentially the same structure provided
+/// by dhcp servers.
+class ParseConfigTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ParseConfigTest() {
+ reset_context();
+ }
+
+ ~ParseConfigTest() {
+ reset_context();
+ }
+
+ /// @brief Parses a configuration.
+ ///
+ /// Parse the given configuration, populating the context storage with
+ /// the parsed elements.
+ ///
+ /// @param config_set is the set of elements to parse.
+ /// @return returns an ConstElementPtr containing the numeric result
+ /// code and outcome comment.
+ isc::data::ConstElementPtr parseElementSet(isc::data::ConstElementPtr
+ config_set) {
+ // Answer will hold the result.
+ ConstElementPtr answer;
+ if (!config_set) {
+ answer = isc::config::createAnswer(1,
+ string("Can't parse NULL config"));
+ return (answer);
+ }
+
+ // option parsing must be done last, so save it if we hit if first
+ ParserPtr option_parser;
+
+ ConfigPair config_pair;
+ try {
+ // Iterate over the config elements.
+ const std::map<std::string, ConstElementPtr>& values_map =
+ config_set->mapValue();
+ BOOST_FOREACH(config_pair, values_map) {
+ // Create the parser based on element name.
+ ParserPtr parser(createConfigParser(config_pair.first));
+ // Options must be parsed last
+ if (config_pair.first == "option-data") {
+ option_parser = parser;
+ } else {
+ // Anything else we can call build straight away.
+ parser->build(config_pair.second);
+ parser->commit();
+ }
+ }
+
+ // The option values parser is the next one to be run.
+ std::map<std::string, ConstElementPtr>::const_iterator
+ option_config = values_map.find("option-data");
+ if (option_config != values_map.end()) {
+ option_parser->build(option_config->second);
+ option_parser->commit();
+ }
+
+ // Everything was fine. Configuration is successful.
+ answer = isc::config::createAnswer(0, "Configuration committed.");
+ } catch (const isc::Exception& ex) {
+ answer = isc::config::createAnswer(1,
+ string("Configuration parsing failed: ") + ex.what());
+
+ } catch (...) {
+ answer = isc::config::createAnswer(1,
+ string("Configuration parsing failed"));
+ }
+
+ return (answer);
+ }
+
+ /// @brief Create an element parser based on the element name.
+ ///
+ /// Creates a parser for the appropriate element and stores a pointer to it
+ /// in the appropriate class variable.
+ ///
+ /// Note that the method currently it only supports option-defs, option-data
+ /// and hooks-libraries.
+ ///
+ /// @param config_id is the name of the configuration element.
+ ///
+ /// @return returns a shared pointer to DhcpConfigParser.
+ ///
+ /// @throw throws NotImplemented if element name isn't supported.
+ ParserPtr createConfigParser(const std::string& config_id) {
+ ParserPtr parser;
+ if (config_id.compare("option-data") == 0) {
+ parser.reset(new OptionDataListParser(config_id,
+ parser_context_->options_,
+ parser_context_,
+ UtestOptionDataParser::factory));
+
+ } else if (config_id.compare("option-def") == 0) {
+ parser.reset(new OptionDefListParser(config_id,
+ parser_context_->option_defs_));
+
+ } else if (config_id.compare("hooks-libraries") == 0) {
+ parser.reset(new HooksLibrariesParser(config_id));
+ hooks_libraries_parser_ =
+ boost::dynamic_pointer_cast<HooksLibrariesParser>(parser);
+ } else if (config_id.compare("dhcp-ddns") == 0) {
+ parser.reset(new D2ClientConfigParser(config_id));
+ } else {
+ isc_throw(NotImplemented,
+ "Parser error: configuration parameter not supported: "
+ << config_id);
+ }
+
+ return (parser);
+ }
+
+ /// @brief Convenience method for parsing a configuration
+ ///
+ /// Given a configuration string, convert it into Elements
+ /// and parse them.
+ /// @param config is the configuration string to parse
+ ///
+ /// @return retuns 0 if the configuration parsed successfully,
+ /// non-zero otherwise failure.
+ int parseConfiguration(const std::string& config) {
+ int rcode_ = 1;
+ // Turn config into elements.
+ // Test json just to make sure its valid.
+ ElementPtr json = Element::fromJSON(config);
+ EXPECT_TRUE(json);
+ if (json) {
+ ConstElementPtr status = parseElementSet(json);
+ ConstElementPtr comment = parseAnswer(rcode_, status);
+ error_text_ = comment->stringValue();
+ }
+
+ return (rcode_);
+ }
+
+ /// @brief Find an option definition for a given space and code within
+ /// the parser context.
+ /// @param space is the space name of the desired option.
+ /// @param code is the numeric "type" of the desired option.
+ /// @return returns an OptionDefinitionPtr which points to the found
+ /// definition or is empty.
+ /// ASSERT_ tests don't work inside functions that return values
+ OptionDefinitionPtr getOptionDef(std::string space, uint32_t code)
+ {
+ OptionDefinitionPtr def;
+ OptionDefContainerPtr defs =
+ parser_context_->option_defs_->getItems(space);
+ // Should always be able to get definitions list even if it is empty.
+ EXPECT_TRUE(defs);
+ if (defs) {
+ // Attempt to find desired definiton.
+ const OptionDefContainerTypeIndex& idx = defs->get<1>();
+ const OptionDefContainerTypeRange& range = idx.equal_range(code);
+ int cnt = std::distance(range.first, range.second);
+ EXPECT_EQ(1, cnt);
+ if (cnt == 1) {
+ def = *(idx.begin());
+ }
+ }
+ return (def);
+ }
+
+ /// @brief Find an option for a given space and code within the parser
+ /// context.
+ /// @param space is the space name of the desired option.
+ /// @param code is the numeric "type" of the desired option.
+ /// @return returns an OptionPtr which points to the found
+ /// option or is empty.
+ /// ASSERT_ tests don't work inside functions that return values
+ OptionPtr getOptionPtr(std::string space, uint32_t code)
+ {
+ OptionPtr option_ptr;
+ Subnet::OptionContainerPtr options =
+ parser_context_->options_->getItems(space);
+ // Should always be able to get options list even if it is empty.
+ EXPECT_TRUE(options);
+ if (options) {
+ // Attempt to find desired option.
+ const Subnet::OptionContainerTypeIndex& idx = options->get<1>();
+ const Subnet::OptionContainerTypeRange& range =
+ idx.equal_range(code);
+ int cnt = std::distance(range.first, range.second);
+ EXPECT_EQ(1, cnt);
+ if (cnt == 1) {
+ Subnet::OptionDescriptor desc = *(idx.begin());
+ option_ptr = desc.option;
+ EXPECT_TRUE(option_ptr);
+ }
+ }
+
+ return (option_ptr);
+ }
+
+ /// @brief Wipes the contents of the context to allowing another parsing
+ /// during a given test if needed.
+ void reset_context(){
+ // Note set context universe to V6 as it has to be something.
+ CfgMgr::instance().deleteSubnets4();
+ CfgMgr::instance().deleteSubnets6();
+ CfgMgr::instance().deleteOptionDefs();
+ parser_context_.reset(new ParserContext(Option::V6));
+
+ // Ensure no hooks libraries are loaded.
+ HooksManager::unloadLibraries();
+
+ // Set it to minimal, disabled config
+ D2ClientConfigPtr tmp(new D2ClientConfig());
+ CfgMgr::instance().setD2ClientConfig(tmp);
+ }
+
+ /// @brief Parsers used in the parsing of the configuration
+ ///
+ /// Allows the tests to interrogate the state of the parsers (if required).
+ boost::shared_ptr<HooksLibrariesParser> hooks_libraries_parser_;
+
+ /// @brief Parser context - provides storage for options and definitions
+ ParserContextPtr parser_context_;
+
+ /// @brief Error string if the parsing failed
+ std::string error_text_;
+};
+
+/// @brief Check Basic parsing of option definitions.
+///
+/// Note that this tests basic operation of the OptionDefinitionListParser and
+/// OptionDefinitionParser. It uses a simple configuration consisting of one
+/// one definition and verifies that it is parsed and committed to storage
+/// correctly.
+TEST_F(ParseConfigTest, basicOptionDefTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"\""
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0);
+
+
+ // Verify that the option definition can be retrieved.
+ OptionDefinitionPtr def = getOptionDef("isc", 100);
+ ASSERT_TRUE(def);
+
+ // Verify that the option definition is correct.
+ EXPECT_EQ("foo", def->getName());
+ EXPECT_EQ(100, def->getCode());
+ EXPECT_FALSE(def->getArrayType());
+ EXPECT_EQ(OPT_IPV4_ADDRESS_TYPE, def->getType());
+ EXPECT_TRUE(def->getEncapsulatedSpace().empty());
+}
+
+/// @brief Check Basic parsing of options.
+///
+/// Note that this tests basic operation of the OptionDataListParser and
+/// OptionDataParser. It uses a simple configuration consisting of one
+/// one definition and matching option data. It verifies that the option
+/// is parsed and committed to storage correctly.
+TEST_F(ParseConfigTest, basicOptionDataTest) {
+
+ // Configuration string.
+ std::string config =
+ "{ \"option-def\": [ {"
+ " \"name\": \"foo\","
+ " \"code\": 100,"
+ " \"type\": \"ipv4-address\","
+ " \"array\": False,"
+ " \"record-types\": \"\","
+ " \"space\": \"isc\","
+ " \"encapsulate\": \"\""
+ " } ], "
+ " \"option-data\": [ {"
+ " \"name\": \"foo\","
+ " \"space\": \"isc\","
+ " \"code\": 100,"
+ " \"data\": \"192.0.2.0\","
+ " \"csv-format\": True"
+ " } ]"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0);
+
+ // Verify that the option can be retrieved.
+ OptionPtr opt_ptr = getOptionPtr("isc", 100);
+ ASSERT_TRUE(opt_ptr);
+
+ // Verify that the option definition is correct.
+ std::string val = "type=100, len=4, data fields:\n "
+ " #0 192.0.2.0 ( ipv4-address ) \n";
+
+ EXPECT_EQ(val, opt_ptr->toText());
+}
+
+}; // Anonymous namespace
+
+/// These tests check basic operation of the HooksLibrariesParser.
+
+// hooks-libraries that do not contain anything.
+TEST_F(ParseConfigTest, noHooksLibrariesTest) {
+
+ // Configuration with hooks-libraries not present.
+ string config = "{ \"hooks-libraries\": [] }";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Check that the parser recorded no change to the current state
+ // (as the test starts with no hooks libraries loaded).
+ std::vector<std::string> libraries;
+ bool changed;
+ hooks_libraries_parser_->getLibraries(libraries, changed);
+ EXPECT_TRUE(libraries.empty());
+ EXPECT_FALSE(changed);
+
+ // Load a single library and repeat the parse.
+ vector<string> basic_library;
+ basic_library.push_back(string(CALLOUT_LIBRARY_1));
+ HooksManager::loadLibraries(basic_library);
+
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // This time the change should have been recorded.
+ hooks_libraries_parser_->getLibraries(libraries, changed);
+ EXPECT_TRUE(libraries.empty());
+ EXPECT_TRUE(changed);
+
+ // But repeating it again and we are back to no change.
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+ hooks_libraries_parser_->getLibraries(libraries, changed);
+ EXPECT_TRUE(libraries.empty());
+ EXPECT_FALSE(changed);
+
+}
+
+
+TEST_F(ParseConfigTest, validHooksLibrariesTest) {
+
+ // Configuration string. This contains a set of valid libraries.
+ const std::string quote("\"");
+ const std::string comma(", ");
+
+ const std::string config =
+ std::string("{ ") +
+ std::string("\"hooks-libraries\": [") +
+ quote + std::string(CALLOUT_LIBRARY_1) + quote + comma +
+ quote + std::string(CALLOUT_LIBRARY_2) + quote +
+ std::string("]") +
+ std::string("}");
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Check that the parser holds two libraries and the configuration is
+ // recorded as having changed.
+ std::vector<std::string> libraries;
+ bool changed;
+ hooks_libraries_parser_->getLibraries(libraries, changed);
+ EXPECT_EQ(2, libraries.size());
+ EXPECT_TRUE(changed);
+
+ // The expected libraries should be the list of libraries specified
+ // in the given order.
+ std::vector<std::string> expected;
+ expected.push_back(CALLOUT_LIBRARY_1);
+ expected.push_back(CALLOUT_LIBRARY_2);
+ EXPECT_TRUE(expected == libraries);
+
+ // Parse the string again.
+ rcode = parseConfiguration(config);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // The list has not changed, and this is what we should see.
+ hooks_libraries_parser_->getLibraries(libraries, changed);
+ EXPECT_EQ(2, libraries.size());
+ EXPECT_FALSE(changed);
+}
+
+// Check with a set of libraries, some of which are invalid.
+TEST_F(ParseConfigTest, invalidHooksLibrariesTest) {
+
+ /// @todo Initialize global library context to null
+
+ // Configuration string. This contains an invalid library which should
+ // trigger an error in the "build" stage.
+ const std::string quote("\"");
+ const std::string comma(", ");
+
+ const std::string config =
+ std::string("{ ") +
+ std::string("\"hooks-libraries\": [") +
+ quote + std::string(CALLOUT_LIBRARY_1) + quote + comma +
+ quote + std::string(NOT_PRESENT_LIBRARY) + quote + comma +
+ quote + std::string(CALLOUT_LIBRARY_2) + quote +
+ std::string("]") +
+ std::string("}");
+
+ // Verify that the configuration fails to parse. (Syntactically it's OK,
+ // but the library is invalid).
+ int rcode = parseConfiguration(config);
+ ASSERT_FALSE(rcode == 0) << error_text_;
+
+ // Check that the message contains the library in error.
+ EXPECT_FALSE(error_text_.find(NOT_PRESENT_LIBRARY) == string::npos) <<
+ "Error text returned from parse failure is " << error_text_;
+}
+
+/// @brief Checks that a valid, enabled D2 client configuration works correctly.
+TEST_F(ParseConfigTest, validD2Config) {
+
+ // Configuration string containing valid values.
+ std::string config_str =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 3432, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config_str);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is enabled and we can fetch the configuration.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+ D2ClientConfigPtr d2_client_config;
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the configuration values are as expected.
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ("192.0.2.0", d2_client_config->getServerIp().toText());
+ EXPECT_EQ(3432, d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+ EXPECT_TRUE(d2_client_config->getRemoveOnRenew());
+ EXPECT_TRUE(d2_client_config->getAlwaysIncludeFqdn());
+ EXPECT_TRUE(d2_client_config->getOverrideNoUpdate());
+ EXPECT_TRUE(d2_client_config->getOverrideClientUpdate());
+ EXPECT_TRUE(d2_client_config->getReplaceClientName());
+ EXPECT_EQ("test.prefix", d2_client_config->getGeneratedPrefix());
+ EXPECT_EQ("test.suffix.", d2_client_config->getQualifyingSuffix());
+
+ // Another valid Configuration string.
+ // This one has IPV6 server ip, control flags false,
+ // empty prefix/suffix
+ std::string config_str2 =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"2001:db8::\", "
+ " \"server-port\" : 43567, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : false, "
+ " \"always-include-fqdn\" : false, "
+ " \"override-no-update\" : false, "
+ " \"override-client-update\" : false, "
+ " \"replace-client-name\" : false, "
+ " \"generated-prefix\" : \"\", "
+ " \"qualifying-suffix\" : \"\" "
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ rcode = parseConfiguration(config_str2);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is enabled and we can fetch the configuration.
+ EXPECT_TRUE(CfgMgr::instance().ddnsEnabled());
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ ASSERT_TRUE(d2_client_config);
+
+ // Verify that the configuration values are as expected.
+ EXPECT_TRUE(d2_client_config->getEnableUpdates());
+ EXPECT_EQ("2001:db8::", d2_client_config->getServerIp().toText());
+ EXPECT_EQ(43567, d2_client_config->getServerPort());
+ EXPECT_EQ(dhcp_ddns::NCR_UDP, d2_client_config->getNcrProtocol());
+ EXPECT_EQ(dhcp_ddns::FMT_JSON, d2_client_config->getNcrFormat());
+ EXPECT_FALSE(d2_client_config->getRemoveOnRenew());
+ EXPECT_FALSE(d2_client_config->getAlwaysIncludeFqdn());
+ EXPECT_FALSE(d2_client_config->getOverrideNoUpdate());
+ EXPECT_FALSE(d2_client_config->getOverrideClientUpdate());
+ EXPECT_FALSE(d2_client_config->getReplaceClientName());
+ EXPECT_EQ("", d2_client_config->getGeneratedPrefix());
+ EXPECT_EQ("", d2_client_config->getQualifyingSuffix());
+}
+
+/// @brief Checks that D2 client can be configured with enable flag of
+/// false only.
+TEST_F(ParseConfigTest, validDisabledD2Config) {
+
+ // Configuration string. This contains a set of valid libraries.
+ std::string config_str =
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : false"
+ " }"
+ "}";
+
+ // Verify that the configuration string parses.
+ int rcode = parseConfiguration(config_str);
+ ASSERT_TRUE(rcode == 0) << error_text_;
+
+ // Verify that DHCP-DDNS is disabled.
+ EXPECT_FALSE(CfgMgr::instance().ddnsEnabled());
+
+ // Make sure fetched config agrees.
+ D2ClientConfigPtr d2_client_config;
+ ASSERT_NO_THROW(d2_client_config = CfgMgr::instance().getD2ClientConfig());
+ EXPECT_TRUE(d2_client_config);
+ EXPECT_FALSE(d2_client_config->getEnableUpdates());
+}
+
+/// @brief Check various invalid D2 client configurations.
+TEST_F(ParseConfigTest, invalidD2Config) {
+ std::string invalid_configs[] = {
+ // only the enable flag of true
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true"
+ " }"
+ "}",
+ // Missing server ip value
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ //" \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // Invalid server ip value
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"x192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // Unknown protocol
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"Bogus\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // Unsupported protocol
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"TCP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // Unknown format
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"Bogus\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // Missig Port
+ "{ \"dhcp-ddns\" :"
+ " {"
+ " \"enable-updates\" : true, "
+ " \"server-ip\" : \"192.0.2.0\", "
+ // " \"server-port\" : 53001, "
+ " \"ncr-protocol\" : \"UDP\", "
+ " \"ncr-format\" : \"JSON\", "
+ " \"remove-on-renew\" : true, "
+ " \"always-include-fqdn\" : true, "
+ " \"override-no-update\" : true, "
+ " \"override-client-update\" : true, "
+ " \"replace-client-name\" : true, "
+ " \"generated-prefix\" : \"test.prefix\", "
+ " \"qualifying-suffix\" : \"test.suffix.\" "
+ " }"
+ "}",
+ // stop
+ ""
+ };
+
+ // Fetch the original config.
+ D2ClientConfigPtr original_config;
+ ASSERT_NO_THROW(original_config = CfgMgr::instance().getD2ClientConfig());
+
+ // Iterate through the invalid configuration strings, attempting to
+ // parse each one. They should fail to parse, but fail gracefully.
+ D2ClientConfigPtr current_config;
+ int i = 0;
+ while (!invalid_configs[i].empty()) {
+ // Verify that the configuration string parses without throwing.
+ int rcode = parseConfiguration(invalid_configs[i]);
+
+ // Verify that parse result indicates a parsing error.
+ ASSERT_TRUE(rcode != 0) << "Invalid config #: " << i
+ << " should not have passed!";
+
+ // Verify that the "official" config still matches the original config.
+ ASSERT_NO_THROW(current_config =
+ CfgMgr::instance().getD2ClientConfig());
+ EXPECT_EQ(*original_config, *current_config);
+ ++i;
+ }
+}
+
+/// @brief DHCP Configuration Parser Context test fixture.
+class ParserContextTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ParserContextTest() { }
+
+ /// @brief Check that the storages of the specific type hold the
+ /// same value.
+ ///
+ /// This function assumes that the ref_values storage holds exactly
+ /// one parameter called 'foo'.
+ ///
+ /// @param ref_values A storage holding reference value. In the typical
+ /// case it is a storage held in the original context, which is assigned
+ /// to another context.
+ /// @param values A storage holding value to be checked.
+ /// @tparam ContainerType A type of the storage.
+ /// @tparam ValueType A type of the value in the container.
+ template<typename ContainerType, typename ValueType>
+ void checkValueEq(const boost::shared_ptr<ContainerType>& ref_values,
+ const boost::shared_ptr<ContainerType>& values) {
+ ASSERT_NO_THROW(values->getParam("foo"));
+ EXPECT_EQ(ref_values->getParam("foo"), values->getParam("foo"));
+ }
+
+ /// @brief Check that the storages of the specific type hold different
+ /// value.
+ ///
+ /// This function assumes that the ref_values storage holds exactly
+ /// one parameter called 'foo'.
+ ///
+ /// @param ref_values A storage holding reference value. In the typical
+ /// case it is a storage held in the original context, which is assigned
+ /// to another context.
+ /// @param values A storage holding value to be checked.
+ /// @tparam ContainerType A type of the storage.
+ /// @tparam ValueType A type of the value in the container.
+ template<typename ContainerType, typename ValueType>
+ void checkValueNeq(const boost::shared_ptr<ContainerType>& ref_values,
+ const boost::shared_ptr<ContainerType>& values) {
+ ASSERT_NO_THROW(values->getParam("foo"));
+ EXPECT_NE(ref_values->getParam("foo"), values->getParam("foo"));
+ }
+
+ /// @brief Check that option definition storage in the context holds
+ /// one option definition of the specified type.
+ ///
+ /// @param ctx A pointer to a context.
+ /// @param opt_type Expected option type.
+ void checkOptionDefinitionType(const ParserContext& ctx,
+ const uint16_t opt_type) {
+ OptionDefContainerPtr opt_defs =
+ ctx.option_defs_->getItems("option-space");
+ ASSERT_TRUE(opt_defs);
+ OptionDefContainerTypeIndex& idx = opt_defs->get<1>();
+ OptionDefContainerTypeRange range = idx.equal_range(opt_type);
+ EXPECT_EQ(1, std::distance(range.first, range.second));
+ }
+
+ /// @brief Check that option storage in the context holds one option
+ /// of the specified type.
+ ///
+ /// @param ctx A pointer to a context.
+ /// @param opt_type Expected option type.
+ void checkOptionType(const ParserContext& ctx, const uint16_t opt_type) {
+ Subnet::OptionContainerPtr options =
+ ctx.options_->getItems("option-space");
+ ASSERT_TRUE(options);
+ Subnet::OptionContainerTypeIndex& idx = options->get<1>();
+ Subnet::OptionContainerTypeRange range = idx.equal_range(opt_type);
+ ASSERT_EQ(1, std::distance(range.first, range.second));
+ }
+
+ /// @brief Test copy constructor or assignment operator when values
+ /// being copied are NULL.
+ ///
+ /// @param copy Indicates that copy constructor should be tested
+ /// (if true), or assignment operator (if false).
+ void testCopyAssignmentNull(const bool copy) {
+ ParserContext ctx(Option::V6);
+ // Release all pointers in the context.
+ ctx.boolean_values_.reset();
+ ctx.uint32_values_.reset();
+ ctx.string_values_.reset();
+ ctx.options_.reset();
+ ctx.option_defs_.reset();
+ ctx.hooks_libraries_.reset();
+
+ // Even if the fields of the context are NULL, it should get
+ // copied.
+ ParserContextPtr ctx_new(new ParserContext(Option::V6));
+ if (copy) {
+ ASSERT_NO_THROW(ctx_new.reset(new ParserContext(ctx)));
+ } else {
+ *ctx_new = ctx;
+ }
+
+ // The resulting context has its fields equal to NULL.
+ EXPECT_FALSE(ctx_new->boolean_values_);
+ EXPECT_FALSE(ctx_new->uint32_values_);
+ EXPECT_FALSE(ctx_new->string_values_);
+ EXPECT_FALSE(ctx_new->options_);
+ EXPECT_FALSE(ctx_new->option_defs_);
+ EXPECT_FALSE(ctx_new->hooks_libraries_);
+
+ }
+
+ /// @brief Test copy constructor or assignment operator.
+ ///
+ /// @param copy Indicates that copy constructor should be tested (if true),
+ /// or assignment operator (if false).
+ void testCopyAssignment(const bool copy) {
+ // Create new context. It will be later copied/assigned to another
+ // context.
+ ParserContext ctx(Option::V6);
+
+ // Set boolean parameter 'foo'.
+ ASSERT_TRUE(ctx.boolean_values_);
+ ctx.boolean_values_->setParam("foo", true);
+
+ // Set uint32 parameter 'foo'.
+ ASSERT_TRUE(ctx.uint32_values_);
+ ctx.uint32_values_->setParam("foo", 123);
+
+ // Ser string parameter 'foo'.
+ ASSERT_TRUE(ctx.string_values_);
+ ctx.string_values_->setParam("foo", "some string");
+
+ // Add new option, with option code 10, to the context.
+ ASSERT_TRUE(ctx.options_);
+ OptionPtr opt1(new Option(Option::V6, 10));
+ Subnet::OptionDescriptor desc1(opt1, false);
+ std::string option_space = "option-space";
+ ASSERT_TRUE(desc1.option);
+ ctx.options_->addItem(desc1, option_space);
+
+ // Add new option definition, with option code 123.
+ OptionDefinitionPtr def1(new OptionDefinition("option-foo", 123,
+ "string"));
+ ctx.option_defs_->addItem(def1, option_space);
+
+ // Allocate container for hooks libraries and add one library name.
+ ctx.hooks_libraries_.reset(new std::vector<std::string>());
+ ctx.hooks_libraries_->push_back("library1");
+
+ // We will use ctx_new to assign another context to it or copy
+ // construct.
+ ParserContextPtr ctx_new(new ParserContext(Option::V4));;
+ if (copy) {
+ ctx_new.reset(new ParserContext(ctx));
+ } else {
+ *ctx_new = ctx;
+ }
+
+ // New context has the same boolean value.
+ ASSERT_TRUE(ctx_new->boolean_values_);
+ {
+ SCOPED_TRACE("Check that boolean values are equal in both"
+ " contexts");
+ checkValueEq<BooleanStorage, bool>(ctx.boolean_values_,
+ ctx_new->boolean_values_);
+ }
+
+ // New context has the same uint32 value.
+ ASSERT_TRUE(ctx_new->uint32_values_);
+ {
+ SCOPED_TRACE("Check that uint32_t values are equal in both"
+ " contexts");
+ checkValueEq<Uint32Storage, uint32_t>(ctx.uint32_values_,
+ ctx_new->uint32_values_);
+ }
+
+ // New context has the same string value.
+ ASSERT_TRUE(ctx_new->string_values_);
+ {
+ SCOPED_TRACE("Check that string values are equal in both contexts");
+ checkValueEq<StringStorage, std::string>(ctx.string_values_,
+ ctx_new->string_values_);
+ }
+
+ // New context has the same option.
+ ASSERT_TRUE(ctx_new->options_);
+ {
+ SCOPED_TRACE("Check that the option values are equal in both"
+ " contexts");
+ checkOptionType(*ctx_new, 10);
+ }
+
+ // New context has the same option definition.
+ ASSERT_TRUE(ctx_new->option_defs_);
+ {
+ SCOPED_TRACE("check that the option definition is equal in both"
+ " contexts");
+ checkOptionDefinitionType(*ctx_new, 123);
+ }
+
+ // New context has the same hooks library.
+ ASSERT_TRUE(ctx_new->hooks_libraries_);
+ {
+ ASSERT_EQ(1, ctx_new->hooks_libraries_->size());
+ EXPECT_EQ("library1", (*ctx_new->hooks_libraries_)[0]);
+ }
+
+ // New context has the same universe.
+ EXPECT_EQ(ctx.universe_, ctx_new->universe_);
+
+ // Change the value of the boolean parameter. This should not affect the
+ // corresponding value in the new context.
+ {
+ SCOPED_TRACE("Check that boolean value isn't changed when original"
+ " value is changed");
+ ctx.boolean_values_->setParam("foo", false);
+ checkValueNeq<BooleanStorage, bool>(ctx.boolean_values_,
+ ctx_new->boolean_values_);
+ }
+
+ // Change the value of the uint32_t parameter. This should not affect
+ // the corresponding value in the new context.
+ {
+ SCOPED_TRACE("Check that uint32_t value isn't changed when original"
+ " value is changed");
+ ctx.uint32_values_->setParam("foo", 987);
+ checkValueNeq<Uint32Storage, uint32_t>(ctx.uint32_values_,
+ ctx_new->uint32_values_);
+ }
+
+ // Change the value of the string parameter. This should not affect the
+ // corresponding value in the new context.
+ {
+ SCOPED_TRACE("Check that string value isn't changed when original"
+ " value is changed");
+ ctx.string_values_->setParam("foo", "different string");
+ checkValueNeq<StringStorage, std::string>(ctx.string_values_,
+ ctx_new->string_values_);
+ }
+
+ // Change the option. This should not affect the option instance in the
+ // new context.
+ {
+ SCOPED_TRACE("Check that option remains the same in the new context"
+ " when the option in the original context is changed");
+ ctx.options_->clearItems();
+ Subnet::OptionDescriptor desc(OptionPtr(new Option(Option::V6,
+ 100)),
+ false);
+
+ ASSERT_TRUE(desc.option);
+ ctx.options_->addItem(desc, "option-space");
+ checkOptionType(*ctx_new, 10);
+ }
+
+ // Change the option definition. This should not affect the option
+ // definition in the new context.
+ {
+ SCOPED_TRACE("Check that option definition remains the same in"
+ " the new context when the option definition in the"
+ " original context is changed");
+ ctx.option_defs_->clearItems();
+ OptionDefinitionPtr def(new OptionDefinition("option-foo", 234,
+ "string"));
+
+ ctx.option_defs_->addItem(def, option_space);
+ checkOptionDefinitionType(*ctx_new, 123);
+ }
+
+ // Change the list of libraries. this should not affect the list in the
+ // new context.
+ ctx.hooks_libraries_->clear();
+ ctx.hooks_libraries_->push_back("library2");
+ ASSERT_EQ(1, ctx_new->hooks_libraries_->size());
+ EXPECT_EQ("library1", (*ctx_new->hooks_libraries_)[0]);
+
+ // Change the universe. This should not affect the universe value in the
+ // new context.
+ ctx.universe_ = Option::V4;
+ EXPECT_EQ(Option::V6, ctx_new->universe_);
+
+ }
+
+};
+
+// Check that the assignment operator of the ParserContext class copies all
+// fields correctly.
+TEST_F(ParserContextTest, assignment) {
+ testCopyAssignment(false);
+}
+
+// Check that the assignment operator of the ParserContext class copies all
+// fields correctly when these fields are NULL.
+TEST_F(ParserContextTest, assignmentNull) {
+ testCopyAssignmentNull(false);
+}
+
+// Check that the context is copy constructed correctly.
+TEST_F(ParserContextTest, copyConstruct) {
+ testCopyAssignment(true);
+}
+
+// Check that the context is copy constructed correctly, when context fields
+// are NULL.
+TEST_F(ParserContextTest, copyConstructNull) {
+ testCopyAssignmentNull(true);
+}
diff --git a/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
index 124301a..ab5a8b2 100644
--- a/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
+++ b/src/lib/dhcpsrv/tests/lease_mgr_unittest.cc
@@ -16,6 +16,8 @@
#include <asiolink/io_address.h>
#include <dhcpsrv/lease_mgr.h>
+#include <dhcpsrv/memfile_lease_mgr.h>
+#include <dhcpsrv/tests/test_utils.h>
#include <gtest/gtest.h>
@@ -28,6 +30,7 @@ using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
+using namespace isc::dhcp::test;
// This is a concrete implementation of a Lease database. It does not do
// anything useful and is used for abstract LeaseMgr class testing.
@@ -110,6 +113,19 @@ public:
return (Lease4Collection());
}
+ /// @brief Returns existing IPv4 lease for specified client identifier,
+ /// HW address and subnet identifier.
+ ///
+ /// @param client_id Aclient identifier
+ /// @param hwaddr A HW address.
+ /// @param subnet_id A subnet identifier.
+ ///
+ /// @return A pointer to an existing lease or NULL if lease not found.
+ virtual Lease4Ptr
+ getLease4(const ClientId&, const HWAddr&, SubnetID) const {
+ return (Lease4Ptr());
+ }
+
/// @brief Returns existing IPv4 lease for specified client-id
///
/// There can be at most one lease for a given HW address in a single
@@ -128,29 +144,32 @@ public:
/// @param addr address of the searched lease
///
/// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const isc::asiolink::IOAddress&) const {
+ virtual Lease6Ptr getLease6(Lease::Type /* not used yet */,
+ const isc::asiolink::IOAddress&) const {
return (Lease6Ptr());
}
/// @brief Returns existing IPv6 lease for a given DUID+IA combination
///
- /// @param duid client DUID
- /// @param iaid IA identifier
+ /// @param duid ignored
+ /// @param iaid ignored
///
- /// @return collection of IPv6 leases
- virtual Lease6Collection getLease6(const DUID&, uint32_t) const {
- return (Lease6Collection());
+ /// @return whatever is set in leases6_ field
+ virtual Lease6Collection getLeases6(Lease::Type /* not used yet */,
+ const DUID&, uint32_t) const {
+ return (leases6_);
}
- /// @brief Returns existing IPv6 lease for a given DUID+IA combination
+ /// @brief Returns existing IPv6 lease for a given DUID+IA+subnet-id combination
///
- /// @param duid client DUID
- /// @param iaid IA identifier
- /// @param subnet_id identifier of the subnet the lease must belong to
+ /// @param duid ignored
+ /// @param iaid ignored
+ /// @param subnet_id ignored
///
- /// @return smart pointer to the lease (or NULL if a lease is not found)
- virtual Lease6Ptr getLease6(const DUID&, uint32_t, SubnetID) const {
- return (Lease6Ptr());
+ /// @return whatever is set in leases6_ field
+ virtual Lease6Collection getLeases6(Lease::Type /* not used yet */,
+ const DUID&, uint32_t, SubnetID) const {
+ return (leases6_);
}
/// @brief Updates IPv4 lease.
@@ -217,15 +236,37 @@ public:
/// @brief Rollback transactions
virtual void rollback() {
}
+
+ // We need to use it in ConcreteLeaseMgr
+ using LeaseMgr::getLease6;
+
+ Lease6Collection leases6_; ///< getLease6 methods return this as is
+};
+
+class LeaseMgrTest : public GenericLeaseMgrTest {
+public:
+ LeaseMgrTest() {
+ }
};
namespace {
+/// Hardware address used by different tests.
+const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
+/// Client id used by different tests.
+const uint8_t CLIENTID[] = {0x17, 0x34, 0xe2, 0xff, 0x09, 0x92, 0x54};
+/// Valid lifetime value used by different tests.
+const uint32_t VALID_LIFETIME = 500;
+/// Subnet ID used by different tests.
+const uint32_t SUBNET_ID = 42;
+/// IAID value used by different tests.
+const uint32_t IAID = 7;
+
/// @brief getParameter test
///
/// This test checks if the LeaseMgr can be instantiated and that it
/// parses parameters string properly.
-TEST(LeaseMgr, getParameter) {
+TEST_F(LeaseMgrTest, getParameter) {
LeaseMgr::ParameterMap pmap;
pmap[std::string("param1")] = std::string("value1");
@@ -237,6 +278,44 @@ TEST(LeaseMgr, getParameter) {
EXPECT_THROW(leasemgr.getParameter("param3"), BadValue);
}
+// This test checks if getLease6() method is working properly for 0 (NULL),
+// 1 (return the lease) and more than 1 leases (throw).
+TEST_F(LeaseMgrTest, getLease6) {
+
+ LeaseMgr::ParameterMap pmap;
+ boost::scoped_ptr<ConcreteLeaseMgr> mgr(new ConcreteLeaseMgr(pmap));
+
+ vector<Lease6Ptr> leases = createLeases6();
+
+ mgr->leases6_.clear();
+ // For no leases, the function should return NULL pointer
+ Lease6Ptr lease;
+
+ // the getLease6() is calling getLeases6(), which is a dummy. It returns
+ // whatever is there in leases6_ field.
+ EXPECT_NO_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_));
+ EXPECT_TRUE(Lease6Ptr() == lease);
+
+ // For a single lease, the function should return that lease
+ mgr->leases6_.push_back(leases[1]);
+ EXPECT_NO_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_));
+ EXPECT_TRUE(lease);
+
+ EXPECT_NO_THROW(detailCompareLease(lease, leases[1]));
+
+ // Add one more lease. There are 2 now. It should throw
+ mgr->leases6_.push_back(leases[2]);
+
+ EXPECT_THROW(lease = mgr->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_,
+ leases[1]->subnet_id_),
+ MultipleRecords);
+}
+
// There's no point in calling any other methods in LeaseMgr, as they
// are purely virtual, so we would only call ConcreteLeaseMgr methods.
// Those methods are just stubs that do not return anything.
@@ -245,7 +324,7 @@ TEST(LeaseMgr, getParameter) {
///
/// Lease4 is also defined in lease_mgr.h, so is tested in this file as well.
// This test checks if the Lease4 structure can be instantiated correctly
-TEST(Lease4, Lease4Constructor) {
+TEST(Lease4, constructor) {
// Random values for the tests
const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
@@ -272,8 +351,9 @@ TEST(Lease4, Lease4Constructor) {
// Create the lease
Lease4 lease(ADDRESS[i], HWADDR, sizeof(HWADDR),
- CLIENTID, sizeof(CLIENTID), VALID_LIFETIME, 0, 0, current_time,
- SUBNET_ID);
+ CLIENTID, sizeof(CLIENTID), VALID_LIFETIME, 0, 0,
+ current_time, SUBNET_ID, true, true,
+ "hostname.example.com.");
EXPECT_EQ(ADDRESS[i], static_cast<uint32_t>(lease.addr_));
EXPECT_EQ(0, lease.ext_);
@@ -285,19 +365,132 @@ TEST(Lease4, Lease4Constructor) {
EXPECT_EQ(current_time, lease.cltt_);
EXPECT_EQ(SUBNET_ID, lease.subnet_id_);
EXPECT_FALSE(lease.fixed_);
- EXPECT_TRUE(lease.hostname_.empty());
- EXPECT_FALSE(lease.fqdn_fwd_);
- EXPECT_FALSE(lease.fqdn_rev_);
+ EXPECT_EQ("hostname.example.com.", lease.hostname_);
+ EXPECT_TRUE(lease.fqdn_fwd_);
+ EXPECT_TRUE(lease.fqdn_rev_);
EXPECT_TRUE(lease.comments_.empty());
}
}
+// This test verfies that copy constructor copies Lease4 fields correctly.
+TEST(Lease4, copyConstructor) {
+
+ // Random values for the tests
+ const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
+ std::vector<uint8_t> hwaddr(HWADDR, HWADDR + sizeof(HWADDR));
+
+ const uint8_t CLIENTID[] = {0x17, 0x34, 0xe2, 0xff, 0x09, 0x92, 0x54};
+ std::vector<uint8_t> clientid_vec(CLIENTID, CLIENTID + sizeof(CLIENTID));
+ ClientId clientid(clientid_vec);
+
+ // ...and a time
+ const time_t current_time = time(NULL);
+
+ // Other random constants.
+ const uint32_t SUBNET_ID = 42;
+ const uint32_t VALID_LIFETIME = 500;
+
+ // Create the lease
+ Lease4 lease(0xffffffff, HWADDR, sizeof(HWADDR),
+ CLIENTID, sizeof(CLIENTID), VALID_LIFETIME, 0, 0, current_time,
+ SUBNET_ID);
+
+ // Use copy constructor to copy the lease.
+ Lease4 copied_lease(lease);
+
+ // Both leases should be now equal. When doing this check we assume that
+ // the equality operator works correctly.
+ EXPECT_TRUE(lease == copied_lease);
+ // Client IDs are equal, but they should be in two distinct pointers.
+ EXPECT_FALSE(lease.client_id_ == copied_lease.client_id_);
+}
+
+// This test verfies that the assignment operator copies all Lease4 fields
+// correctly.
+TEST(Lease4, operatorAssign) {
+
+ // Random values for the tests
+ const uint8_t HWADDR[] = {0x08, 0x00, 0x2b, 0x02, 0x3f, 0x4e};
+ std::vector<uint8_t> hwaddr(HWADDR, HWADDR + sizeof(HWADDR));
+
+ const uint8_t CLIENTID[] = {0x17, 0x34, 0xe2, 0xff, 0x09, 0x92, 0x54};
+ std::vector<uint8_t> clientid_vec(CLIENTID, CLIENTID + sizeof(CLIENTID));
+ ClientId clientid(clientid_vec);
+
+ // ...and a time
+ const time_t current_time = time(NULL);
+
+ // Other random constants.
+ const uint32_t SUBNET_ID = 42;
+ const uint32_t VALID_LIFETIME = 500;
+
+ // Create the lease
+ Lease4 lease(0xffffffff, HWADDR, sizeof(HWADDR),
+ CLIENTID, sizeof(CLIENTID), VALID_LIFETIME, 0, 0, current_time,
+ SUBNET_ID);
+
+ // Use assignment operator to assign the lease.
+ Lease4 copied_lease = lease;
+
+ // Both leases should be now equal. When doing this check we assume that
+ // the equality operator works correctly.
+ EXPECT_TRUE(lease == copied_lease);
+ // Client IDs are equal, but they should be in two distinct pointers.
+ EXPECT_FALSE(lease.client_id_ == copied_lease.client_id_);
+}
+
+// This test verifies that the matches() returns true if two leases differ
+// by values other than address, HW address, Client ID and ext_.
+TEST(Lease4, matches) {
+ // Create two leases which share the same address, HW address, client id
+ // and ext_ value.
+ const time_t current_time = time(NULL);
+ Lease4 lease1(IOAddress("192.0.2.3"), HWADDR, sizeof(HWADDR), CLIENTID,
+ sizeof(CLIENTID), VALID_LIFETIME, current_time, 0, 0,
+ SUBNET_ID);
+ lease1.hostname_ = "lease1.example.com.";
+ lease1.fqdn_fwd_ = true;
+ lease1.fqdn_rev_ = true;
+ Lease4 lease2(IOAddress("192.0.2.3"), HWADDR, sizeof(HWADDR), CLIENTID,
+ sizeof(CLIENTID), VALID_LIFETIME + 10, current_time - 10,
+ 100, 200, SUBNET_ID);
+ lease2.hostname_ = "lease2.example.com.";
+ lease2.fqdn_fwd_ = false;
+ lease2.fqdn_rev_ = true;
+
+ // Leases should match.
+ EXPECT_TRUE(lease1.matches(lease2));
+ EXPECT_TRUE(lease2.matches(lease1));
+
+ // Change address, leases should not match anymore.
+ lease1.addr_ = IOAddress("192.0.2.4");
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.addr_ = lease2.addr_;
+
+ // Change HW address, leases should not match.
+ lease1.hwaddr_[1] += 1;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.hwaddr_ = lease2.hwaddr_;
+
+ // Chanage client id, leases should not match.
+ std::vector<uint8_t> client_id = lease1.client_id_->getClientId();
+ client_id[1] += 1;
+ lease1.client_id_.reset(new ClientId(client_id));
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.client_id_ = lease2.client_id_;
+
+ // Change ext_, leases should not match.
+ lease1.ext_ += 1;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.ext_ = lease2.ext_;
+}
+
/// @brief Lease4 Equality Test
///
/// Checks that the operator==() correctly compares two leases for equality.
/// As operator!=() is also defined for this class, every check on operator==()
/// is followed by the reverse check on operator!=().
-TEST(Lease4, OperatorEquals) {
+TEST(Lease4, operatorEquals) {
// Random values for the tests
const uint32_t ADDRESS = 0x01020304;
@@ -427,7 +620,7 @@ TEST(Lease4, OperatorEquals) {
// Lease6 is also defined in lease_mgr.h, so is tested in this file as well.
// This test checks if the Lease6 structure can be instantiated correctly
-TEST(Lease6, Lease6Constructor) {
+TEST(Lease6, Lease6ConstructorDefault) {
// check a variety of addresses with different bits set.
const char* ADDRESS[] = {
@@ -445,7 +638,7 @@ TEST(Lease6, Lease6Constructor) {
for (int i = 0; i < sizeof(ADDRESS) / sizeof(ADDRESS[0]); ++i) {
IOAddress addr(ADDRESS[i]);
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr,
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr,
duid, iaid, 100, 200, 50, 80,
subnet_id));
@@ -453,21 +646,125 @@ TEST(Lease6, Lease6Constructor) {
EXPECT_TRUE(*lease->duid_ == *duid);
EXPECT_TRUE(lease->iaid_ == iaid);
EXPECT_TRUE(lease->subnet_id_ == subnet_id);
- EXPECT_TRUE(lease->type_ == Lease6::LEASE_IA_NA);
+ EXPECT_TRUE(lease->type_ == Lease::TYPE_NA);
EXPECT_TRUE(lease->preferred_lft_ == 100);
EXPECT_TRUE(lease->valid_lft_ == 200);
EXPECT_TRUE(lease->t1_ == 50);
EXPECT_TRUE(lease->t2_ == 80);
+ EXPECT_FALSE(lease->fqdn_fwd_);
+ EXPECT_FALSE(lease->fqdn_rev_);
+ EXPECT_TRUE(lease->hostname_.empty());
+
}
// Lease6 must be instantiated with a DUID, not with NULL pointer
IOAddress addr(ADDRESS[0]);
Lease6Ptr lease2;
- EXPECT_THROW(lease2.reset(new Lease6(Lease6::LEASE_IA_NA, addr,
+ EXPECT_THROW(lease2.reset(new Lease6(Lease::TYPE_NA, addr,
DuidPtr(), iaid, 100, 200, 50, 80,
subnet_id)), InvalidOperation);
}
+// This test verifies that the Lease6 constructor which accepts FQDN data,
+// sets the data correctly for the lease.
+TEST(Lease6, Lease6ConstructorWithFQDN) {
+
+ // check a variety of addresses with different bits set.
+ const char* ADDRESS[] = {
+ "::", "::1", "2001:db8:1::456",
+ "7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ "8000::", "8000::1",
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ };
+
+ // Other values
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+ uint32_t iaid = 7; // Just a number
+ SubnetID subnet_id = 8; // Just another number
+
+ for (int i = 0; i < sizeof(ADDRESS) / sizeof(ADDRESS[0]); ++i) {
+ IOAddress addr(ADDRESS[i]);
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr,
+ duid, iaid, 100, 200, 50, 80, subnet_id,
+ true, true, "host.example.com."));
+
+ EXPECT_TRUE(lease->addr_ == addr);
+ EXPECT_TRUE(*lease->duid_ == *duid);
+ EXPECT_TRUE(lease->iaid_ == iaid);
+ EXPECT_TRUE(lease->subnet_id_ == subnet_id);
+ EXPECT_TRUE(lease->type_ == Lease::TYPE_NA);
+ EXPECT_TRUE(lease->preferred_lft_ == 100);
+ EXPECT_TRUE(lease->valid_lft_ == 200);
+ EXPECT_TRUE(lease->t1_ == 50);
+ EXPECT_TRUE(lease->t2_ == 80);
+ EXPECT_TRUE(lease->fqdn_fwd_);
+ EXPECT_TRUE(lease->fqdn_rev_);
+ EXPECT_EQ("host.example.com.", lease->hostname_);
+ }
+
+ // Lease6 must be instantiated with a DUID, not with NULL pointer
+ IOAddress addr(ADDRESS[0]);
+ Lease6Ptr lease2;
+ EXPECT_THROW(lease2.reset(new Lease6(Lease::TYPE_NA, addr,
+ DuidPtr(), iaid, 100, 200, 50, 80,
+ subnet_id)), InvalidOperation);
+}
+
+// This test verifies that the matches() function returns true if two leases
+// differ by values other than address, type, prefix length, IAID and DUID.
+TEST(Lease6, matches) {
+
+ // Create two matching leases.
+ uint8_t llt[] = {0, 1, 2, 3, 4, 5, 6, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf};
+ DuidPtr duid(new DUID(llt, sizeof(llt)));
+
+ Lease6 lease1(Lease6::TYPE_NA, IOAddress("2001:db8:1::1"), duid,
+ IAID, 100, 200, 50, 80,
+ SUBNET_ID);
+ lease1.hostname_ = "lease1.example.com.";
+ lease1.fqdn_fwd_ = true;
+ lease1.fqdn_rev_ = true;
+ Lease6 lease2(Lease6::TYPE_NA, IOAddress("2001:db8:1::1"), duid,
+ IAID, 200, 300, 90, 70,
+ SUBNET_ID);
+ lease2.hostname_ = "lease1.example.com.";
+ lease2.fqdn_fwd_ = false;
+ lease2.fqdn_rev_ = true;
+
+ EXPECT_TRUE(lease1.matches(lease2));
+
+ // Modify each value used to match both leases, and make sure that
+ // leases don't match.
+
+ // Modify address.
+ lease1.addr_ = IOAddress("2001:db8:1::2");
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.addr_ = lease2.addr_;
+
+ // Modify lease type.
+ lease1.type_ = Lease6::TYPE_TA;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.type_ = lease2.type_;
+
+ // Modify prefix length.
+ lease1.prefixlen_ += 1;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.prefixlen_ = lease2.prefixlen_;
+
+ // Modify IAID.
+ lease1.iaid_ += 1;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.iaid_ = lease2.iaid_;
+
+ // Modify DUID.
+ llt[1] += 1;
+ duid.reset(new DUID(llt, sizeof(llt)));
+ lease1.duid_ = duid;
+ EXPECT_FALSE(lease1.matches(lease2));
+ lease1.duid_ = lease2.duid_;
+}
+
/// @brief Lease6 Equality Test
///
/// Checks that the operator==() correctly compares two leases for equality.
@@ -483,10 +780,14 @@ TEST(Lease6, OperatorEquals) {
SubnetID subnet_id = 8; // just another number
// Check for equality.
- Lease6 lease1(Lease6::LEASE_IA_NA, addr, duid, iaid, 100, 200, 50, 80,
+ Lease6 lease1(Lease::TYPE_NA, addr, duid, iaid, 100, 200, 50, 80,
subnet_id);
- Lease6 lease2(Lease6::LEASE_IA_NA, addr, duid, iaid, 100, 200, 50, 80,
+ Lease6 lease2(Lease::TYPE_NA, addr, duid, iaid, 100, 200, 50, 80,
subnet_id);
+
+ // cltt_ constructs with time(NULL), make sure they are always equal
+ lease1.cltt_ = lease2.cltt_;
+
EXPECT_TRUE(lease1 == lease2);
EXPECT_FALSE(lease1 != lease2);
@@ -499,7 +800,7 @@ TEST(Lease6, OperatorEquals) {
EXPECT_TRUE(lease1 == lease2); // Check that the reversion has made the
EXPECT_FALSE(lease1 != lease2); // ... leases equal
- lease1.type_ = Lease6::LEASE_IA_PD;
+ lease1.type_ = Lease::TYPE_PD;
EXPECT_FALSE(lease1 == lease2);
EXPECT_TRUE(lease1 != lease2);
lease1.type_ = lease2.type_;
@@ -614,7 +915,7 @@ TEST(Lease6, Lease6Expired) {
const DuidPtr duid(new DUID(duid_array, sizeof(duid_array)));
const uint32_t iaid = 7; // Just a number
const SubnetID subnet_id = 8; // Just another number
- Lease6 lease(Lease6::LEASE_IA_NA, addr, duid, iaid, 100, 200, 50, 80,
+ Lease6 lease(Lease::TYPE_NA, addr, duid, iaid, 100, 200, 50, 80,
subnet_id);
// Case 1: a second before expiration
diff --git a/src/lib/dhcpsrv/tests/lease_unittest.cc b/src/lib/dhcpsrv/tests/lease_unittest.cc
new file mode 100644
index 0000000..6f57f30
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/lease_unittest.cc
@@ -0,0 +1,71 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <config.h>
+#include <dhcp/duid.h>
+#include <dhcpsrv/lease.h>
+#include <gtest/gtest.h>
+#include <vector>
+
+using namespace isc;
+using namespace isc::dhcp;
+
+namespace {
+
+// @todo Currently this file contains tests for new functions which return DUID
+// or client identifier. Other tests for Lease objects must be implemented.
+// See http://bind10.isc.org/ticket/3240.
+
+// Verify that the client id can be returned as a vector object and if client
+// id is NULL the empty vector is returned.
+TEST(Lease4Test, getClientIdVector) {
+ // Create a lease.
+ Lease4 lease;
+ // By default, the lease should have client id set to NULL. If it doesn't,
+ // continuing the test makes no sense.
+ ASSERT_FALSE(lease.client_id_);
+ // When client id is NULL the vector returned should be empty.
+ EXPECT_TRUE(lease.getClientIdVector().empty());
+ // Now, let's set the non NULL client id. Fill it with the 8 bytes, each
+ // holding a value of 0x42.
+ std::vector<uint8_t> client_id_vec(8, 0x42);
+ lease.client_id_ = ClientIdPtr(new ClientId(client_id_vec));
+ // Check that the returned vector, encapsulating client id is equal to
+ // the one that has been used to set the client id for the lease.
+ std::vector<uint8_t> returned_vec = lease.getClientIdVector();
+ EXPECT_TRUE(returned_vec == client_id_vec);
+}
+
+// Verify that the DUID can be returned as a vector object and if DUID is NULL
+// the empty vector is returned.
+TEST(Lease6Test, getDuidVector) {
+ // Create a lease.
+ Lease6 lease;
+ // By default, the lease should have client id set to NULL. If it doesn't,
+ // continuing the test makes no sense.
+ ASSERT_FALSE(lease.duid_);
+ // When client id is NULL the vector returned should be empty.
+ EXPECT_TRUE(lease.getDuidVector().empty());
+ // Now, let's set the non NULL DUID. Fill it with the 8 bytes, each
+ // holding a value of 0x42.
+ std::vector<uint8_t> duid_vec(8, 0x42);
+ lease.duid_ = DuidPtr(new DUID(duid_vec));
+ // Check that the returned vector, encapsulating DUID is equal to
+ // the one that has been used to set the DUID for the lease.
+ std::vector<uint8_t> returned_vec = lease.getDuidVector();
+ EXPECT_TRUE(returned_vec == duid_vec);
+}
+
+
+}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
index 08186dc..6570750 100644
--- a/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
+++ b/src/lib/dhcpsrv/tests/memfile_lease_mgr_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -18,7 +18,7 @@
#include <dhcp/duid.h>
#include <dhcpsrv/lease_mgr.h>
#include <dhcpsrv/memfile_lease_mgr.h>
-
+#include <dhcpsrv/tests/test_utils.h>
#include <gtest/gtest.h>
#include <iostream>
@@ -28,13 +28,21 @@ using namespace std;
using namespace isc;
using namespace isc::asiolink;
using namespace isc::dhcp;
+using namespace isc::dhcp::test;
namespace {
// empty class for now, but may be extended once Addr6 becomes bigger
-class MemfileLeaseMgrTest : public ::testing::Test {
+class MemfileLeaseMgrTest : public GenericLeaseMgrTest {
public:
MemfileLeaseMgrTest() {
+ const LeaseMgr::ParameterMap pmap;
+ lmptr_ = new Memfile_LeaseMgr(pmap);
+ }
+
+ virtual ~MemfileLeaseMgrTest() {
+ delete lmptr_;
}
+
};
// This test checks if the LeaseMgr can be instantiated and that it
@@ -59,7 +67,7 @@ TEST_F(MemfileLeaseMgrTest, getTypeAndName) {
// Checks that adding/getting/deleting a Lease6 object works.
TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
const LeaseMgr::ParameterMap pmap; // Empty parameter map
- boost::scoped_ptr<Memfile_LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
+ boost::scoped_ptr<LeaseMgr> lease_mgr(new Memfile_LeaseMgr(pmap));
IOAddress addr("2001:db8:1::456");
@@ -70,7 +78,7 @@ TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
SubnetID subnet_id = 8; // just another number
- Lease6Ptr lease(new Lease6(Lease6::LEASE_IA_NA, addr,
+ Lease6Ptr lease(new Lease6(Lease::TYPE_NA, addr,
duid, iaid, 100, 200, 50, 80,
subnet_id));
@@ -79,44 +87,50 @@ TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
// should not be allowed to add a second lease with the same address
EXPECT_FALSE(lease_mgr->addLease(lease));
- Lease6Ptr x = lease_mgr->getLease6(IOAddress("2001:db8:1::234"));
+ Lease6Ptr x = lease_mgr->getLease6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::234"));
EXPECT_EQ(Lease6Ptr(), x);
- x = lease_mgr->getLease6(IOAddress("2001:db8:1::456"));
+ x = lease_mgr->getLease6(Lease::TYPE_NA,
+ IOAddress("2001:db8:1::456"));
ASSERT_TRUE(x);
- EXPECT_EQ(x->addr_.toText(), addr.toText());
+ EXPECT_EQ(x->addr_, addr);
EXPECT_TRUE(*x->duid_ == *duid);
EXPECT_EQ(x->iaid_, iaid);
EXPECT_EQ(x->subnet_id_, subnet_id);
// These are not important from lease management perspective, but
// let's check them anyway.
- EXPECT_EQ(x->type_, Lease6::LEASE_IA_NA);
+ EXPECT_EQ(x->type_, Lease::TYPE_NA);
EXPECT_EQ(x->preferred_lft_, 100);
EXPECT_EQ(x->valid_lft_, 200);
EXPECT_EQ(x->t1_, 50);
EXPECT_EQ(x->t2_, 80);
// Test getLease6(duid, iaid, subnet_id) - positive case
- Lease6Ptr y = lease_mgr->getLease6(*duid, iaid, subnet_id);
+ Lease6Ptr y = lease_mgr->getLease6(Lease::TYPE_NA, *duid, iaid,
+ subnet_id);
ASSERT_TRUE(y);
EXPECT_TRUE(*y->duid_ == *duid);
EXPECT_EQ(y->iaid_, iaid);
- EXPECT_EQ(y->addr_.toText(), addr.toText());
+ EXPECT_EQ(y->addr_, addr);
// Test getLease6(duid, iaid, subnet_id) - wrong iaid
uint32_t invalid_iaid = 9; // no such iaid
- y = lease_mgr->getLease6(*duid, invalid_iaid, subnet_id);
+ y = lease_mgr->getLease6(Lease::TYPE_NA, *duid, invalid_iaid,
+ subnet_id);
EXPECT_FALSE(y);
uint32_t invalid_subnet_id = 999;
- y = lease_mgr->getLease6(*duid, iaid, invalid_subnet_id);
+ y = lease_mgr->getLease6(Lease::TYPE_NA, *duid, iaid,
+ invalid_subnet_id);
EXPECT_FALSE(y);
// truncated duid
DuidPtr invalid_duid(new DUID(llt, sizeof(llt) - 1));
- y = lease_mgr->getLease6(*invalid_duid, iaid, subnet_id);
+ y = lease_mgr->getLease6(Lease::TYPE_NA, *invalid_duid, iaid,
+ subnet_id);
EXPECT_FALSE(y);
// should return false - there's no such address
@@ -126,10 +140,30 @@ TEST_F(MemfileLeaseMgrTest, addGetDelete6) {
EXPECT_TRUE(lease_mgr->deleteLease(IOAddress("2001:db8:1::456")));
// after the lease is deleted, it should really be gone
- x = lease_mgr->getLease6(IOAddress("2001:db8:1::456"));
+ x = lease_mgr->getLease6(Lease::TYPE_NA, IOAddress("2001:db8:1::456"));
EXPECT_EQ(Lease6Ptr(), x);
}
-// TODO: Write more memfile tests
+/// @todo Write more memfile tests
+
+// Simple test about lease4 retrieval through client id method
+TEST_F(MemfileLeaseMgrTest, getLease4ClientId) {
+ testGetLease4ClientId();
+}
+
+// Checks that lease4 retrieval client id is null is working
+TEST_F(MemfileLeaseMgrTest, getLease4NullClientId) {
+ testGetLease4NullClientId();
+}
+
+// Checks lease4 retrieval through HWAddr
+TEST_F(MemfileLeaseMgrTest, getLease4HWAddr) {
+ testGetLease4HWAddr();
+}
+
+// Checks lease4 retrieval with clientId, HWAddr and subnet_id
+TEST_F(MemfileLeaseMgrTest, getLease4ClientIdHWAddrSubnetId) {
+ testGetLease4ClientIdHWAddrSubnetId();
+}
}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
index 5b2fa9e..578ef3c 100644
--- a/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
+++ b/src/lib/dhcpsrv/tests/mysql_lease_mgr_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -20,6 +20,7 @@
#include <dhcpsrv/tests/test_utils.h>
#include <exceptions/exceptions.h>
+
#include <gtest/gtest.h>
#include <algorithm>
@@ -39,18 +40,6 @@ namespace {
// This holds statements to create and destroy the schema.
#include "schema_copy.h"
-// IPv4 and IPv6 addresses used in the tests
-const char* ADDRESS4[] = {
- "192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3",
- "192.0.2.4", "192.0.2.5", "192.0.2.6", "192.0.2.7",
- NULL
-};
-const char* ADDRESS6[] = {
- "2001:db8::0", "2001:db8::1", "2001:db8::2", "2001:db8::3",
- "2001:db8::4", "2001:db8::5", "2001:db8::6", "2001:db8::7",
- NULL
-};
-
// Connection strings.
// Database: keatest
// Host: localhost
@@ -155,26 +144,12 @@ void createSchema() {
/// Opens the database prior to each test and closes it afterwards.
/// All pending transactions are deleted prior to closure.
-class MySqlLeaseMgrTest : public ::testing::Test {
+class MySqlLeaseMgrTest : public GenericLeaseMgrTest {
public:
/// @brief Constructor
///
/// Deletes everything from the database and opens it.
MySqlLeaseMgrTest() {
- // Initialize address strings and IOAddresses
- for (int i = 0; ADDRESS4[i] != NULL; ++i) {
- string addr(ADDRESS4[i]);
- straddress4_.push_back(addr);
- IOAddress ioaddr(addr);
- ioaddress4_.push_back(ioaddr);
- }
-
- for (int i = 0; ADDRESS6[i] != NULL; ++i) {
- string addr(ADDRESS6[i]);
- straddress6_.push_back(addr);
- IOAddress ioaddr(addr);
- ioaddress6_.push_back(ioaddr);
- }
// Ensure schema is the correct one.
destroySchema();
@@ -214,319 +189,6 @@ public:
lmptr_ = &(LeaseMgrFactory::instance());
}
- /// @brief Initialize Lease4 Fields
- ///
- /// Returns a pointer to a Lease4 structure. Different values are put into
- /// the lease according to the address passed.
- ///
- /// This is just a convenience function for the test methods.
- ///
- /// @param address Address to use for the initialization
- ///
- /// @return Lease4Ptr. This will not point to anything if the
- /// initialization failed (e.g. unknown address).
- Lease4Ptr initializeLease4(std::string address) {
- Lease4Ptr lease(new Lease4());
-
- // Set the address of the lease
- lease->addr_ = IOAddress(address);
-
- // Initialize unused fields.
- lease->ext_ = 0; // Not saved
- lease->t1_ = 0; // Not saved
- lease->t2_ = 0; // Not saved
- lease->fixed_ = false; // Unused
- lease->hostname_ = std::string(""); // Unused
- lease->fqdn_fwd_ = false; // Unused
- lease->fqdn_rev_ = false; // Unused
- lease->comments_ = std::string(""); // Unused
-
- // Set other parameters. For historical reasons, address 0 is not used.
- if (address == straddress4_[0]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x08);
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x42)));
- lease->valid_lft_ = 8677;
- lease->cltt_ = 168256;
- lease->subnet_id_ = 23;
-
- } else if (address == straddress4_[1]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x19);
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x53)));
- lease->valid_lft_ = 3677;
- lease->cltt_ = 123456;
- lease->subnet_id_ = 73;
-
- } else if (address == straddress4_[2]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x2a);
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x64)));
- lease->valid_lft_ = 5412;
- lease->cltt_ = 234567;
- lease->subnet_id_ = 73; // Same as lease 1
-
- } else if (address == straddress4_[3]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x19); // Same as lease 1
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x75)));
-
- // The times used in the next tests are deliberately restricted - we
- // should be able to cope with valid lifetimes up to 0xffffffff.
- // However, this will lead to overflows.
- // @TODO: test overflow conditions when code has been fixed
- lease->valid_lft_ = 7000;
- lease->cltt_ = 234567;
- lease->subnet_id_ = 37;
-
- } else if (address == straddress4_[4]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x4c);
- // Same ClientId as straddr4_[1]
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
- lease->valid_lft_ = 7736;
- lease->cltt_ = 222456;
- lease->subnet_id_ = 85;
-
- } else if (address == straddress4_[5]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x19); // Same as lease 1
- // Same ClientId and IAID as straddress4_1
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
- lease->valid_lft_ = 7832;
- lease->cltt_ = 227476;
- lease->subnet_id_ = 175;
-
- } else if (address == straddress4_[6]) {
- lease->hwaddr_ = vector<uint8_t>(6, 0x6e);
- // Same ClientId as straddress4_1
- lease->client_id_ = ClientIdPtr(
- new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
- lease->valid_lft_ = 1832;
- lease->cltt_ = 627476;
- lease->subnet_id_ = 112;
-
- } else if (address == straddress4_[7]) {
- lease->hwaddr_ = vector<uint8_t>(); // Empty
- lease->client_id_ = ClientIdPtr(); // Empty
- lease->valid_lft_ = 7975;
- lease->cltt_ = 213876;
- lease->subnet_id_ = 19;
-
- } else {
- // Unknown address, return an empty pointer.
- lease.reset();
-
- }
-
- return (lease);
- }
-
- /// @brief Initialize Lease6 Fields
- ///
- /// Returns a pointer to a Lease6 structure. Different values are put into
- /// the lease according to the address passed.
- ///
- /// This is just a convenience function for the test methods.
- ///
- /// @param address Address to use for the initialization
- ///
- /// @return Lease6Ptr. This will not point to anything if the initialization
- /// failed (e.g. unknown address).
- Lease6Ptr initializeLease6(std::string address) {
- Lease6Ptr lease(new Lease6());
-
- // Set the address of the lease
- lease->addr_ = IOAddress(address);
-
- // Initialize unused fields.
- lease->t1_ = 0; // Not saved
- lease->t2_ = 0; // Not saved
- lease->fixed_ = false; // Unused
- lease->hostname_ = std::string(""); // Unused
- lease->fqdn_fwd_ = false; // Unused
- lease->fqdn_rev_ = false; // Unused
- lease->comments_ = std::string(""); // Unused
-
- // Set other parameters. For historical reasons, address 0 is not used.
- if (address == straddress6_[0]) {
- lease->type_ = Lease6::LEASE_IA_TA;
- lease->prefixlen_ = 4;
- lease->iaid_ = 142;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x77)));
- lease->preferred_lft_ = 900;
- lease->valid_lft_ = 8677;
- lease->cltt_ = 168256;
- lease->subnet_id_ = 23;
-
- } else if (address == straddress6_[1]) {
- lease->type_ = Lease6::LEASE_IA_TA;
- lease->prefixlen_ = 0;
- lease->iaid_ = 42;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
- lease->preferred_lft_ = 3600;
- lease->valid_lft_ = 3677;
- lease->cltt_ = 123456;
- lease->subnet_id_ = 73;
-
- } else if (address == straddress6_[2]) {
- lease->type_ = Lease6::LEASE_IA_PD;
- lease->prefixlen_ = 7;
- lease->iaid_ = 89;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x3a)));
- lease->preferred_lft_ = 1800;
- lease->valid_lft_ = 5412;
- lease->cltt_ = 234567;
- lease->subnet_id_ = 73; // Same as lease 1
-
- } else if (address == straddress6_[3]) {
- lease->type_ = Lease6::LEASE_IA_NA;
- lease->prefixlen_ = 28;
- lease->iaid_ = 0xfffffffe;
- vector<uint8_t> duid;
- for (uint8_t i = 31; i < 126; ++i) {
- duid.push_back(i);
- }
- lease->duid_ = DuidPtr(new DUID(duid));
-
- // The times used in the next tests are deliberately restricted - we
- // should be able to cope with valid lifetimes up to 0xffffffff.
- // However, this will lead to overflows.
- // @TODO: test overflow conditions when code has been fixed
- lease->preferred_lft_ = 7200;
- lease->valid_lft_ = 7000;
- lease->cltt_ = 234567;
- lease->subnet_id_ = 37;
-
- } else if (address == straddress6_[4]) {
- // Same DUID and IAID as straddress6_1
- lease->type_ = Lease6::LEASE_IA_PD;
- lease->prefixlen_ = 15;
- lease->iaid_ = 42;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
- lease->preferred_lft_ = 4800;
- lease->valid_lft_ = 7736;
- lease->cltt_ = 222456;
- lease->subnet_id_ = 671;
-
- } else if (address == straddress6_[5]) {
- // Same DUID and IAID as straddress6_1
- lease->type_ = Lease6::LEASE_IA_PD;
- lease->prefixlen_ = 24;
- lease->iaid_ = 42; // Same as lease 4
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
- // Same as lease 4
- lease->preferred_lft_ = 5400;
- lease->valid_lft_ = 7832;
- lease->cltt_ = 227476;
- lease->subnet_id_ = 175;
-
- } else if (address == straddress6_[6]) {
- // Same DUID as straddress6_1
- lease->type_ = Lease6::LEASE_IA_PD;
- lease->prefixlen_ = 24;
- lease->iaid_ = 93;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
- // Same as lease 4
- lease->preferred_lft_ = 5400;
- lease->valid_lft_ = 1832;
- lease->cltt_ = 627476;
- lease->subnet_id_ = 112;
-
- } else if (address == straddress6_[7]) {
- // Same IAID as straddress6_1
- lease->type_ = Lease6::LEASE_IA_PD;
- lease->prefixlen_ = 24;
- lease->iaid_ = 42;
- lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0xe5)));
- lease->preferred_lft_ = 5600;
- lease->valid_lft_ = 7975;
- lease->cltt_ = 213876;
- lease->subnet_id_ = 19;
-
- } else {
- // Unknown address, return an empty pointer.
- lease.reset();
-
- }
-
- return (lease);
- }
-
- /// @brief Check Leases present and different
- ///
- /// Checks a vector of lease pointers and ensures that all the leases
- /// they point to are present and different. If not, a GTest assertion
- /// will fail.
- ///
- /// @param leases Vector of pointers to leases
- template <typename T>
- void checkLeasesDifferent(const std::vector<T>& leases) const {
-
- // Check they were created
- for (int i = 0; i < leases.size(); ++i) {
- ASSERT_TRUE(leases[i]);
- }
-
- // Check they are different
- for (int i = 0; i < (leases.size() - 1); ++i) {
- for (int j = (i + 1); j < leases.size(); ++j) {
- stringstream s;
- s << "Comparing leases " << i << " & " << j << " for equality";
- SCOPED_TRACE(s.str());
- EXPECT_TRUE(*leases[i] != *leases[j]);
- }
- }
- }
-
- /// @brief Creates leases for the test
- ///
- /// Creates all leases for the test and checks that they are different.
- ///
- /// @return vector<Lease4Ptr> Vector of pointers to leases
- vector<Lease4Ptr> createLeases4() {
-
- // Create leases for each address
- vector<Lease4Ptr> leases;
- for (int i = 0; i < straddress4_.size(); ++i) {
- leases.push_back(initializeLease4(straddress4_[i]));
- }
- EXPECT_EQ(8, leases.size());
-
- // Check all were created and that they are different.
- checkLeasesDifferent(leases);
-
- return (leases);
- }
-
- /// @brief Creates leases for the test
- ///
- /// Creates all leases for the test and checks that they are different.
- ///
- /// @return vector<Lease6Ptr> Vector of pointers to leases
- vector<Lease6Ptr> createLeases6() {
-
- // Create leases for each address
- vector<Lease6Ptr> leases;
- for (int i = 0; i < straddress6_.size(); ++i) {
- leases.push_back(initializeLease6(straddress6_[i]));
- }
- EXPECT_EQ(8, leases.size());
-
- // Check all were created and that they are different.
- checkLeasesDifferent(leases);
-
- return (leases);
- }
-
-
- // Member variables
-
- LeaseMgr* lmptr_; ///< Pointer to the lease manager
- vector<string> straddress4_; ///< String forms of IPv4 addresses
- vector<IOAddress> ioaddress4_; ///< IOAddress forms of IPv4 addresses
- vector<string> straddress6_; ///< String forms of IPv6 addresses
- vector<IOAddress> ioaddress6_; ///< IOAddress forms of IPv6 addresses
};
/// @brief Check that database can be opened
@@ -779,6 +441,31 @@ TEST_F(MySqlLeaseMgrTest, lease4NullClientId) {
}
+/// @brief Verify that too long hostname for Lease4 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(MySqlLeaseMgrTest, lease4InvalidHostname) {
+ // Get the leases to be used for the test.
+ vector<Lease4Ptr> leases = createLeases4();
+
+ // Create a dummy hostname, consisting of 255 characters.
+ leases[1]->hostname_.assign(255, 'a');
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // The new lease must be in the database.
+ Lease4Ptr l_returned = lmptr_->getLease4(ioaddress4_[1]);
+ detailCompareLease(leases[1], l_returned);
+
+ // Let's delete the lease, so as we can try to add it again with
+ // invalid hostname.
+ EXPECT_TRUE(lmptr_->deleteLease(ioaddress4_[1]));
+
+ // Create a hostname with 256 characters. It should not be accepted.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
/// @brief Basic Lease6 Checks
///
/// Checks that the addLease, getLease6 (by address) and deleteLease (with an
@@ -797,15 +484,15 @@ TEST_F(MySqlLeaseMgrTest, basicLease6) {
// Reopen the database to ensure that they actually got stored.
reopen();
- Lease6Ptr l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
- l_returned = lmptr_->getLease6(ioaddress6_[2]);
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[2], l_returned);
- l_returned = lmptr_->getLease6(ioaddress6_[3]);
+ l_returned = lmptr_->getLease6(leasetype6_[3], ioaddress6_[3]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[3], l_returned);
@@ -815,16 +502,41 @@ TEST_F(MySqlLeaseMgrTest, basicLease6) {
// Delete a lease, check that it's gone, and that we can't delete it
// a second time.
EXPECT_TRUE(lmptr_->deleteLease(ioaddress6_[1]));
- l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
EXPECT_FALSE(l_returned);
EXPECT_FALSE(lmptr_->deleteLease(ioaddress6_[1]));
// Check that the second address is still there.
- l_returned = lmptr_->getLease6(ioaddress6_[2]);
+ l_returned = lmptr_->getLease6(leasetype6_[2], ioaddress6_[2]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[2], l_returned);
}
+/// @brief Verify that too long hostname for Lease6 is not accepted.
+///
+/// Checks that the it is not possible to create a lease when the hostname
+/// length exceeds 255 characters.
+TEST_F(MySqlLeaseMgrTest, lease6InvalidHostname) {
+ // Get the leases to be used for the test.
+ vector<Lease6Ptr> leases = createLeases6();
+
+ // Create a dummy hostname, consisting of 255 characters.
+ leases[1]->hostname_.assign(255, 'a');
+ ASSERT_TRUE(lmptr_->addLease(leases[1]));
+
+ // The new lease must be in the database.
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
+ detailCompareLease(leases[1], l_returned);
+
+ // Let's delete the lease, so as we can try to add it again with
+ // invalid hostname.
+ EXPECT_TRUE(lmptr_->deleteLease(ioaddress6_[1]));
+
+ // Create a hostname with 256 characters. It should not be accepted.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->addLease(leases[1]), DbOperationError);
+}
+
/// @brief Check GetLease4 methods - access by Hardware Address
///
/// Adds leases to the database and checks that they can be accessed via
@@ -837,7 +549,7 @@ TEST_F(MySqlLeaseMgrTest, getLease4Hwaddr) {
}
// Get the leases matching the hardware address of lease 1
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
HWAddr tmp(leases[1]->hwaddr_, HTYPE_ETHER);
Lease4Collection returned = lmptr_->getLease4(tmp);
@@ -856,14 +568,14 @@ TEST_F(MySqlLeaseMgrTest, getLease4Hwaddr) {
EXPECT_EQ(straddress4_[5], addresses[2]);
// Repeat test with just one expected match
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
returned = lmptr_->getLease4(HWAddr(leases[2]->hwaddr_, HTYPE_ETHER));
ASSERT_EQ(1, returned.size());
detailCompareLease(leases[2], *returned.begin());
// Check that an empty vector is valid
EXPECT_TRUE(leases[7]->hwaddr_.empty());
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
returned = lmptr_->getLease4(HWAddr(leases[7]->hwaddr_, HTYPE_ETHER));
ASSERT_EQ(1, returned.size());
detailCompareLease(leases[7], *returned.begin());
@@ -887,8 +599,8 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSize) {
for (uint8_t i = 0; i <= HWAddr::MAX_HWADDR_LEN; ++i) {
leases[1]->hwaddr_.resize(i, i);
EXPECT_TRUE(lmptr_->addLease(leases[1]));
- // @todo: Simply use HWAddr directly once 2589 is implemented
- Lease4Collection returned =
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Collection returned =
lmptr_->getLease4(HWAddr(leases[1]->hwaddr_, HTYPE_ETHER));
ASSERT_EQ(1, returned.size());
@@ -898,7 +610,7 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSize) {
// Database should not let us add one that is too big
// (The 42 is a random value put in each byte of the address.)
- // @todo: 2589 will make this test impossible
+ /// @todo: 2589 will make this test impossible
leases[1]->hwaddr_.resize(HWAddr::MAX_HWADDR_LEN + 100, 42);
EXPECT_THROW(lmptr_->addLease(leases[1]), isc::dhcp::DbOperationError);
}
@@ -916,8 +628,8 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetId) {
// Get the leases matching the hardware address of lease 1 and
// subnet ID of lease 1. Result should be a single lease - lease 1.
- // @todo: Simply use HWAddr directly once 2589 is implemented
- Lease4Ptr returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Ptr returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
HTYPE_ETHER), leases[1]->subnet_id_);
ASSERT_TRUE(returned);
@@ -925,7 +637,7 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetId) {
// Try for a match to the hardware address of lease 1 and the wrong
// subnet ID.
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_, HTYPE_ETHER),
leases[1]->subnet_id_ + 1);
EXPECT_FALSE(returned);
@@ -933,14 +645,14 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetId) {
// Try for a match to the subnet ID of lease 1 (and lease 4) but
// the wrong hardware address.
vector<uint8_t> invalid_hwaddr(15, 0x77);
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
returned = lmptr_->getLease4(HWAddr(invalid_hwaddr, HTYPE_ETHER),
leases[1]->subnet_id_);
EXPECT_FALSE(returned);
// Try for a match to an unknown hardware address and an unknown
// subnet ID.
- // @todo: Simply use HWAddr directly once 2589 is implemented
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
returned = lmptr_->getLease4(HWAddr(invalid_hwaddr, HTYPE_ETHER),
leases[1]->subnet_id_ + 1);
EXPECT_FALSE(returned);
@@ -953,17 +665,12 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetId) {
EXPECT_TRUE(lmptr_->deleteLease(leases[2]->addr_));
leases[1]->addr_ = leases[2]->addr_;
EXPECT_TRUE(lmptr_->addLease(leases[1]));
- // @todo: Simply use HWAddr directly once 2589 is implemented
- EXPECT_THROW(returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
- HTYPE_ETHER),
- leases[1]->subnet_id_),
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ EXPECT_THROW(returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
+ HTYPE_ETHER),
+ leases[1]->subnet_id_),
isc::dhcp::MultipleRecords);
- // Delete all leases in the database
- for (int i = 0; ADDRESS4[i] != NULL; ++i) {
- IOAddress addr(ADDRESS4[i]);
- (void) lmptr_->deleteLease(addr);
- }
}
// @brief Get lease4 by hardware address and subnet ID (2)
@@ -980,9 +687,9 @@ TEST_F(MySqlLeaseMgrTest, getLease4HwaddrSubnetIdSize) {
for (uint8_t i = 0; i <= HWAddr::MAX_HWADDR_LEN; ++i) {
leases[1]->hwaddr_.resize(i, i);
EXPECT_TRUE(lmptr_->addLease(leases[1]));
- // @todo: Simply use HWAddr directly once 2589 is implemented
- Lease4Ptr returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
- HTYPE_ETHER),
+ /// @todo: Simply use HWAddr directly once 2589 is implemented
+ Lease4Ptr returned = lmptr_->getLease4(HWAddr(leases[1]->hwaddr_,
+ HTYPE_ETHER),
leases[1]->subnet_id_);
ASSERT_TRUE(returned);
detailCompareLease(leases[1], returned);
@@ -1117,7 +824,7 @@ TEST_F(MySqlLeaseMgrTest, getLease4ClientIdSubnetId) {
///
/// Adds leases to the database and checks that they can be accessed via
/// a combination of DIUID and IAID.
-TEST_F(MySqlLeaseMgrTest, getLease6DuidIaid) {
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidIaid) {
// Get the leases to be used for the test.
vector<Lease6Ptr> leases = createLeases6();
ASSERT_LE(6, leases.size()); // Expect to access leases 0 through 5
@@ -1128,11 +835,12 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaid) {
}
// Get the leases matching the DUID and IAID of lease[1].
- Lease6Collection returned = lmptr_->getLease6(*leases[1]->duid_,
- leases[1]->iaid_);
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[1],
+ *leases[1]->duid_,
+ leases[1]->iaid_);
- // Should be three leases, matching leases[1], [4] and [5].
- ASSERT_EQ(3, returned.size());
+ // Should be two leases, matching leases[1] and [4].
+ ASSERT_EQ(2, returned.size());
// Easiest way to check is to look at the addresses.
vector<string> addresses;
@@ -1143,25 +851,25 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaid) {
sort(addresses.begin(), addresses.end());
EXPECT_EQ(straddress6_[1], addresses[0]);
EXPECT_EQ(straddress6_[4], addresses[1]);
- EXPECT_EQ(straddress6_[5], addresses[2]);
// Check that nothing is returned when either the IAID or DUID match
// nothing.
- returned = lmptr_->getLease6(*leases[1]->duid_, leases[1]->iaid_ + 1);
+ returned = lmptr_->getLeases6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_ + 1);
EXPECT_EQ(0, returned.size());
// Alter the leases[1] DUID to match nothing in the database.
vector<uint8_t> duid_vector = leases[1]->duid_->getDuid();
++duid_vector[0];
DUID new_duid(duid_vector);
- returned = lmptr_->getLease6(new_duid, leases[1]->iaid_);
+ returned = lmptr_->getLeases6(leasetype6_[1], new_duid, leases[1]->iaid_);
EXPECT_EQ(0, returned.size());
}
// @brief Get Lease4 by DUID and IAID (2)
//
// Check that the system can cope with a DUID of any size.
-TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSize) {
+TEST_F(MySqlLeaseMgrTest, getLeases6DuidIaidSize) {
// Create leases, although we need only one.
vector<Lease6Ptr> leases = createLeases6();
@@ -1178,8 +886,9 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSize) {
vector<uint8_t> duid_vec(i, i);
leases[1]->duid_.reset(new DUID(duid_vec));
EXPECT_TRUE(lmptr_->addLease(leases[1]));
- Lease6Collection returned = lmptr_->getLease6(*leases[1]->duid_,
- leases[1]->iaid_);
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[1],
+ *leases[1]->duid_,
+ leases[1]->iaid_);
ASSERT_EQ(1, returned.size());
detailCompareLease(leases[1], *returned.begin());
(void) lmptr_->deleteLease(leases[1]->addr_);
@@ -1190,6 +899,104 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSize) {
// tests.
}
+/// @brief Check that getLease6 methods discriminate by lease type.
+///
+/// Adds six leases, two per lease type all with the same duid and iad but
+/// with alternating subnet_ids.
+/// It then verifies that all of getLeases6() method variants correctly
+/// discriminate between the leases based on lease type alone.
+TEST_F(MySqlLeaseMgrTest, lease6LeaseTypeCheck) {
+
+ Lease6Ptr empty_lease(new Lease6());
+
+ DuidPtr duid(new DUID(vector<uint8_t>(8, 0x77)));
+
+ // Initialize unused fields.
+ empty_lease->t1_ = 0; // Not saved
+ empty_lease->t2_ = 0; // Not saved
+ empty_lease->fixed_ = false; // Unused
+ empty_lease->comments_ = std::string(""); // Unused
+ empty_lease->iaid_ = 142;
+ empty_lease->duid_ = DuidPtr(new DUID(*duid));
+ empty_lease->subnet_id_ = 23;
+ empty_lease->preferred_lft_ = 100;
+ empty_lease->valid_lft_ = 100;
+ empty_lease->cltt_ = 100;
+ empty_lease->fqdn_fwd_ = true;
+ empty_lease->fqdn_rev_ = true;
+ empty_lease->hostname_ = "myhost.example.com.";
+ empty_lease->prefixlen_ = 4;
+
+ // Make Two leases per lease type, all with the same DUID, IAID but
+ // alternate the subnet_ids.
+ vector<Lease6Ptr> leases;
+ for (int i = 0; i < 6; ++i) {
+ Lease6Ptr lease(new Lease6(*empty_lease));
+ lease->type_ = leasetype6_[i / 2];
+ lease->addr_ = IOAddress(straddress6_[i]);
+ lease->subnet_id_ += (i % 2);
+ leases.push_back(lease);
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ }
+
+ // Verify getting a single lease by type and address.
+ for (int i = 0; i < 6; ++i) {
+ // Look for exact match for each lease type.
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[i / 2],
+ leases[i]->addr_);
+ // We should match one per lease type.
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(*returned == *leases[i]);
+
+ // Same address but wrong lease type, should not match.
+ returned = lmptr_->getLease6(leasetype6_[i / 2 + 1], leases[i]->addr_);
+ ASSERT_FALSE(returned);
+ }
+
+ // Verify getting a collection of leases by type, DUID, and IAID.
+ // Iterate over the lease types, asking for leases based on
+ // lease type, DUID, and IAID.
+ for (int i = 0; i < 3; ++i) {
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[i],
+ *duid, 142);
+ // We should match two per lease type.
+ ASSERT_EQ(2, returned.size());
+
+ // Collection order returned is not guaranteed.
+ // Easiest way to check is to look at the addresses.
+ vector<string> addresses;
+ for (Lease6Collection::const_iterator it = returned.begin();
+ it != returned.end(); ++it) {
+ addresses.push_back((*it)->addr_.toText());
+ }
+ sort(addresses.begin(), addresses.end());
+
+ // Now verify that the lease addresses match.
+ EXPECT_EQ(addresses[0], leases[(i * 2)]->addr_.toText());
+ EXPECT_EQ(addresses[1], leases[(i * 2 + 1)]->addr_.toText());
+ }
+
+ // Verify getting a collection of leases by type, DUID, IAID, and subnet id.
+ // Iterate over the lease types, asking for leases based on
+ // lease type, DUID, IAID, and subnet_id.
+ for (int i = 0; i < 3; ++i) {
+ Lease6Collection returned = lmptr_->getLeases6(leasetype6_[i],
+ *duid, 142, 23);
+ // We should match one per lease type.
+ ASSERT_EQ(1, returned.size());
+ EXPECT_TRUE(*(returned[0]) == *leases[i * 2]);
+ }
+
+ // Verify getting a single lease by type, duid, iad, and subnet id.
+ for (int i = 0; i < 6; ++i) {
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[i / 2],
+ *duid, 142, (23 + (i % 2)));
+ // We should match one per lease type.
+ ASSERT_TRUE(returned);
+ EXPECT_TRUE(*returned == *leases[i]);
+ }
+}
+
/// @brief Check GetLease6 methods - access by DUID/IAID/SubnetID
///
/// Adds leases to the database and checks that they can be accessed via
@@ -1202,7 +1009,7 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetId) {
}
// Get the leases matching the DUID and IAID of lease[1].
- Lease6Ptr returned = lmptr_->getLease6(*leases[1]->duid_,
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
leases[1]->iaid_,
leases[1]->subnet_id_);
ASSERT_TRUE(returned);
@@ -1210,23 +1017,24 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetId) {
// Modify each of the three parameters (DUID, IAID, Subnet ID) and
// check that nothing is returned.
- returned = lmptr_->getLease6(*leases[1]->duid_, leases[1]->iaid_ + 1,
- leases[1]->subnet_id_);
+ returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_ + 1, leases[1]->subnet_id_);
EXPECT_FALSE(returned);
- returned = lmptr_->getLease6(*leases[1]->duid_, leases[1]->iaid_,
- leases[1]->subnet_id_ + 1);
+ returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
+ leases[1]->iaid_, leases[1]->subnet_id_ + 1);
EXPECT_FALSE(returned);
// Alter the leases[1] DUID to match nothing in the database.
vector<uint8_t> duid_vector = leases[1]->duid_->getDuid();
++duid_vector[0];
DUID new_duid(duid_vector);
- returned = lmptr_->getLease6(new_duid, leases[1]->iaid_,
+ returned = lmptr_->getLease6(leasetype6_[1], new_duid, leases[1]->iaid_,
leases[1]->subnet_id_);
EXPECT_FALSE(returned);
}
+
// @brief Get Lease4 by DUID, IAID & subnet ID (2)
//
// Check that the system can cope with a DUID of any size.
@@ -1247,7 +1055,7 @@ TEST_F(MySqlLeaseMgrTest, getLease6DuidIaidSubnetIdSize) {
vector<uint8_t> duid_vec(i, i);
leases[1]->duid_.reset(new DUID(duid_vec));
EXPECT_TRUE(lmptr_->addLease(leases[1]));
- Lease6Ptr returned = lmptr_->getLease6(*leases[1]->duid_,
+ Lease6Ptr returned = lmptr_->getLease6(leasetype6_[1], *leases[1]->duid_,
leases[1]->iaid_,
leases[1]->subnet_id_);
ASSERT_TRUE(returned);
@@ -1273,6 +1081,9 @@ TEST_F(MySqlLeaseMgrTest, updateLease4) {
// Modify some fields in lease 1 (not the address) and update it.
++leases[1]->subnet_id_;
leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
lmptr_->updateLease4(leases[1]);
// ... and check what is returned is what is expected.
@@ -1299,6 +1110,10 @@ TEST_F(MySqlLeaseMgrTest, updateLease4) {
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
+ // Try to update the lease with the too long hostname.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->updateLease4(leases[1]), isc::dhcp::DbOperationError);
+
// Try updating a lease not in the database.
lmptr_->deleteLease(ioaddress4_[2]);
EXPECT_THROW(lmptr_->updateLease4(leases[2]), isc::dhcp::NoSuchLease);
@@ -1316,42 +1131,49 @@ TEST_F(MySqlLeaseMgrTest, updateLease6) {
EXPECT_TRUE(lmptr_->addLease(leases[1]));
lmptr_->commit();
- Lease6Ptr l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ Lease6Ptr l_returned = lmptr_->getLease6(leasetype6_[1], ioaddress6_[1]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
// Modify some fields in lease 1 (not the address) and update it.
++leases[1]->iaid_;
- leases[1]->type_ = Lease6::LEASE_IA_PD;
+ leases[1]->type_ = Lease::TYPE_PD;
leases[1]->valid_lft_ *= 2;
+ leases[1]->hostname_ = "modified.hostname.v6.";
+ leases[1]->fqdn_fwd_ = !leases[1]->fqdn_fwd_;
+ leases[1]->fqdn_rev_ = !leases[1]->fqdn_rev_;;
lmptr_->updateLease6(leases[1]);
lmptr_->commit();
// ... and check what is returned is what is expected.
l_returned.reset();
- l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ l_returned = lmptr_->getLease6(Lease::TYPE_PD, ioaddress6_[1]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
// Alter the lease again and check.
++leases[1]->iaid_;
- leases[1]->type_ = Lease6::LEASE_IA_TA;
+ leases[1]->type_ = Lease::TYPE_TA;
leases[1]->cltt_ += 6;
leases[1]->prefixlen_ = 93;
lmptr_->updateLease6(leases[1]);
l_returned.reset();
- l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ l_returned = lmptr_->getLease6(Lease::TYPE_TA, ioaddress6_[1]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
// Check we can do an update without changing data.
lmptr_->updateLease6(leases[1]);
l_returned.reset();
- l_returned = lmptr_->getLease6(ioaddress6_[1]);
+ l_returned = lmptr_->getLease6(Lease::TYPE_TA, ioaddress6_[1]);
ASSERT_TRUE(l_returned);
detailCompareLease(leases[1], l_returned);
+ // Try to update the lease with the too long hostname.
+ leases[1]->hostname_.assign(256, 'a');
+ EXPECT_THROW(lmptr_->updateLease6(leases[1]), isc::dhcp::DbOperationError);
+
// Try updating a lease not in the database.
EXPECT_THROW(lmptr_->updateLease6(leases[2]), isc::dhcp::NoSuchLease);
}
diff --git a/src/lib/dhcpsrv/tests/pool_unittest.cc b/src/lib/dhcpsrv/tests/pool_unittest.cc
index 4e18a3c..402ca2a 100644
--- a/src/lib/dhcpsrv/tests/pool_unittest.cc
+++ b/src/lib/dhcpsrv/tests/pool_unittest.cc
@@ -39,14 +39,14 @@ TEST(Pool4Test, constructor_first_last) {
EXPECT_EQ(IOAddress("192.0.2.255"), pool1.getLastAddress());
// This is Pool4, IPv6 addresses do not belong here
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::1"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::1"),
IOAddress("192.168.0.5")), BadValue);
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("192.168.0.2"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.168.0.2"),
IOAddress("2001:db8::1")), BadValue);
// Should throw. Range should be 192.0.2.1-192.0.2.2, not
// the other way around.
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("192.0.2.2"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.0.2.2"),
IOAddress("192.0.2.1")), BadValue);
}
@@ -99,57 +99,68 @@ TEST(Pool4Test, unique_id) {
}
}
}
-
}
+// Simple check if toText returns reasonable values
+TEST(Poo4Test,toText) {
+ Pool4 pool1(IOAddress("192.0.2.7"), IOAddress("192.0.2.17"));
+ EXPECT_EQ("type=V4, 192.0.2.7-192.0.2.17", pool1.toText());
+
+ Pool4 pool2(IOAddress("192.0.2.128"), 28);
+ EXPECT_EQ("type=V4, 192.0.2.128-192.0.2.143", pool2.toText());
+}
TEST(Pool6Test, constructor_first_last) {
// let's construct 2001:db8:1:: - 2001:db8:1::ffff:ffff:ffff:ffff pool
- Pool6 pool1(Pool6::TYPE_IA, IOAddress("2001:db8:1::"),
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::"),
IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"));
- EXPECT_EQ(Pool6::TYPE_IA, pool1.getType());
+ EXPECT_EQ(Lease::TYPE_NA, pool1.getType());
EXPECT_EQ(IOAddress("2001:db8:1::"), pool1.getFirstAddress());
EXPECT_EQ(IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"),
pool1.getLastAddress());
// This is Pool6, IPv4 addresses do not belong here
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::1"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::1"),
IOAddress("192.168.0.5")), BadValue);
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("192.168.0.2"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.168.0.2"),
IOAddress("2001:db8::1")), BadValue);
// Should throw. Range should be 2001:db8::1 - 2001:db8::2, not
// the other way around.
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::2"),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::2"),
IOAddress("2001:db8::1")), BadValue);
}
TEST(Pool6Test, constructor_prefix_len) {
// let's construct 2001:db8:1::/96 pool
- Pool6 pool1(Pool6::TYPE_IA, IOAddress("2001:db8:1::"), 96);
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 96);
- EXPECT_EQ(Pool6::TYPE_IA, pool1.getType());
+ EXPECT_EQ(Lease::TYPE_NA, pool1.getType());
EXPECT_EQ("2001:db8:1::", pool1.getFirstAddress().toText());
EXPECT_EQ("2001:db8:1::ffff:ffff", pool1.getLastAddress().toText());
// No such thing as /130 prefix
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::"), 130),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 130),
BadValue);
// /0 prefix does not make sense
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::"), 0),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 0),
BadValue);
// This is Pool6, IPv4 addresses do not belong here
- EXPECT_THROW(Pool6(Pool6::TYPE_IA, IOAddress("192.168.0.2"), 96),
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("192.168.0.2"), 96),
+ BadValue);
+
+ // Delegated prefix length for addresses must be /128
+ EXPECT_THROW(Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"), 96, 125),
BadValue);
}
TEST(Pool6Test, in_range) {
- Pool6 pool1(Pool6::TYPE_IA, IOAddress("2001:db8:1::1"),
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8:1::1"),
IOAddress("2001:db8:1::f"));
EXPECT_FALSE(pool1.inRange(IOAddress("2001:db8:1::")));
@@ -160,6 +171,65 @@ TEST(Pool6Test, in_range) {
EXPECT_FALSE(pool1.inRange(IOAddress("::")));
}
+// Checks that Prefix Delegation pools are handled properly
+TEST(Pool6Test, PD) {
+
+ // Let's construct 2001:db8:1::/96 PD pool, split into /112 prefixes
+ Pool6 pool1(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+
+ EXPECT_EQ(Lease::TYPE_PD, pool1.getType());
+ EXPECT_EQ(112, pool1.getLength());
+ EXPECT_EQ("2001:db8:1::", pool1.getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool1.getLastAddress().toText());
+
+ // Check that it's not possible to have min-max range for PD
+ EXPECT_THROW(Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::f")), BadValue);
+
+ // Check that it's not allowed to delegate bigger prefix than the pool
+ // Let's try to split /64 prefix into /56 chunks (should be impossible)
+ EXPECT_THROW(Pool6 pool3(Lease::TYPE_PD, IOAddress("2001:db8:1::"),
+ 64, 56), BadValue);
+
+ // It should be possible to have a pool split into just a single chunk
+ // Let's try to split 2001:db8:1::/77 into a single /77 delegated prefix
+ EXPECT_NO_THROW(Pool6 pool4(Lease::TYPE_PD, IOAddress("2001:db8:1::"),
+ 77, 77));
+}
+
+// Checks that temporary address pools are handled properly
+TEST(Pool6Test, TA) {
+ // Note: since we defined TA pool types during PD work, we can test it
+ // now. Although the configuration to take advantage of it is not
+ // planned for now, we will support it some day.
+
+ // Let's construct 2001:db8:1::/96 temporary addresses
+ Pool6Ptr pool1;
+ EXPECT_NO_THROW(pool1.reset(new Pool6(Lease::TYPE_TA,
+ IOAddress("2001:db8:1::"), 96)));
+
+ // Check that TA range can be only defined for single addresses
+ EXPECT_THROW(Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1::"), 96, 127),
+ BadValue);
+
+ ASSERT_TRUE(pool1);
+ EXPECT_EQ(Lease::TYPE_TA, pool1->getType());
+ EXPECT_EQ(128, pool1->getLength()); // singular addresses, not prefixes
+ EXPECT_EQ("2001:db8:1::", pool1->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::ffff:ffff", pool1->getLastAddress().toText());
+
+ // Check that it's possible to have min-max range for TA
+ Pool6Ptr pool2;
+ EXPECT_NO_THROW(pool2.reset(new Pool6(Lease::TYPE_TA,
+ IOAddress("2001:db8:1::1"),
+ IOAddress("2001:db8:1::f"))));
+ ASSERT_TRUE(pool2);
+ EXPECT_EQ(Lease::TYPE_TA, pool2->getType());
+ EXPECT_EQ(128, pool2->getLength()); // singular addresses, not prefixes
+ EXPECT_EQ("2001:db8:1::1", pool2->getFirstAddress().toText());
+ EXPECT_EQ("2001:db8:1::f", pool2->getLastAddress().toText());
+}
+
// This test creates 100 pools and verifies that their IDs are unique.
TEST(Pool6Test, unique_id) {
@@ -167,7 +237,7 @@ TEST(Pool6Test, unique_id) {
std::vector<Pool6Ptr> pools;
for (int i = 0; i < num_pools; ++i) {
- pools.push_back(Pool6Ptr(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1::"),
+ pools.push_back(Pool6Ptr(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1::"),
IOAddress("2001:db8:1::ffff:ffff:ffff:ffff"))));
}
@@ -181,4 +251,16 @@ TEST(Pool6Test, unique_id) {
}
+// Simple check if toText returns reasonable values
+TEST(Poo6Test,toText) {
+ Pool6 pool1(Lease::TYPE_NA, IOAddress("2001:db8::1"),
+ IOAddress("2001:db8::2"));
+ EXPECT_EQ("type=IA_NA, 2001:db8::1-2001:db8::2, delegated_len=128",
+ pool1.toText());
+
+ Pool6 pool2(Lease::TYPE_PD, IOAddress("2001:db8:1::"), 96, 112);
+ EXPECT_EQ("type=IA_PD, 2001:db8:1::-2001:db8:1::ffff:ffff, delegated_len=112",
+ pool2.toText());
+}
+
}; // end of anonymous namespace
diff --git a/src/lib/dhcpsrv/tests/schema_copy.h b/src/lib/dhcpsrv/tests/schema_copy.h
index 48a11ca..0ccb74e 100644
--- a/src/lib/dhcpsrv/tests/schema_copy.h
+++ b/src/lib/dhcpsrv/tests/schema_copy.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -42,15 +42,23 @@ const char* destroy_statement[] = {
// Creation of the new tables.
const char* create_statement[] = {
+ "START TRANSACTION",
"CREATE TABLE lease4 ("
"address INT UNSIGNED PRIMARY KEY NOT NULL,"
"hwaddr VARBINARY(20),"
"client_id VARBINARY(128),"
"valid_lifetime INT UNSIGNED,"
"expire TIMESTAMP,"
- "subnet_id INT UNSIGNED"
+ "subnet_id INT UNSIGNED,"
+ "fqdn_fwd BOOL,"
+ "fqdn_rev BOOL,"
+ "hostname VARCHAR(255)"
") ENGINE = INNODB",
+ "CREATE INDEX lease4_by_hwaddr_subnet_id ON lease4 (hwaddr, subnet_id)",
+
+ "CREATE INDEX lease4_by_client_id_subnet_id ON lease4 (client_id, subnet_id)",
+
"CREATE TABLE lease6 ("
"address VARCHAR(39) PRIMARY KEY NOT NULL,"
"duid VARBINARY(128),"
@@ -60,9 +68,14 @@ const char* create_statement[] = {
"pref_lifetime INT UNSIGNED,"
"lease_type TINYINT,"
"iaid INT UNSIGNED,"
- "prefix_len TINYINT UNSIGNED"
+ "prefix_len TINYINT UNSIGNED,"
+ "fqdn_fwd BOOL,"
+ "fqdn_rev BOOL,"
+ "hostname VARCHAR(255)"
") ENGINE = INNODB",
+ "CREATE INDEX lease6_by_iaid_subnet_id_duid ON lease6 (iaid, subnet_id, duid)",
+
"CREATE TABLE lease6_types ("
"lease_type TINYINT PRIMARY KEY NOT NULL,"
"name VARCHAR(5)"
@@ -78,6 +91,7 @@ const char* create_statement[] = {
")",
"INSERT INTO schema_version VALUES (1, 0)",
+ "COMMIT",
NULL
};
diff --git a/src/lib/dhcpsrv/tests/subnet_unittest.cc b/src/lib/dhcpsrv/tests/subnet_unittest.cc
index 1f0ef8b..d0dd57a 100644
--- a/src/lib/dhcpsrv/tests/subnet_unittest.cc
+++ b/src/lib/dhcpsrv/tests/subnet_unittest.cc
@@ -16,6 +16,7 @@
#include <asiolink/io_address.h>
#include <dhcp/option.h>
+#include <dhcp/dhcp6.h>
#include <dhcpsrv/subnet.h>
#include <exceptions/exceptions.h>
@@ -57,6 +58,24 @@ TEST(Subnet4Test, in_range) {
EXPECT_FALSE(subnet.inRange(IOAddress("255.255.255.255")));
}
+// Checks whether siaddr field can be set and retrieved correctly.
+TEST(Subnet4Test, siaddr) {
+ Subnet4 subnet(IOAddress("192.0.2.1"), 24, 1000, 2000, 3000);
+
+ // Check if the default is 0.0.0.0
+ EXPECT_EQ("0.0.0.0", subnet.getSiaddr().toText());
+
+ // Check that we can set it up
+ EXPECT_NO_THROW(subnet.setSiaddr(IOAddress("1.2.3.4")));
+
+ // Check that we can get it back
+ EXPECT_EQ("1.2.3.4", subnet.getSiaddr().toText());
+
+ // Check that only v4 addresses are supported
+ EXPECT_THROW(subnet.setSiaddr(IOAddress("2001:db8::1")),
+ BadValue);
+}
+
TEST(Subnet4Test, Pool4InSubnet4) {
Subnet4Ptr subnet(new Subnet4(IOAddress("192.1.2.0"), 24, 1, 2, 3));
@@ -65,24 +84,25 @@ TEST(Subnet4Test, Pool4InSubnet4) {
PoolPtr pool2(new Pool4(IOAddress("192.1.2.128"), 26));
PoolPtr pool3(new Pool4(IOAddress("192.1.2.192"), 30));
- subnet->addPool(pool1);
+ EXPECT_NO_THROW(subnet->addPool(pool1));
// If there's only one pool, get that pool
- PoolPtr mypool = subnet->getPool();
+ PoolPtr mypool = subnet->getAnyPool(Lease::TYPE_V4);
EXPECT_EQ(mypool, pool1);
- subnet->addPool(pool2);
- subnet->addPool(pool3);
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+ EXPECT_NO_THROW(subnet->addPool(pool3));
// If there are more than one pool and we didn't provide hint, we
// should get the first pool
- mypool = subnet->getPool();
+ EXPECT_NO_THROW(mypool = subnet->getAnyPool(Lease::TYPE_V4));
EXPECT_EQ(mypool, pool1);
// If we provide a hint, we should get a pool that this hint belongs to
- mypool = subnet->getPool(IOAddress("192.1.2.195"));
+ EXPECT_NO_THROW(mypool = subnet->getPool(Lease::TYPE_V4,
+ IOAddress("192.1.2.195")));
EXPECT_EQ(mypool, pool3);
@@ -138,27 +158,27 @@ TEST(Subnet4Test, inRangeinPool) {
EXPECT_TRUE(subnet->inRange(IOAddress("192.1.1.1")));
// ... but it does not belong to any pool within
- EXPECT_FALSE(subnet->inPool(IOAddress("192.1.1.1")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.1.1.1")));
// the last address that is in range, but out of pool
EXPECT_TRUE(subnet->inRange(IOAddress("192.1.255.255")));
- EXPECT_FALSE(subnet->inPool(IOAddress("192.1.255.255")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.1.255.255")));
// the first address that is in range, in pool
EXPECT_TRUE(subnet->inRange(IOAddress("192.2.0.0")));
- EXPECT_TRUE (subnet->inPool(IOAddress("192.2.0.0")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.0.0")));
// let's try something in the middle as well
EXPECT_TRUE(subnet->inRange(IOAddress("192.2.3.4")));
- EXPECT_TRUE (subnet->inPool(IOAddress("192.2.3.4")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.3.4")));
// the last address that is in range, in pool
EXPECT_TRUE(subnet->inRange(IOAddress("192.2.255.255")));
- EXPECT_TRUE (subnet->inPool(IOAddress("192.2.255.255")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_V4, IOAddress("192.2.255.255")));
// the first address that is in range, but out of pool
EXPECT_TRUE(subnet->inRange(IOAddress("192.3.0.0")));
- EXPECT_FALSE(subnet->inPool(IOAddress("192.3.0.0")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_V4, IOAddress("192.3.0.0")));
}
// This test checks if the toText() method returns text representation
@@ -174,6 +194,74 @@ TEST(Subnet4Test, get) {
EXPECT_EQ(28, subnet->get().second);
}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet4Test, lastAllocated) {
+ IOAddress addr("192.0.2.17");
+
+ IOAddress last("192.0.2.255");
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.0.2.0"), 24, 1, 2, 3));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_V4, addr));
+ EXPECT_EQ(addr.toText(), subnet->getLastAllocated(Lease::TYPE_V4).toText());
+
+ // No, you can't set the last allocated IPv6 address in IPv4 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_TA, addr), BadValue);
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_PD, addr), BadValue);
+}
+
+// Checks if the V4 is the only allowed type for Pool4 and if getPool()
+// is working properly.
+TEST(Subnet4Test, PoolType) {
+
+ Subnet4Ptr subnet(new Subnet4(IOAddress("192.2.0.0"), 16, 1, 2, 3));
+
+ PoolPtr pool1(new Pool4(IOAddress("192.2.1.0"), 24));
+ PoolPtr pool2(new Pool4(IOAddress("192.2.2.0"), 24));
+ PoolPtr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::"), 64));
+ PoolPtr pool4(new Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1:4::"), 64));
+ PoolPtr pool5(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:1::"), 64));
+
+ // There should be no pools of any type by default
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_V4));
+
+ // It should not be possible to ask for V6 pools in Subnet4
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_NA), BadValue);
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_TA), BadValue);
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_PD), BadValue);
+
+ // Let's add a single V4 pool and check that it can be retrieved
+ EXPECT_NO_THROW(subnet->addPool(pool1));
+
+ // If there's only one IA pool, get that pool (without and with hint)
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_V4));
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("192.0.1.167")));
+
+ // Let's add additional V4 pool
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+
+ // Try without hints
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_V4));
+
+ // Try with valid hints
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("192.2.1.5")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_V4, IOAddress("192.2.2.254")));
+
+ // Try with bogus hints (hints should be ingored)
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_V4, IOAddress("10.1.1.1")));
+
+ // Trying to add Pool6 to Subnet4 is a big no,no!
+ EXPECT_THROW(subnet->addPool(pool3), BadValue);
+ EXPECT_THROW(subnet->addPool(pool4), BadValue);
+ EXPECT_THROW(subnet->addPool(pool5), BadValue);
+}
+
// Tests for Subnet6
TEST(Subnet6Test, constructor) {
@@ -207,52 +295,122 @@ TEST(Subnet6Test, Pool6InSubnet6) {
Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
- PoolPtr pool1(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:1::"), 64));
- PoolPtr pool2(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:2::"), 64));
- PoolPtr pool3(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:3::"), 64));
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
+ PoolPtr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:2::"), 64));
+ PoolPtr pool3(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:3::"), 64));
subnet->addPool(pool1);
// If there's only one pool, get that pool
- PoolPtr mypool = subnet->getPool();
+ PoolPtr mypool = subnet->getAnyPool(Lease::TYPE_NA);
EXPECT_EQ(mypool, pool1);
-
subnet->addPool(pool2);
subnet->addPool(pool3);
// If there are more than one pool and we didn't provide hint, we
// should get the first pool
- mypool = subnet->getPool();
+ mypool = subnet->getAnyPool(Lease::TYPE_NA);
EXPECT_EQ(mypool, pool1);
// If we provide a hint, we should get a pool that this hint belongs to
- mypool = subnet->getPool(IOAddress("2001:db8:1:3::dead:beef"));
+ mypool = subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:3::dead:beef"));
EXPECT_EQ(mypool, pool3);
}
+// Check if Subnet6 supports different types of pools properly.
+TEST(Subnet6Test, PoolTypes) {
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ PoolPtr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
+ PoolPtr pool2(new Pool6(Lease::TYPE_TA, IOAddress("2001:db8:1:2::"), 64));
+ PoolPtr pool3(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:3::"), 64));
+ PoolPtr pool4(new Pool6(Lease::TYPE_PD, IOAddress("2001:db8:1:4::"), 64));
+
+ PoolPtr pool5(new Pool4(IOAddress("192.0.2.0"), 24));
+
+ // There should be no pools of any type by default
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_PD));
+
+ // Trying to get IPv4 pool from Subnet6 is not allowed
+ EXPECT_THROW(subnet->getAnyPool(Lease::TYPE_V4), BadValue);
+
+ // Let's add a single IA pool and check that it can be retrieved
+ EXPECT_NO_THROW(subnet->addPool(pool1));
+
+ // If there's only one IA pool, get that pool
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:1::1")));
+
+ // Check if pools of different type are not returned
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(PoolPtr(), subnet->getAnyPool(Lease::TYPE_PD));
+
+ // We ask with good hints, but wrong types, should return nothing
+ EXPECT_EQ(PoolPtr(), subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:2::1")));
+ EXPECT_EQ(PoolPtr(), subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:3::1")));
+
+ // Let's add TA and PD pools
+ EXPECT_NO_THROW(subnet->addPool(pool2));
+ EXPECT_NO_THROW(subnet->addPool(pool3));
+
+ // Try without hints
+ EXPECT_EQ(pool1, subnet->getAnyPool(Lease::TYPE_NA));
+ EXPECT_EQ(pool2, subnet->getAnyPool(Lease::TYPE_TA));
+ EXPECT_EQ(pool3, subnet->getAnyPool(Lease::TYPE_PD));
+
+ // Try with valid hints
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:1::1")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:2::1")));
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:3::1")));
+
+ // Try with bogus hints (hints should be ingored)
+ EXPECT_EQ(pool1, subnet->getPool(Lease::TYPE_NA, IOAddress("2001:db8:1:7::1")));
+ EXPECT_EQ(pool2, subnet->getPool(Lease::TYPE_TA, IOAddress("2001:db8:1:7::1")));
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:7::1")));
+
+ // Let's add a second PD pool
+ EXPECT_NO_THROW(subnet->addPool(pool4));
+
+ // Without hints, it should return the first pool
+ EXPECT_EQ(pool3, subnet->getAnyPool(Lease::TYPE_PD));
+
+ // With valid hint, it should return that hint
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:3::1")));
+ EXPECT_EQ(pool4, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8:1:4::1")));
+
+ // With invalid hint, it should return the first pool
+ EXPECT_EQ(pool3, subnet->getPool(Lease::TYPE_PD, IOAddress("2001:db8::123")));
+
+ // Adding Pool4 to Subnet6 is a big no, no!
+ EXPECT_THROW(subnet->addPool(pool5), BadValue);
+}
+
TEST(Subnet6Test, Subnet6_Pool6_checks) {
Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
// this one is in subnet
- Pool6Ptr pool1(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8:1:1::"), 64));
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8:1:1::"), 64));
subnet->addPool(pool1);
// this one is larger than the subnet!
- Pool6Ptr pool2(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::"), 48));
+ Pool6Ptr pool2(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8::"), 48));
EXPECT_THROW(subnet->addPool(pool2), BadValue);
// this one is totally out of blue
- Pool6Ptr pool3(new Pool6(Pool6::TYPE_IA, IOAddress("3000::"), 16));
+ Pool6Ptr pool3(new Pool6(Lease::TYPE_NA, IOAddress("3000::"), 16));
EXPECT_THROW(subnet->addPool(pool3), BadValue);
- Pool6Ptr pool4(new Pool6(Pool6::TYPE_IA, IOAddress("4001:db8:1::"), 80));
+ Pool6Ptr pool4(new Pool6(Lease::TYPE_NA, IOAddress("4001:db8:1::"), 80));
EXPECT_THROW(subnet->addPool(pool4), BadValue);
}
@@ -457,39 +615,109 @@ TEST(Subnet6Test, getOptionDescriptor) {
}
}
+
+TEST(Subnet6Test, addVendorOptions) {
+
+ uint32_t vendor_id1 = 12345678;
+ uint32_t vendor_id2 = 87654321;
+ uint32_t vendor_id_bogus = 1111111;
+
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ // Differentiate options by their codes (100-109)
+ for (uint16_t code = 100; code < 110; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->addVendorOption(option, false, vendor_id1));
+ }
+
+ // Add 7 options to another option space. The option codes partially overlap
+ // with option codes that we have added to dhcp6 option space.
+ for (uint16_t code = 105; code < 112; ++code) {
+ OptionPtr option(new Option(Option::V6, code, OptionBuffer(10, 0xFF)));
+ ASSERT_NO_THROW(subnet->addVendorOption(option, false, vendor_id2));
+ }
+
+ // Get options from the Subnet and check if all 10 are there.
+ Subnet::OptionContainerPtr options = subnet->getVendorOptionDescriptors(vendor_id1);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(10, options->size());
+
+ // Validate codes of options added to dhcp6 option space.
+ uint16_t expected_code = 100;
+ for (Subnet::OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option);
+ EXPECT_EQ(expected_code, option_desc->option->getType());
+ ++expected_code;
+ }
+
+ options = subnet->getVendorOptionDescriptors(vendor_id2);
+ ASSERT_TRUE(options);
+ ASSERT_EQ(7, options->size());
+
+ // Validate codes of options added to isc option space.
+ expected_code = 105;
+ for (Subnet::OptionContainer::const_iterator option_desc = options->begin();
+ option_desc != options->end(); ++option_desc) {
+ ASSERT_TRUE(option_desc->option);
+ EXPECT_EQ(expected_code, option_desc->option->getType());
+ ++expected_code;
+ }
+
+ // Try to get options from a non-existing option space.
+ options = subnet->getVendorOptionDescriptors(vendor_id_bogus);
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+
+ // Delete options from all spaces.
+ subnet->delVendorOptions();
+
+ // Make sure that all options have been removed.
+ options = subnet->getVendorOptionDescriptors(vendor_id1);
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+
+ options = subnet->getVendorOptionDescriptors(vendor_id2);
+ ASSERT_TRUE(options);
+ EXPECT_TRUE(options->empty());
+}
+
+
+
// This test verifies that inRange() and inPool() methods work properly.
TEST(Subnet6Test, inRangeinPool) {
Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8::"), 32, 1, 2, 3, 4));
// this one is in subnet
- Pool6Ptr pool1(new Pool6(Pool6::TYPE_IA, IOAddress("2001:db8::10"),
+ Pool6Ptr pool1(new Pool6(Lease::TYPE_NA, IOAddress("2001:db8::10"),
IOAddress("2001:db8::20")));
subnet->addPool(pool1);
// 192.1.1.1 belongs to the subnet...
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::1")));
// ... but it does not belong to any pool within
- EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::1")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::1")));
// the last address that is in range, but out of pool
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::f")));
- EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::f")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::f")));
// the first address that is in range, in pool
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::10")));
- EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::10")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::10")));
// let's try something in the middle as well
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::18")));
- EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::18")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::18")));
// the last address that is in range, in pool
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::20")));
- EXPECT_TRUE (subnet->inPool(IOAddress("2001:db8::20")));
+ EXPECT_TRUE (subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::20")));
// the first address that is in range, but out of pool
EXPECT_TRUE(subnet->inRange(IOAddress("2001:db8::21")));
- EXPECT_FALSE(subnet->inPool(IOAddress("2001:db8::21")));
+ EXPECT_FALSE(subnet->inPool(Lease::TYPE_NA, IOAddress("2001:db8::21")));
}
// This test checks if the toText() method returns text representation
@@ -516,4 +744,54 @@ TEST(Subnet6Test, iface) {
EXPECT_EQ("en1", subnet.getIface());
}
+// This trivial test checks if the interface-id option can be set and
+// later retrieved for a subnet6 object.
+TEST(Subnet6Test, interfaceId) {
+ // Create as subnet to add options to it.
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 56, 1, 2, 3, 4));
+
+ EXPECT_FALSE(subnet->getInterfaceId());
+
+ OptionPtr option(new Option(Option::V6, D6O_INTERFACE_ID, OptionBuffer(10, 0xFF)));
+ subnet->setInterfaceId(option);
+
+ EXPECT_EQ(option, subnet->getInterfaceId());
+
+}
+
+// Checks if last allocated address/prefix is stored/retrieved properly
+TEST(Subnet6Test, lastAllocated) {
+ IOAddress ia("2001:db8:1::1");
+ IOAddress ta("2001:db8:1::abcd");
+ IOAddress pd("2001:db8:1::1234:5678");
+
+ IOAddress last("2001:db8:1::ffff:ffff:ffff:ffff");
+
+ Subnet6Ptr subnet(new Subnet6(IOAddress("2001:db8:1::"), 64, 1, 2, 3, 4));
+
+ // Check initial conditions (all should be set to the last address in range)
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set last allocated for IA
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_NA, ia));
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+
+ // TA and PD should be unchanged
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(last.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // Now set TA and PD
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_TA, ta));
+ EXPECT_NO_THROW(subnet->setLastAllocated(Lease::TYPE_PD, pd));
+
+ EXPECT_EQ(ia.toText(), subnet->getLastAllocated(Lease::TYPE_NA).toText());
+ EXPECT_EQ(ta.toText(), subnet->getLastAllocated(Lease::TYPE_TA).toText());
+ EXPECT_EQ(pd.toText(), subnet->getLastAllocated(Lease::TYPE_PD).toText());
+
+ // No, you can't set the last allocated IPv4 address in IPv6 subnet
+ EXPECT_THROW(subnet->setLastAllocated(Lease::TYPE_V4, ia), BadValue);
+}
+
};
diff --git a/src/lib/dhcpsrv/tests/test_get_callout_handle.cc b/src/lib/dhcpsrv/tests/test_get_callout_handle.cc
new file mode 100644
index 0000000..5d7cd9d
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_get_callout_handle.cc
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <dhcpsrv/callout_handle_store.h>
+#include "test_get_callout_handle.h"
+
+// Just instantiate the getCalloutHandle function and call it.
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+isc::hooks::CalloutHandlePtr
+testGetCalloutHandle(const Pkt6Ptr& pktptr) {
+ return (isc::dhcp::getCalloutHandle(pktptr));
+}
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
diff --git a/src/lib/dhcpsrv/tests/test_get_callout_handle.h b/src/lib/dhcpsrv/tests/test_get_callout_handle.h
new file mode 100644
index 0000000..f29ab42
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_get_callout_handle.h
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TEST_GET_CALLOUT_HANDLE_H
+#define TEST_GET_CALLOUT_HANDLE_H
+
+#include <dhcp/pkt6.h>
+#include <hooks/callout_handle.h>
+
+namespace isc {
+namespace dhcp {
+namespace test {
+
+/// @file
+/// @brief Get Callout Handle
+///
+/// This function is a shall around getCalloutHandle. It's purpose is to
+/// ensure that the getCalloutHandle() template function is referred to by
+/// two separate compilation units, and so test that data stored in one unit
+/// can be accessed by another. (This should be the case, but some compilers
+/// mabe be odd when it comes to template instantiation.)
+///
+/// @param pktptr Pointer to a Pkt6 object.
+///
+/// @return CalloutHandlePtr pointing to CalloutHandle associated with the
+/// Pkt6 object.
+isc::hooks::CalloutHandlePtr
+testGetCalloutHandle(const Pkt6Ptr& pktptr);
+
+} // namespace test
+} // namespace dhcp
+} // namespace isc
+
+
+#endif // TEST_GET_CALLOUT_HANDLE_H
diff --git a/src/lib/dhcpsrv/tests/test_libraries.h.in b/src/lib/dhcpsrv/tests/test_libraries.h.in
new file mode 100644
index 0000000..a2843dd
--- /dev/null
+++ b/src/lib/dhcpsrv/tests/test_libraries.h.in
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// shared library.
+
+// Library with load/unload functions creating marker files to check their
+// operation.
+static const char* CALLOUT_LIBRARY_1 = "@abs_builddir@/.libs/libco1.so";
+static const char* CALLOUT_LIBRARY_2 = "@abs_builddir@/.libs/libco2.so";
+
+// Name of a library which is not present.
+static const char* NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/lib/dhcpsrv/tests/test_utils.cc b/src/lib/dhcpsrv/tests/test_utils.cc
index ea62225..44d60ea 100644
--- a/src/lib/dhcpsrv/tests/test_utils.cc
+++ b/src/lib/dhcpsrv/tests/test_utils.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -13,19 +13,42 @@
// PERFORMANCE OF THIS SOFTWARE.
#include "test_utils.h"
+#include <asiolink/io_address.h>
#include <gtest/gtest.h>
+#include <sstream>
+
+using namespace std;
+using namespace isc::asiolink;
namespace isc {
namespace dhcp {
namespace test {
+// IPv4 and IPv6 addresses used in the tests
+const char* ADDRESS4[] = {
+ "192.0.2.0", "192.0.2.1", "192.0.2.2", "192.0.2.3",
+ "192.0.2.4", "192.0.2.5", "192.0.2.6", "192.0.2.7",
+ NULL
+};
+const char* ADDRESS6[] = {
+ "2001:db8::0", "2001:db8::1", "2001:db8::2", "2001:db8::3",
+ "2001:db8::4", "2001:db8::5", "2001:db8::6", "2001:db8::7",
+ NULL
+};
+
+// Lease types that correspond to ADDRESS6 leases
+static const Lease::Type LEASETYPE6[] = {
+ Lease::TYPE_NA, Lease::TYPE_TA, Lease::TYPE_PD, Lease::TYPE_NA,
+ Lease::TYPE_TA, Lease::TYPE_PD, Lease::TYPE_NA, Lease::TYPE_TA
+};
+
void
detailCompareLease(const Lease4Ptr& first, const Lease4Ptr& second) {
// Compare address strings. Comparison of address objects is not used, as
// odd things happen when they are different: the EXPECT_EQ macro appears to
// call the operator uint32_t() function, which causes an exception to be
// thrown for IPv6 addresses.
- EXPECT_EQ(first->addr_.toText(), second->addr_.toText());
+ EXPECT_EQ(first->addr_, second->addr_);
EXPECT_TRUE(first->hwaddr_ == second->hwaddr_);
if (first->client_id_ && second->client_id_) {
EXPECT_TRUE(*first->client_id_ == *second->client_id_);
@@ -47,6 +70,9 @@ detailCompareLease(const Lease4Ptr& first, const Lease4Ptr& second) {
EXPECT_EQ(first->valid_lft_, second->valid_lft_);
EXPECT_EQ(first->cltt_, second->cltt_);
EXPECT_EQ(first->subnet_id_, second->subnet_id_);
+ EXPECT_EQ(first->fqdn_fwd_, second->fqdn_fwd_);
+ EXPECT_EQ(first->fqdn_rev_, second->fqdn_rev_);
+ EXPECT_EQ(first->hostname_, second->hostname_);
}
void
@@ -57,7 +83,7 @@ detailCompareLease(const Lease6Ptr& first, const Lease6Ptr& second) {
// odd things happen when they are different: the EXPECT_EQ macro appears to
// call the operator uint32_t() function, which causes an exception to be
// thrown for IPv6 addresses.
- EXPECT_EQ(first->addr_.toText(), second->addr_.toText());
+ EXPECT_EQ(first->addr_, second->addr_);
EXPECT_EQ(first->prefixlen_, second->prefixlen_);
EXPECT_EQ(first->iaid_, second->iaid_);
ASSERT_TRUE(first->duid_);
@@ -67,8 +93,491 @@ detailCompareLease(const Lease6Ptr& first, const Lease6Ptr& second) {
EXPECT_EQ(first->valid_lft_, second->valid_lft_);
EXPECT_EQ(first->cltt_, second->cltt_);
EXPECT_EQ(first->subnet_id_, second->subnet_id_);
+ EXPECT_EQ(first->fqdn_fwd_, second->fqdn_fwd_);
+ EXPECT_EQ(first->fqdn_rev_, second->fqdn_rev_);
+ EXPECT_EQ(first->hostname_, second->hostname_);
+}
+
+GenericLeaseMgrTest::GenericLeaseMgrTest()
+ : lmptr_(NULL) {
+ // Initialize address strings and IOAddresses
+ for (int i = 0; ADDRESS4[i] != NULL; ++i) {
+ string addr(ADDRESS4[i]);
+ straddress4_.push_back(addr);
+ IOAddress ioaddr(addr);
+ ioaddress4_.push_back(ioaddr);
+ }
+
+ for (int i = 0; ADDRESS6[i] != NULL; ++i) {
+ string addr(ADDRESS6[i]);
+ straddress6_.push_back(addr);
+ IOAddress ioaddr(addr);
+ ioaddress6_.push_back(ioaddr);
+
+ /// Let's create different lease types. We use LEASETYPE6 values as
+ /// a template
+ leasetype6_.push_back(LEASETYPE6[i]);
+ }
}
+GenericLeaseMgrTest::~GenericLeaseMgrTest() {
+ // Does nothing. The derived classes are expected to clean up, i.e.
+ // remove the lmptr_ pointer.
+}
+
+/// @brief Initialize Lease4 Fields
+///
+/// Returns a pointer to a Lease4 structure. Different values are put into
+/// the lease according to the address passed.
+///
+/// This is just a convenience function for the test methods.
+///
+/// @param address Address to use for the initialization
+///
+/// @return Lease4Ptr. This will not point to anything if the
+/// initialization failed (e.g. unknown address).
+Lease4Ptr
+GenericLeaseMgrTest::initializeLease4(std::string address) {
+ Lease4Ptr lease(new Lease4());
+
+ // Set the address of the lease
+ lease->addr_ = IOAddress(address);
+
+ // Initialize unused fields.
+ lease->ext_ = 0; // Not saved
+ lease->t1_ = 0; // Not saved
+ lease->t2_ = 0; // Not saved
+ lease->fixed_ = false; // Unused
+ lease->comments_ = std::string(""); // Unused
+
+ // Set other parameters. For historical reasons, address 0 is not used.
+ if (address == straddress4_[0]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x08);
+ lease->client_id_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x42)));
+ lease->valid_lft_ = 8677;
+ lease->cltt_ = 168256;
+ lease->subnet_id_ = 23;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[1]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x19);
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53)));
+ lease->valid_lft_ = 3677;
+ lease->cltt_ = 123456;
+ lease->subnet_id_ = 73;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[2]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x2a);
+ lease->client_id_ = ClientIdPtr(new ClientId(vector<uint8_t>(8, 0x64)));
+ lease->valid_lft_ = 5412;
+ lease->cltt_ = 234567;
+ lease->subnet_id_ = 73; // Same as lease 1
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "";
+
+ } else if (address == straddress4_[3]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x19); // Same as lease 1
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x75)));
+
+ // The times used in the next tests are deliberately restricted - we
+ // should be able to cope with valid lifetimes up to 0xffffffff.
+ // However, this will lead to overflows.
+ // @TODO: test overflow conditions when code has been fixed
+ lease->valid_lft_ = 7000;
+ lease->cltt_ = 234567;
+ lease->subnet_id_ = 37;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress4_[4]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x4c);
+ // Same ClientId as straddr4_[1]
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 7736;
+ lease->cltt_ = 222456;
+ lease->subnet_id_ = 85;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress4_[5]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x19); // Same as lease 1
+ // Same ClientId and IAID as straddress4_1
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 7832;
+ lease->cltt_ = 227476;
+ lease->subnet_id_ = 175;
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = false;
+ lease->hostname_ = "otherhost.example.com.";
+ } else if (address == straddress4_[6]) {
+ lease->hwaddr_ = vector<uint8_t>(6, 0x6e);
+ // Same ClientId as straddress4_1
+ lease->client_id_ = ClientIdPtr(
+ new ClientId(vector<uint8_t>(8, 0x53))); // Same as lease 1
+ lease->valid_lft_ = 1832;
+ lease->cltt_ = 627476;
+ lease->subnet_id_ = 112;
+ lease->fqdn_rev_ = false;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress4_[7]) {
+ lease->hwaddr_ = vector<uint8_t>(); // Empty
+ lease->client_id_ = ClientIdPtr(); // Empty
+ lease->valid_lft_ = 7975;
+ lease->cltt_ = 213876;
+ lease->subnet_id_ = 19;
+ lease->fqdn_rev_ = true;
+ lease->fqdn_fwd_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else {
+ // Unknown address, return an empty pointer.
+ lease.reset();
+
+ }
+
+ return (lease);
+}
+
+/// @brief Initialize Lease6 Fields
+///
+/// Returns a pointer to a Lease6 structure. Different values are put into
+/// the lease according to the address passed.
+///
+/// This is just a convenience function for the test methods.
+///
+/// @param address Address to use for the initialization
+///
+/// @return Lease6Ptr. This will not point to anything if the initialization
+/// failed (e.g. unknown address).
+Lease6Ptr
+GenericLeaseMgrTest::initializeLease6(std::string address) {
+ Lease6Ptr lease(new Lease6());
+
+ // Set the address of the lease
+ lease->addr_ = IOAddress(address);
+
+ // Initialize unused fields.
+ lease->t1_ = 0; // Not saved
+ lease->t2_ = 0; // Not saved
+ lease->fixed_ = false; // Unused
+ lease->comments_ = std::string(""); // Unused
+
+ // Set other parameters. For historical reasons, address 0 is not used.
+ if (address == straddress6_[0]) {
+ lease->type_ = leasetype6_[0];
+ lease->prefixlen_ = 4;
+ lease->iaid_ = 142;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x77)));
+ lease->preferred_lft_ = 900;
+ lease->valid_lft_ = 8677;
+ lease->cltt_ = 168256;
+ lease->subnet_id_ = 23;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[1]) {
+ lease->type_ = leasetype6_[1];
+ lease->prefixlen_ = 0;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ lease->preferred_lft_ = 3600;
+ lease->valid_lft_ = 3677;
+ lease->cltt_ = 123456;
+ lease->subnet_id_ = 73;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[2]) {
+ lease->type_ = leasetype6_[2];
+ lease->prefixlen_ = 7;
+ lease->iaid_ = 89;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x3a)));
+ lease->preferred_lft_ = 1800;
+ lease->valid_lft_ = 5412;
+ lease->cltt_ = 234567;
+ lease->subnet_id_ = 73; // Same as lease 1
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[3]) {
+ lease->type_ = leasetype6_[3];
+ lease->prefixlen_ = 28;
+ lease->iaid_ = 0xfffffffe;
+ vector<uint8_t> duid;
+ for (uint8_t i = 31; i < 126; ++i) {
+ duid.push_back(i);
+ }
+ lease->duid_ = DuidPtr(new DUID(duid));
+
+ // The times used in the next tests are deliberately restricted - we
+ // should be able to cope with valid lifetimes up to 0xffffffff.
+ // However, this will lead to overflows.
+ // @TODO: test overflow conditions when code has been fixed
+ lease->preferred_lft_ = 7200;
+ lease->valid_lft_ = 7000;
+ lease->cltt_ = 234567;
+ lease->subnet_id_ = 37;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = false;
+ lease->hostname_ = "myhost.example.com.";
+
+ } else if (address == straddress6_[4]) {
+ // Same DUID and IAID as straddress6_1
+ lease->type_ = leasetype6_[4];
+ lease->prefixlen_ = 15;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ lease->preferred_lft_ = 4800;
+ lease->valid_lft_ = 7736;
+ lease->cltt_ = 222456;
+ lease->subnet_id_ = 671;
+ lease->fqdn_fwd_ = true;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "otherhost.example.com.";
+
+ } else if (address == straddress6_[5]) {
+ // Same DUID and IAID as straddress6_1
+ lease->type_ = leasetype6_[5];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 42; // Same as lease 4
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ // Same as lease 4
+ lease->preferred_lft_ = 5400;
+ lease->valid_lft_ = 7832;
+ lease->cltt_ = 227476;
+ lease->subnet_id_ = 175;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+
+ } else if (address == straddress6_[6]) {
+ // Same DUID as straddress6_1
+ lease->type_ = leasetype6_[6];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 93;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0x42)));
+ // Same as lease 4
+ lease->preferred_lft_ = 5400;
+ lease->valid_lft_ = 1832;
+ lease->cltt_ = 627476;
+ lease->subnet_id_ = 112;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+
+ } else if (address == straddress6_[7]) {
+ // Same IAID as straddress6_1
+ lease->type_ = leasetype6_[7];
+ lease->prefixlen_ = 24;
+ lease->iaid_ = 42;
+ lease->duid_ = DuidPtr(new DUID(vector<uint8_t>(8, 0xe5)));
+ lease->preferred_lft_ = 5600;
+ lease->valid_lft_ = 7975;
+ lease->cltt_ = 213876;
+ lease->subnet_id_ = 19;
+ lease->fqdn_fwd_ = false;
+ lease->fqdn_rev_ = true;
+ lease->hostname_ = "hostname.example.com.";
+
+ } else {
+ // Unknown address, return an empty pointer.
+ lease.reset();
+
+ }
+
+ return (lease);
+}
+
+/// @brief Check Leases present and different
+///
+/// Checks a vector of lease pointers and ensures that all the leases
+/// they point to are present and different. If not, a GTest assertion
+/// will fail.
+///
+/// @param leases Vector of pointers to leases
+template <typename T>
+void GenericLeaseMgrTest::checkLeasesDifferent(const std::vector<T>& leases) const {
+
+ // Check they were created
+ for (int i = 0; i < leases.size(); ++i) {
+ ASSERT_TRUE(leases[i]);
+ }
+
+ // Check they are different
+ for (int i = 0; i < (leases.size() - 1); ++i) {
+ for (int j = (i + 1); j < leases.size(); ++j) {
+ stringstream s;
+ s << "Comparing leases " << i << " & " << j << " for equality";
+ SCOPED_TRACE(s.str());
+ EXPECT_TRUE(*leases[i] != *leases[j]);
+ }
+ }
+}
+
+/// @brief Creates leases for the test
+///
+/// Creates all leases for the test and checks that they are different.
+///
+/// @return vector<Lease4Ptr> Vector of pointers to leases
+vector<Lease4Ptr>
+GenericLeaseMgrTest::createLeases4() {
+
+ // Create leases for each address
+ vector<Lease4Ptr> leases;
+ for (int i = 0; i < straddress4_.size(); ++i) {
+ leases.push_back(initializeLease4(straddress4_[i]));
+ }
+ EXPECT_EQ(8, leases.size());
+
+ // Check all were created and that they are different.
+ checkLeasesDifferent(leases);
+
+ return (leases);
+}
+
+/// @brief Creates leases for the test
+///
+/// Creates all leases for the test and checks that they are different.
+///
+/// @return vector<Lease6Ptr> Vector of pointers to leases
+vector<Lease6Ptr>
+GenericLeaseMgrTest::createLeases6() {
+
+ // Create leases for each address
+ vector<Lease6Ptr> leases;
+ for (int i = 0; i < straddress6_.size(); ++i) {
+ leases.push_back(initializeLease6(straddress6_[i]));
+ }
+ EXPECT_EQ(8, leases.size());
+
+ // Check all were created and that they are different.
+ checkLeasesDifferent(leases);
+
+ return (leases);
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientId() {
+ // Let's initialize a specific lease ...
+ Lease4Ptr lease = initializeLease4(straddress4_[1]);
+ EXPECT_TRUE(lmptr_->addLease(lease));
+ Lease4Collection returned = lmptr_->getLease4(*lease->client_id_);
+
+ ASSERT_EQ(1, returned.size());
+ // We should retrieve our lease...
+ detailCompareLease(lease, *returned.begin());
+ lease = initializeLease4(straddress4_[2]);
+ returned = lmptr_->getLease4(*lease->client_id_);
+
+ ASSERT_EQ(0, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4NullClientId() {
+ // Let's initialize a specific lease ... But this time
+ // We keep its client id for further lookup and
+ // We clearly 'reset' it ...
+ Lease4Ptr leaseA = initializeLease4(straddress4_[4]);
+ ClientIdPtr client_id = leaseA->client_id_;
+ leaseA->client_id_ = ClientIdPtr();
+ ASSERT_TRUE(lmptr_->addLease(leaseA));
+
+ Lease4Collection returned = lmptr_->getLease4(*client_id);
+ // Shouldn't have our previous lease ...
+ ASSERT_TRUE(returned.empty());
+
+ // Add another lease with the non-NULL client id, and make sure that the
+ // lookup will not break due to existence of both leases with non-NULL and
+ // NULL client ids.
+ Lease4Ptr leaseB = initializeLease4(straddress4_[0]);
+ // Shouldn't throw any null pointer exception
+ ASSERT_TRUE(lmptr_->addLease(leaseB));
+ // Try to get the lease.
+ returned = lmptr_->getLease4(*client_id);
+ ASSERT_TRUE(returned.empty());
+
+ // Let's make it more interesting and add another lease with NULL client id.
+ Lease4Ptr leaseC = initializeLease4(straddress4_[5]);
+ leaseC->client_id_.reset();
+ ASSERT_TRUE(lmptr_->addLease(leaseC));
+ returned = lmptr_->getLease4(*client_id);
+ ASSERT_TRUE(returned.empty());
+
+ // But getting the lease with non-NULL client id should be successful.
+ returned = lmptr_->getLease4(*leaseB->client_id_);
+ ASSERT_EQ(1, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4HWAddr() {
+ // Let's initialize two different leases 4 and just add the first ...
+ Lease4Ptr leaseA = initializeLease4(straddress4_[5]);
+ HWAddr hwaddrA(leaseA->hwaddr_, HTYPE_ETHER);
+ HWAddr hwaddrB(vector<uint8_t>(6, 0x80), HTYPE_ETHER);
+
+ EXPECT_TRUE(lmptr_->addLease(leaseA));
+
+ // we should not have a lease, with this MAC Addr
+ Lease4Collection returned = lmptr_->getLease4(hwaddrB);
+ ASSERT_EQ(0, returned.size());
+
+ // But with this one
+ returned = lmptr_->getLease4(hwaddrA);
+ ASSERT_EQ(1, returned.size());
+}
+
+void
+GenericLeaseMgrTest::testGetLease4ClientIdHWAddrSubnetId() {
+ Lease4Ptr leaseA = initializeLease4(straddress4_[4]);
+ Lease4Ptr leaseB = initializeLease4(straddress4_[5]);
+ Lease4Ptr leaseC = initializeLease4(straddress4_[6]);
+ // Set NULL client id for one of the leases. This is to make sure that such
+ // a lease may coexist with other leases with non NULL client id.
+ leaseC->client_id_.reset();
+
+ HWAddr hwaddrA(leaseA->hwaddr_, HTYPE_ETHER);
+ HWAddr hwaddrB(leaseB->hwaddr_, HTYPE_ETHER);
+ HWAddr hwaddrC(leaseC->hwaddr_, HTYPE_ETHER);
+ EXPECT_TRUE(lmptr_->addLease(leaseA));
+ EXPECT_TRUE(lmptr_->addLease(leaseB));
+ EXPECT_TRUE(lmptr_->addLease(leaseC));
+ // First case we should retrieve our lease
+ Lease4Ptr lease = lmptr_->getLease4(*leaseA->client_id_, hwaddrA, leaseA->subnet_id_);
+ detailCompareLease(lease, leaseA);
+ // Retrieve the other lease.
+ lease = lmptr_->getLease4(*leaseB->client_id_, hwaddrB, leaseB->subnet_id_);
+ detailCompareLease(lease, leaseB);
+ // The last lease has NULL client id so we will use a different getLease4 function
+ // which doesn't require client id (just a hwaddr and subnet id).
+ lease = lmptr_->getLease4(hwaddrC, leaseC->subnet_id_);
+ detailCompareLease(lease, leaseC);
+
+ // An attempt to retrieve the lease with non matching lease parameters should
+ // result in NULL pointer being returned.
+ lease = lmptr_->getLease4(*leaseA->client_id_, hwaddrB, leaseA->subnet_id_);
+ EXPECT_FALSE(lease);
+ lease = lmptr_->getLease4(*leaseA->client_id_, hwaddrA, leaseB->subnet_id_);
+ EXPECT_FALSE(lease);
+}
+
+
};
};
};
diff --git a/src/lib/dhcpsrv/tests/test_utils.h b/src/lib/dhcpsrv/tests/test_utils.h
index 46df9fc..6a86724 100644
--- a/src/lib/dhcpsrv/tests/test_utils.h
+++ b/src/lib/dhcpsrv/tests/test_utils.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -16,6 +16,8 @@
#define LIBDHCPSRV_TEST_UTILS_H
#include <dhcpsrv/lease_mgr.h>
+#include <gtest/gtest.h>
+#include <vector>
namespace isc {
namespace dhcp {
@@ -41,6 +43,90 @@ detailCompareLease(const Lease6Ptr& first, const Lease6Ptr& second);
void
detailCompareLease(const Lease4Ptr& first, const Lease4Ptr& second);
+/// @brief Test Fixture class with utility functions for LeaseMgr backends
+///
+/// It contains utility functions, like dummy lease creation.
+/// All concrete LeaseMgr test classes should be derived from it.
+class GenericLeaseMgrTest : public ::testing::Test {
+public:
+
+ /// @brief Default constructor.
+ GenericLeaseMgrTest();
+
+ /// @brief Virtual destructor.
+ virtual ~GenericLeaseMgrTest();
+
+ /// @brief Initialize Lease4 Fields
+ ///
+ /// Returns a pointer to a Lease4 structure. Different values are put into
+ /// the lease according to the address passed.
+ ///
+ /// This is just a convenience function for the test methods.
+ ///
+ /// @param address Address to use for the initialization
+ ///
+ /// @return Lease4Ptr. This will not point to anything if the
+ /// initialization failed (e.g. unknown address).
+ Lease4Ptr initializeLease4(std::string address);
+
+ /// @brief Initialize Lease6 Fields
+ ///
+ /// Returns a pointer to a Lease6 structure. Different values are put into
+ /// the lease according to the address passed.
+ ///
+ /// This is just a convenience function for the test methods.
+ ///
+ /// @param address Address to use for the initialization
+ ///
+ /// @return Lease6Ptr. This will not point to anything if the initialization
+ /// failed (e.g. unknown address).
+ Lease6Ptr initializeLease6(std::string address);
+
+ /// @brief Check Leases present and different
+ ///
+ /// Checks a vector of lease pointers and ensures that all the leases
+ /// they point to are present and different. If not, a GTest assertion
+ /// will fail.
+ ///
+ /// @param leases Vector of pointers to leases
+ template <typename T>
+ void checkLeasesDifferent(const std::vector<T>& leases) const;
+
+ /// @brief Creates leases for the test
+ ///
+ /// Creates all leases for the test and checks that they are different.
+ ///
+ /// @return vector<Lease4Ptr> Vector of pointers to leases
+ std::vector<Lease4Ptr> createLeases4();
+
+ /// @brief Creates leases for the test
+ ///
+ /// Creates all leases for the test and checks that they are different.
+ ///
+ /// @return vector<Lease6Ptr> Vector of pointers to leases
+ std::vector<Lease6Ptr> createLeases6();
+
+ /// @brief Test lease retrieval using client id.
+ void testGetLease4ClientId();
+
+ /// @brief Test lease retrieval when leases with NULL client id are present.
+ void testGetLease4NullClientId();
+
+ /// @brief Test lease retrieval using HW address.
+ void testGetLease4HWAddr();
+
+ /// @brief Test lease retrieval using client id, HW address and subnet id.
+ void testGetLease4ClientIdHWAddrSubnetId();
+
+ // Member variables
+ std::vector<std::string> straddress4_; ///< String forms of IPv4 addresses
+ std::vector<isc::asiolink::IOAddress> ioaddress4_; ///< IOAddress forms of IPv4 addresses
+ std::vector<std::string> straddress6_; ///< String forms of IPv6 addresses
+ std::vector<Lease::Type> leasetype6_; ///< Lease types
+ std::vector<isc::asiolink::IOAddress> ioaddress6_; ///< IOAddress forms of IPv6 addresses
+
+ LeaseMgr* lmptr_; ///< Pointer to the lease manager
+};
};
};
diff --git a/src/lib/dns/.gitignore b/src/lib/dns/.gitignore
index cdf707c..9606dac 100644
--- a/src/lib/dns/.gitignore
+++ b/src/lib/dns/.gitignore
@@ -4,3 +4,4 @@
/rrclass.h
/rrparamregistry.cc
/rrtype.h
+/s-rdatacode
diff --git a/src/lib/dns/Makefile.am b/src/lib/dns/Makefile.am
index bbf33ed..bda4e85 100644
--- a/src/lib/dns/Makefile.am
+++ b/src/lib/dns/Makefile.am
@@ -7,7 +7,7 @@ AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(B10_CXXFLAGS)
CLEANFILES = *.gcno *.gcda
-CLEANFILES += rrclass.h rrtype.h rrparamregistry.cc rdataclass.h rdataclass.cc
+CLEANFILES += rrclass.h rrtype.h rrparamregistry.cc rdataclass.h rdataclass.cc s-rdatacode
# These two are created with rrtype/class.h, so not explicitly listed in
# BUILT_SOURCES.
CLEANFILES += python/rrtype_constants_inc.cc
@@ -157,8 +157,12 @@ nodist_libb10_dns___la_SOURCES = rdataclass.cc rrparamregistry.cc
rrclass.h: rrclass-placeholder.h
rrtype.h: rrtype-placeholder.h
rrparamregistry.cc: rrparamregistry-placeholder.cc
-rrclass.h rrtype.h rrparamregistry.cc rdataclass.h rdataclass.cc: Makefile
+
+s-rdatacode: Makefile
$(PYTHON) ./gen-rdatacode.py
+ touch $@
+
+rrclass.h rrtype.h rrparamregistry.cc rdataclass.h rdataclass.cc: s-rdatacode
libdns___includedir = $(includedir)/$(PACKAGE_NAME)/dns
libdns___include_HEADERS = \
diff --git a/src/lib/dns/gen-rdatacode.py.in b/src/lib/dns/gen-rdatacode.py.in
index a49f72f..3fd3b33 100755
--- a/src/lib/dns/gen-rdatacode.py.in
+++ b/src/lib/dns/gen-rdatacode.py.in
@@ -24,39 +24,6 @@ from os.path import getmtime
import re
import sys
-# new_rdata_factory_users[] is a list of tuples of the form (rrtype,
-# rrclass). Items in the list use the (new) RdataFactory class, and
-# items which are not in the list use OldRdataFactory class.
-# Note: rrtype and rrclass must be specified in lowercase in
-# new_rdata_factory_users.
-#
-# Example:
-# new_rdata_factory_users = [('a', 'in'), ('a', 'ch'), ('soa', 'generic')]
-new_rdata_factory_users = [('a', 'in'),
- ('aaaa', 'in'),
- ('afsdb', 'generic'),
- ('cname', 'generic'),
- ('dhcid', 'in'),
- ('dlv', 'generic'),
- ('dname', 'generic'),
- ('dnskey', 'generic'),
- ('ds', 'generic'),
- ('hinfo', 'generic'),
- ('naptr', 'generic'),
- ('mx', 'generic'),
- ('ns', 'generic'),
- ('nsec', 'generic'),
- ('nsec3', 'generic'),
- ('nsec3param', 'generic'),
- ('opt', 'generic'),
- ('ptr', 'generic'),
- ('rrsig', 'generic'),
- ('soa', 'generic'),
- ('spf', 'generic'),
- ('srv', 'in'),
- ('txt', 'generic')
- ]
-
re_typecode = re.compile('([\da-z\-]+)_(\d+)')
classcode2txt = {}
typecode2txt = {}
@@ -66,7 +33,7 @@ meta_types = {
# Real meta types. We won't have Rdata implement for them, but we need
# RRType constants.
'251': 'ixfr', '252': 'axfr', '255': 'any',
- # Obsolete types. We probalby won't implement Rdata for them, but it's
+ # Obsolete types. We probably won't implement Rdata for them, but it's
# better to have RRType constants.
'3': 'md', '4': 'mf', '7': 'mb', '8': 'mg', '9': 'mr', '30': 'nxt',
'38': 'a6', '254': 'maila',
@@ -375,28 +342,15 @@ def generate_rrparam(fileprefix, basemtime):
indent = ' ' * 8
typeandclassparams += indent
- # By default, we use OldRdataFactory (see bug #2497). If you
- # want to pick RdataFactory for a particular type, add it to
- # new_rdata_factory_users. Note that we explicitly generate (for
- # optimization) class-independent ("generic") factories for class IN
- # for optimization.
- if (((type_txt.lower(), class_txt.lower()) in
- new_rdata_factory_users) or
- ((class_txt.lower() == 'in') and
- ((type_txt.lower(), 'generic') in new_rdata_factory_users))):
- rdf_class = 'RdataFactory'
- else:
- rdf_class = 'OldRdataFactory'
-
if class_tuple[1] != 'generic':
typeandclassparams += 'add("' + type_utxt + '", '
typeandclassparams += str(type_code) + ', "' + class_utxt
typeandclassparams += '", ' + str(class_code)
- typeandclassparams += ', RdataFactoryPtr(new ' + rdf_class + '<'
+ typeandclassparams += ', RdataFactoryPtr(new ' + 'RdataFactory' + '<'
typeandclassparams += class_txt + '::' + type_utxt + '>()));\n'
else:
typeandclassparams += 'add("' + type_utxt + '", ' + str(type_code)
- typeandclassparams += ', RdataFactoryPtr(new ' + rdf_class + '<'
+ typeandclassparams += ', RdataFactoryPtr(new ' + 'RdataFactory' + '<'
typeandclassparams += class_txt + '::' + type_utxt + '>()));\n'
typeandclassparams += indent + '// Meta and non-implemented RR types\n'
diff --git a/src/lib/dns/master_lexer.h b/src/lib/dns/master_lexer.h
index 31c6443..e33f964 100644
--- a/src/lib/dns/master_lexer.h
+++ b/src/lib/dns/master_lexer.h
@@ -22,6 +22,8 @@
#include <stdint.h>
+#include <boost/noncopyable.hpp>
+
namespace isc {
namespace dns {
namespace master_lexer_internal {
@@ -303,7 +305,7 @@ private:
/// implementation of the exception handling). For these reasons, some of
/// this class does not throw for an error that would be reported as an
/// exception in other classes.
-class MasterLexer {
+class MasterLexer : public boost::noncopyable {
friend class master_lexer_internal::State;
public:
/// \brief Exception thrown when we fail to read from the input
diff --git a/src/lib/dns/master_loader.cc b/src/lib/dns/master_loader.cc
index 5c96563..6b6e091 100644
--- a/src/lib/dns/master_loader.cc
+++ b/src/lib/dns/master_loader.cc
@@ -224,15 +224,10 @@ private:
// after the RR class below.
}
- const MaybeRRClass rrclass =
- RRClass::createFromText(rrparam_token.getString());
+ boost::scoped_ptr<RRClass> rrclass
+ (RRClass::createFromText(rrparam_token.getString()));
if (rrclass) {
- // FIXME: The following code re-parses the rrparam_token to
- // make an RRClass instead of using the MaybeRRClass above,
- // because some old versions of boost::optional (that we
- // still want to support) have a bug (see trac #2593). This
- // workaround should be removed at some point in the future.
- if (RRClass(rrparam_token.getString()) != zone_class_) {
+ if (*rrclass != zone_class_) {
isc_throw(InternalException, "Class mismatch: " << *rrclass <<
" vs. " << zone_class_);
}
@@ -280,11 +275,7 @@ private:
// care about where it comes from). see LimitTTL() for parameter
// post_parsing.
void setDefaultTTL(const RRTTL& ttl, bool post_parsing) {
- if (!default_ttl_) {
- default_ttl_.reset(new RRTTL(ttl));
- } else {
- *default_ttl_ = ttl;
- }
+ assignTTL(default_ttl_, ttl);
limitTTL(*default_ttl_, post_parsing);
}
@@ -296,9 +287,9 @@ private:
// We use the factory version instead of RRTTL constructor as we
// need to expect cases where ttl_txt does not actually represent a TTL
// but an RR class or type.
- const MaybeRRTTL maybe_ttl = RRTTL::createFromText(ttl_txt);
- if (maybe_ttl) {
- current_ttl_ = maybe_ttl;
+ RRTTL* rrttl = RRTTL::createFromText(ttl_txt);
+ if (rrttl) {
+ current_ttl_.reset(rrttl);
limitTTL(*current_ttl_, false);
return (true);
}
@@ -329,7 +320,7 @@ private:
dynamic_cast<const rdata::generic::SOA&>(*rdata).
getMinimum();
setDefaultTTL(RRTTL(ttl_val), true);
- current_ttl_ = *default_ttl_;
+ assignTTL(current_ttl_, *default_ttl_);
} else {
// On catching the exception we'll try to reach EOL again,
// so we need to unget it now.
@@ -338,7 +329,7 @@ private:
"no TTL specified; load rejected");
}
} else if (!explicit_ttl && default_ttl_) {
- current_ttl_ = *default_ttl_;
+ assignTTL(current_ttl_, *default_ttl_);
} else if (!explicit_ttl && warn_rfc1035_ttl_) {
// Omitted (class and) TTL values are default to the last
// explicitly stated values (RFC 1035, Sec. 5.1).
@@ -395,6 +386,17 @@ private:
}
}
+ /// \brief Assign the right RRTTL's value to the left RRTTL. If one
+ /// doesn't exist in the scoped_ptr, make a new RRTTL copy of the
+ /// right argument.
+ static void assignTTL(boost::scoped_ptr<RRTTL>& left, const RRTTL& right) {
+ if (!left) {
+ left.reset(new RRTTL(right));
+ } else {
+ *left = right;
+ }
+ }
+
private:
MasterLexer lexer_;
const Name zone_origin_;
@@ -407,8 +409,10 @@ private:
boost::scoped_ptr<RRTTL> default_ttl_; // Default TTL of RRs used when
// unspecified. If NULL no default
// is known.
- MaybeRRTTL current_ttl_; // The TTL used most recently. Initially unset.
- // Once set always stores a valid RRTTL.
+ boost::scoped_ptr<RRTTL> current_ttl_; // The TTL used most recently.
+ // Initially unset. Once set
+ // always stores a valid
+ // RRTTL.
const MasterLoader::Options options_;
const std::string master_file_;
std::string string_token_;
diff --git a/src/lib/dns/masterload.cc b/src/lib/dns/masterload.cc
index 53a2b8d..f583c4f 100644
--- a/src/lib/dns/masterload.cc
+++ b/src/lib/dns/masterload.cc
@@ -19,11 +19,13 @@
#include <cctype>
#include <cerrno>
+#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <exceptions/exceptions.h>
#include <dns/masterload.h>
+#include <dns/master_loader.h>
#include <dns/name.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
@@ -31,6 +33,7 @@
#include <dns/rrset.h>
#include <dns/rrttl.h>
#include <dns/rrtype.h>
+#include <dns/rrcollator.h>
using namespace std;
using namespace boost;
@@ -39,25 +42,45 @@ using namespace isc::dns::rdata;
namespace isc {
namespace dns {
namespace {
-// A helper function that strips off any comment or whitespace at the end of
-// an RR.
-// This is an incomplete implementation, and cannot handle all such comments;
-// it's considered a short term workaround to deal with some real world
-// cases.
-string
-stripLine(string& s, const Exception& ex) {
- // Find any ';' in the text data, and locate the position of the last
- // occurrence. Note that unless/until we support empty RDATA it
- // shouldn't be placed at the beginning of the data.
- const size_t pos_semicolon = s.rfind(';');
- if (pos_semicolon == 0) {
- throw ex;
- } else if (pos_semicolon != string::npos) {
- s.resize(pos_semicolon);
+void
+callbackWrapper(const RRsetPtr& rrset, MasterLoadCallback callback,
+ const Name* origin)
+{
+ // Origin related validation:
+ // - reject out-of-zone data
+ // - reject SOA whose owner is not at the top of zone
+ const NameComparisonResult cmp_result =
+ rrset->getName().compare(*origin);
+ if (cmp_result.getRelation() != NameComparisonResult::EQUAL &&
+ cmp_result.getRelation() != NameComparisonResult::SUBDOMAIN) {
+ isc_throw(MasterLoadError, "Out-of-zone data for " << *origin
+ << "/" << rrset->getClass() << ": " << rrset->getName());
+ }
+ if (rrset->getType() == RRType::SOA() &&
+ cmp_result.getRelation() != NameComparisonResult::EQUAL) {
+ isc_throw(MasterLoadError, "SOA not at top of zone: "
+ << *rrset);
+ }
+
+ callback(rrset);
+}
+
+template <typename InputType>
+void
+loadHelper(InputType input, const Name& origin,
+ const RRClass& zone_class, MasterLoadCallback callback)
+{
+ RRCollator rr_collator(boost::bind(callbackWrapper, _1,
+ callback, &origin));
+ MasterLoader loader(input, origin, zone_class,
+ MasterLoaderCallbacks::getNullCallbacks(),
+ rr_collator.getCallback());
+ try {
+ loader.load();
+ } catch (const MasterLoaderError& ex) {
+ isc_throw(MasterLoadError, ex.what());
}
- // Remove any trailing whitespace return the resulting text.
- s.resize(s.find_last_not_of(" \t") + 1);
- return (s);
+ rr_collator.flush();
}
}
@@ -69,154 +92,14 @@ masterLoad(const char* const filename, const Name& origin,
isc_throw(MasterLoadError, "Name of master file must not be null");
}
- ifstream ifs;
- ifs.open(filename, ios_base::in);
- if (ifs.fail()) {
- isc_throw(MasterLoadError, "Failed to open master file: " <<
- filename << ": " << strerror(errno));
- }
- masterLoad(ifs, origin, zone_class, callback, filename);
- ifs.close();
+ loadHelper<const char*>(filename, origin, zone_class, callback);
}
void
masterLoad(istream& input, const Name& origin, const RRClass& zone_class,
- MasterLoadCallback callback, const char* source)
+ MasterLoadCallback callback, const char*)
{
- RRsetPtr rrset;
- ConstRdataPtr prev_rdata; // placeholder for special case of RRSIGs
- string line;
- unsigned int line_count = 1;
-
- if (source == NULL) {
- source = "<unknown>";
- }
-
- do {
- getline(input, line);
- if (input.bad() || (input.fail() && !input.eof())) {
- isc_throw(MasterLoadError, "Unexpectedly failed to read a line in "
- << source);
- }
-
- // blank/comment lines should be simply skipped.
- if (line.empty() || line[0] == ';') {
- continue;
- }
-
- // The line shouldn't have leading space (which means omitting the
- // owner name).
- if (isspace(line[0])) {
- isc_throw(MasterLoadError, "Leading space in " << source
- << " at line " << line_count);
- }
-
- // Parse a single RR
- istringstream iss(line);
- string owner_txt, ttl_txt, rrclass_txt, rrtype_txt;
- stringbuf rdatabuf;
- iss >> owner_txt >> ttl_txt >> rrclass_txt >> rrtype_txt >> &rdatabuf;
- if (iss.bad() || iss.fail()) {
- isc_throw(MasterLoadError, "Parse failure for a valid RR in "
- << source << " at line " << line_count);
- }
-
- // This simple version doesn't support relative owner names with a
- // separate origin.
- if (owner_txt.empty() || *(owner_txt.end() - 1) != '.') {
- isc_throw(MasterLoadError, "Owner name is not absolute in "
- << source << " at line " << line_count);
- }
-
- // XXX: this part is a bit tricky (and less efficient). We are going
- // to validate the text for the RR parameters, and throw an exception
- // if any of them is invalid by converting an underlying exception
- // to MasterLoadError. To do that, we need to define the corresponding
- // variables used for RRset construction outside the try-catch block,
- // but we don't like to use a temporary variable with a meaningless
- // initial value. So we define pointers outside the try block
- // and allocate/initialize the actual objects within the block.
- // To make it exception safe we use Boost.scoped_ptr.
- scoped_ptr<const Name> owner;
- scoped_ptr<const RRTTL> ttl;
- scoped_ptr<const RRClass> rrclass;
- scoped_ptr<const RRType> rrtype;
- ConstRdataPtr rdata;
- try {
- owner.reset(new Name(owner_txt));
- ttl.reset(new RRTTL(ttl_txt));
- rrclass.reset(new RRClass(rrclass_txt));
- rrtype.reset(new RRType(rrtype_txt));
- string rdtext = rdatabuf.str();
- try {
- rdata = createRdata(*rrtype, *rrclass, rdtext);
- } catch (const Exception& ex) {
- // If the parse for the RDATA fails, check if it has comments
- // or whitespace at the end, and if so, retry the conversion
- // after stripping off the comment or whitespace
- rdata = createRdata(*rrtype, *rrclass, stripLine(rdtext, ex));
- }
- } catch (const Exception& ex) {
- isc_throw(MasterLoadError, "Invalid RR text in " << source <<
- " at line " << line_count << ": "
- << ex.what());
- }
-
- // Origin related validation:
- // - reject out-of-zone data
- // - reject SOA whose owner is not at the top of zone
- const NameComparisonResult cmp_result = owner->compare(origin);
- if (cmp_result.getRelation() != NameComparisonResult::EQUAL &&
- cmp_result.getRelation() != NameComparisonResult::SUBDOMAIN) {
- isc_throw(MasterLoadError, "Out-of-zone data for " << origin
- << "/" << rrclass_txt << " in "
- << source << " at line "
- << line_count);
- }
- if (*rrtype == RRType::SOA() &&
- cmp_result.getRelation() != NameComparisonResult::EQUAL) {
- isc_throw(MasterLoadError, "SOA not at top of zone in "
- << source << " at line " << line_count);
- }
-
- // Reject RR class mismatching
- if (*rrclass != zone_class) {
- isc_throw(MasterLoadError, "RR class (" << rrclass_txt
- << ") does not match the zone class (" << zone_class
- << ") in " << source << " at line " << line_count);
- }
-
- // Everything is okay. Now create/update RRset with the new RR.
- // If this is the first RR or the RR type/name is new, we are seeing
- // a new RRset.
- bool new_rrset = false;
- if (!rrset || rrset->getType() != *rrtype ||
- rrset->getName() != *owner) {
- new_rrset = true;
- } else if (rrset->getType() == RRType::RRSIG()) {
- // We are seeing two consecutive RRSIGs of the same name.
- // They can be combined iff they have the same type covered.
- if (dynamic_cast<const generic::RRSIG&>(*rdata).typeCovered() !=
- dynamic_cast<const generic::RRSIG&>(*prev_rdata).typeCovered())
- {
- new_rrset = true;
- }
- }
- if (new_rrset) {
- // Commit the previous RRset, if any.
- if (rrset) {
- callback(rrset);
- }
- rrset = RRsetPtr(new RRset(*owner, *rrclass, *rrtype, *ttl));
- }
- rrset->addRdata(rdata);
- prev_rdata = rdata;
- } while (++line_count, !input.eof());
-
- // Commit the last RRset, if any.
- if (rrset) {
- callback(rrset);
- }
+ loadHelper<istream&>(input, origin, zone_class, callback);
}
} // namespace dns
diff --git a/src/lib/dns/masterload.h b/src/lib/dns/masterload.h
index 5303785..8ff45e0 100644
--- a/src/lib/dns/masterload.h
+++ b/src/lib/dns/masterload.h
@@ -64,86 +64,22 @@ typedef boost::function<void(RRsetPtr)> MasterLoadCallback;
/// and this function never uses it once it is called.
/// The callback can freely modify the passed \c RRset.
///
-/// This function performs minimum level of validation on the input:
-/// - Each RR is a valid textual representation per the DNS protocol.
-/// - The class of each RR must be identical to the specified RR class.
-/// - The owner name of each RR must be a subdomain of the origin name
-/// (that can be equal to the origin).
-/// - If an SOA RR is included, its owner name must be the origin name.
-/// If any of these validation checks fails, this function throws an
-/// exception of class \c MasterLoadError.
+/// This function internally uses the MasterLoader class, and basically
+/// accepts and rejects input that MasterLoader accepts and rejects,
+/// accordingly. In addition, this function performs the following validation:
+/// if an SOA RR is included, its owner name must be the origin name.
///
/// It does not perform other semantical checks, however. For example,
/// it doesn't check if an NS RR of the origin name is included or if
/// there is more than one SOA RR. Such further checks are the caller's
/// (or the callback's) responsibility.
///
-/// <b>Acceptable Format</b>
-///
-/// The current implementation only supports a restricted form of master files
-/// for simplicity. One easy way to ensure that a handwritten zone file is
-/// acceptable to this implementation is to preprocess it with BIND 9's
-/// named-compilezone tool with both the input and output formats being
-/// "text".
-/// Here is an example:
-/// \code % named-compilezone -f text -F text -o example.com.norm
-/// example.com example.com.zone
-/// \endcode
-/// where example.com.zone is the original zone file for the "example.com"
-/// zone. The output file is example.com.norm, which should be acceptable
-/// by this implementation.
-///
-/// Below are specific restrictions that this implementation assumes.
-/// Basically, each RR must consist of exactly one line
-/// (so there shouldn't be a multi-line RR) in the following format:
-/// \code <owner name> <TTL> <RRCLASS> <RRTYPE> <RDATA (single line)>
-/// \endcode
-/// Here are some more details about the restrictions:
-/// - No special directives such as $TTL are supported.
-/// - The owner name must be absolute, that is, it must end with a period.
-/// - "@" is not recognized as a valid owner name.
-/// - Owner names, TTL and RRCLASS cannot be omitted.
-/// - As a corollary, a non blank line must not begin with a space character.
-/// - The order of the RR parameters is fixed, for example, this is acceptable:
-/// \code example.com. 3600 IN A 192.0.2.1
-/// \endcode
-/// but this is not even though it's valid per RFC1035:
-/// \code example.com. IN 3600 A 192.0.2.1
-/// \endcode
-/// - "TTL", "RRCLASS", and "RRTYPE" must be recognizable by the \c RRTTL,
-/// RRClass and RRType class implementations of this library. In particular,
-/// as of this writing TTL must be a decimal number (a convenient extension
-/// such as "1H" instead of 3600 cannot be used). Not all standard RR
-/// classes and RR types are supported yet, so the mnemonics of them will
-/// be rejected, too.
-/// - RR TTLs of the same RRset must be the same; even if they are different,
-/// this implementation simply uses the TTL of the first RR.
-///
-/// Blank lines and lines beginning with a semi-colon are allowed, and will
-/// be simply ignored. Comments cannot coexist with an RR line, however.
-/// For example, this will be rejected:
-/// \code example.com. 3600 IN A 192.0.2.1 ; this is a comment
-/// \endcode
-///
-/// This implementation assumes that RRs of a single RRset are not
-/// interleaved with RRs of a different RRset.
-/// That is, the following sequence shouldn't happen:
-/// \code example.com. 3600 IN A 192.0.2.1
-/// example.com. 3600 IN AAAA 2001:db8::1
-/// example.com. 3600 IN A 192.0.2.2
-/// \endcode
-/// But it does not consider this an error; it will simply regard each RR
-/// as a separate RRset and call the callback with them separately.
-/// It is up to the callback to merge multiple RRsets into one if possible
-/// and necessary.
-///
/// <b>Exceptions</b>
///
/// This function throws an exception of class \c MasterLoadError in the
/// following cases:
-/// - Any of the validation checks fails (see the class description).
-/// - The input data is not in the acceptable format (see the details of
-/// the format above).
+/// - Any of the validation checks fails (see above).
+/// - The input data has a syntax error.
/// - The specified file cannot be opened for loading.
/// - An I/O error occurs during the loading.
///
@@ -196,16 +132,12 @@ typedef boost::function<void(RRsetPtr)> MasterLoadCallback;
/// The current implementation is in a preliminary level and needs further
/// extensions. Some design decisions may also have to be reconsidered as
/// we gain experiences. Those include:
-/// - We should be more flexible about the input format.
/// - We may want to allow optional conditions. For example, we may want to
/// be generous about some validation failures and be able to continue
/// parsing.
/// - Especially if we allow to be generous, we may also want to support
/// returning an error code instead of throwing an exception when we
/// encounter validation failure.
-/// - We may want to support incremental loading.
-/// - If we add these optional features we may want to introduce a class
-/// that encapsulates loading status and options.
/// - RRSIGs are handled as separate RRsets, i.e. they are not included in
/// the RRset they cover.
///
@@ -227,14 +159,16 @@ void masterLoad(const char* const filename, const Name& origin,
/// All descriptions of the other version apply to this version except those
/// specific to file I/O.
///
+/// Note: The 'source' parameter is now ignored, but it was only used in
+/// exception messages on some error. So the compatibility effect should be
+/// minimal.
+///
/// \param input An input stream object that is to emit zone's RRs.
/// \param origin The origin name of the zone.
/// \param zone_class The RR class of the zone.
/// \param callback A callback functor or function that is to be called for
/// each RRset.
-/// \param source A string to use in error messages if zone content is bad
-/// (e.g. the file name when reading from a file). If this value is NULL,
-/// or left out, the error will use the string '\<unknown\>'.
+/// \param source This parameter is now ignored but left for compatibility.
void masterLoad(std::istream& input, const Name& origin,
const RRClass& zone_class, MasterLoadCallback callback,
const char* source = NULL);
diff --git a/src/lib/dns/message.cc b/src/lib/dns/message.cc
index dba0e7b..2bc337a 100644
--- a/src/lib/dns/message.cc
+++ b/src/lib/dns/message.cc
@@ -495,7 +495,7 @@ Message::getTSIGRecord() const {
unsigned int
Message::getRRCount(const Section section) const {
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
return (impl_->counts_[section]);
@@ -511,7 +511,7 @@ Message::addRRset(const Section section, RRsetPtr rrset) {
isc_throw(InvalidMessageOperation,
"addRRset performed in non-render mode");
}
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
@@ -522,9 +522,9 @@ Message::addRRset(const Section section, RRsetPtr rrset) {
bool
Message::hasRRset(const Section section, const Name& name,
- const RRClass& rrclass, const RRType& rrtype)
+ const RRClass& rrclass, const RRType& rrtype) const
{
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
@@ -540,13 +540,14 @@ Message::hasRRset(const Section section, const Name& name,
}
bool
-Message::hasRRset(const Section section, const RRsetPtr& rrset) {
- return (hasRRset(section, rrset->getName(), rrset->getClass(), rrset->getType()));
+Message::hasRRset(const Section section, const RRsetPtr& rrset) const {
+ return (hasRRset(section, rrset->getName(),
+ rrset->getClass(), rrset->getType()));
}
bool
Message::removeRRset(const Section section, RRsetIterator& iterator) {
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
@@ -575,7 +576,7 @@ Message::clearSection(const Section section) {
isc_throw(InvalidMessageOperation,
"clearSection performed in non-render mode");
}
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
if (section == Message::SECTION_QUESTION) {
@@ -732,7 +733,7 @@ int
MessageImpl::parseSection(const Message::Section section,
InputBuffer& buffer, Message::ParseOptions options)
{
- assert(section < MessageImpl::NUM_SECTIONS);
+ assert(static_cast<int>(section) < MessageImpl::NUM_SECTIONS);
unsigned int added = 0;
@@ -869,8 +870,11 @@ struct SectionFormatter {
void operator()(const T& entry) {
if (section_ == Message::SECTION_QUESTION) {
output_ += ";";
+ output_ += entry->toText();
+ output_ += "\n";
+ } else {
+ output_ += entry->toText();
}
- output_ += entry->toText();
}
const Message::Section section_;
string& output_;
@@ -984,7 +988,7 @@ Message::clear(Mode mode) {
void
Message::appendSection(const Section section, const Message& source) {
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
@@ -1130,7 +1134,7 @@ Message::endQuestion() const {
///
const SectionIterator<RRsetPtr>
Message::beginSection(const Section section) const {
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
if (section == SECTION_QUESTION) {
@@ -1143,7 +1147,7 @@ Message::beginSection(const Section section) const {
const SectionIterator<RRsetPtr>
Message::endSection(const Section section) const {
- if (section >= MessageImpl::NUM_SECTIONS) {
+ if (static_cast<int>(section) >= MessageImpl::NUM_SECTIONS) {
isc_throw(OutOfRange, "Invalid message section: " << section);
}
if (section == SECTION_QUESTION) {
diff --git a/src/lib/dns/message.h b/src/lib/dns/message.h
index 8aaaa48..aaa0d76 100644
--- a/src/lib/dns/message.h
+++ b/src/lib/dns/message.h
@@ -98,10 +98,10 @@ struct SectionIteratorImpl;
template <typename T>
class SectionIterator : public std::iterator<std::input_iterator_tag, T> {
public:
- SectionIterator<T>() : impl_(NULL) {}
- SectionIterator<T>(const SectionIteratorImpl<T>& impl);
- ~SectionIterator<T>();
- SectionIterator<T>(const SectionIterator<T>& source);
+ SectionIterator() : impl_(NULL) {}
+ SectionIterator(const SectionIteratorImpl<T>& impl);
+ ~SectionIterator();
+ SectionIterator(const SectionIterator<T>& source);
void operator=(const SectionIterator<T>& source);
SectionIterator<T>& operator++();
SectionIterator<T> operator++(int);
@@ -481,14 +481,14 @@ public:
/// This should probably be extended to be a "find" method that returns
/// a matching RRset if found.
bool hasRRset(const Section section, const Name& name,
- const RRClass& rrclass, const RRType& rrtype);
+ const RRClass& rrclass, const RRType& rrtype) const;
/// \brief Determine whether the given section already has an RRset
/// matching the one pointed to by the argumet
///
/// \c section must be a valid constant of the \c Section type;
/// otherwise, an exception of class \c OutOfRange will be thrown.
- bool hasRRset(const Section section, const RRsetPtr& rrset);
+ bool hasRRset(const Section section, const RRsetPtr& rrset) const;
/// \brief Remove RRSet from Message
///
diff --git a/src/lib/dns/messagerenderer.h b/src/lib/dns/messagerenderer.h
index 092d6de..7defe95 100644
--- a/src/lib/dns/messagerenderer.h
+++ b/src/lib/dns/messagerenderer.h
@@ -367,7 +367,6 @@ public:
using AbstractMessageRenderer::CASE_INSENSITIVE;
using AbstractMessageRenderer::CASE_SENSITIVE;
- /// \brief Constructor from an output buffer.
MessageRenderer();
virtual ~MessageRenderer();
diff --git a/src/lib/dns/python/.gitignore b/src/lib/dns/python/.gitignore
index 025ddd1..0713cf8 100644
--- a/src/lib/dns/python/.gitignore
+++ b/src/lib/dns/python/.gitignore
@@ -1,2 +1,3 @@
/rrclass_constants_inc.cc
/rrtype_constants_inc.cc
+/pydnspp_config.h
diff --git a/src/lib/dns/python/Makefile.am b/src/lib/dns/python/Makefile.am
index a221bfe..9654239 100644
--- a/src/lib/dns/python/Makefile.am
+++ b/src/lib/dns/python/Makefile.am
@@ -5,7 +5,8 @@ AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(B10_CXXFLAGS)
lib_LTLIBRARIES = libb10-pydnspp.la
-libb10_pydnspp_la_SOURCES = pydnspp_common.cc pydnspp_common.h pydnspp_towire.h
+libb10_pydnspp_la_SOURCES = pydnspp_common.cc pydnspp_common.h
+libb10_pydnspp_la_SOURCES += pydnspp_config.h pydnspp_towire.h
libb10_pydnspp_la_SOURCES += name_python.cc name_python.h
libb10_pydnspp_la_SOURCES += nsec3hash_python.cc nsec3hash_python.h
libb10_pydnspp_la_SOURCES += rrset_python.cc rrset_python.h
diff --git a/src/lib/dns/python/pydnspp_common.cc b/src/lib/dns/python/pydnspp_common.cc
index e9d62e0..4250db7 100644
--- a/src/lib/dns/python/pydnspp_common.cc
+++ b/src/lib/dns/python/pydnspp_common.cc
@@ -56,9 +56,8 @@ PyObject* po_DNSMessageBADVERS;
int
readDataFromSequence(uint8_t *data, size_t len, PyObject* sequence) {
- PyObject* el = NULL;
for (size_t i = 0; i < len; i++) {
- el = PySequence_GetItem(sequence, i);
+ PyObject *el = PySequence_GetItem(sequence, i);
if (!el) {
PyErr_SetString(PyExc_TypeError,
"sequence too short");
diff --git a/src/lib/dns/python/pydnspp_common.h b/src/lib/dns/python/pydnspp_common.h
index 8e498b3..9c27cfd 100644
--- a/src/lib/dns/python/pydnspp_common.h
+++ b/src/lib/dns/python/pydnspp_common.h
@@ -15,6 +15,8 @@
#ifndef LIBDNS_PYTHON_COMMON_H
#define LIBDNS_PYTHON_COMMON_H 1
+#include <dns/python/pydnspp_config.h>
+
#include <Python.h>
#include <boost/static_assert.hpp>
@@ -60,11 +62,6 @@ int addClassVariable(PyTypeObject& c, const char* name, PyObject* obj);
/// \return true on success, false on failure
bool initClass(PyTypeObject& type, const char* name, PyObject* mod);
-// Short term workaround for unifying the return type of tp_hash
-#if PY_MINOR_VERSION < 2
-typedef long Py_hash_t;
-#endif
-
/// \brief Convert a hash value of arbitrary type to a Python hash value.
///
/// This templated function is a convenient wrapper to produce a valid hash
diff --git a/src/lib/dns/python/pydnspp_config.h.in b/src/lib/dns/python/pydnspp_config.h.in
new file mode 100644
index 0000000..6326e8c
--- /dev/null
+++ b/src/lib/dns/python/pydnspp_config.h.in
@@ -0,0 +1,25 @@
+// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LIBDNS_PYTHON_CONFIG_H
+#define LIBDNS_PYTHON_CONFIG_H 1
+
+// Short term workaround for unifying the return type of tp_hash.
+// Remove this test (and associated changes in configure.ac) when we
+// require Python 3.2.
+#if (!(@HAVE_PY_HASH_T@))
+typedef long Py_hash_t;
+#endif
+
+#endif // LIBDNS_PYTHON_CONFIG_H
diff --git a/src/lib/dns/python/rrset_python.cc b/src/lib/dns/python/rrset_python.cc
index dc2af22..de5925a 100644
--- a/src/lib/dns/python/rrset_python.cc
+++ b/src/lib/dns/python/rrset_python.cc
@@ -57,7 +57,6 @@ PyObject* RRset_getName(PyObject* self, PyObject* args);
PyObject* RRset_getClass(PyObject* self, PyObject* args);
PyObject* RRset_getType(PyObject* self, PyObject* args);
PyObject* RRset_getTTL(PyObject* self, PyObject* args);
-PyObject* RRset_setName(PyObject* self, PyObject* args);
PyObject* RRset_setTTL(PyObject* self, PyObject* args);
PyObject* RRset_toText(PyObject* self, PyObject* args);
PyObject* RRset_str(PyObject* self);
@@ -79,8 +78,6 @@ PyMethodDef RRset_methods[] = {
"Returns the type of the RRset as an RRType object." },
{ "get_ttl", RRset_getTTL, METH_NOARGS,
"Returns the TTL of the RRset as an RRTTL object." },
- { "set_name", RRset_setName, METH_VARARGS,
- "Sets the name of the RRset.\nTakes a Name object as an argument." },
{ "set_ttl", RRset_setTTL, METH_VARARGS,
"Sets the TTL of the RRset.\nTakes an RRTTL object as an argument." },
{ "to_text", RRset_toText, METH_NOARGS,
@@ -207,16 +204,6 @@ RRset_getTTL(PyObject* self, PyObject*) {
}
PyObject*
-RRset_setName(PyObject* self, PyObject* args) {
- PyObject* name;
- if (!PyArg_ParseTuple(args, "O!", &name_type, &name)) {
- return (NULL);
- }
- static_cast<s_RRset*>(self)->cppobj->setName(PyName_ToName(name));
- Py_RETURN_NONE;
-}
-
-PyObject*
RRset_setTTL(PyObject* self, PyObject* args) {
PyObject* rrttl;
if (!PyArg_ParseTuple(args, "O!", &rrttl_type, &rrttl)) {
diff --git a/src/lib/dns/python/tests/question_python_test.py b/src/lib/dns/python/tests/question_python_test.py
index 8c8c815..43b80d3 100644
--- a/src/lib/dns/python/tests/question_python_test.py
+++ b/src/lib/dns/python/tests/question_python_test.py
@@ -70,9 +70,9 @@ class QuestionTest(unittest.TestCase):
def test_to_text(self):
- self.assertEqual("foo.example.com. IN NS\n", self.test_question1.to_text())
- self.assertEqual("foo.example.com. IN NS\n", str(self.test_question1))
- self.assertEqual("bar.example.com. CH A\n", self.test_question2.to_text())
+ self.assertEqual("foo.example.com. IN NS", self.test_question1.to_text())
+ self.assertEqual("foo.example.com. IN NS", str(self.test_question1))
+ self.assertEqual("bar.example.com. CH A", self.test_question2.to_text())
def test_to_wire_buffer(self):
obuffer = bytes()
diff --git a/src/lib/dns/python/tests/rrset_python_test.py b/src/lib/dns/python/tests/rrset_python_test.py
index 0ffcdbe..9592b42 100644
--- a/src/lib/dns/python/tests/rrset_python_test.py
+++ b/src/lib/dns/python/tests/rrset_python_test.py
@@ -70,11 +70,6 @@ class TestModuleSpec(unittest.TestCase):
self.assertEqual(RRTTL(0), self.rrset_a.get_ttl());
self.assertRaises(TypeError, self.rrset_a.set_ttl, 1)
- def test_set_name(self):
- self.rrset_a.set_name(self.test_nsname);
- self.assertEqual(self.test_nsname, self.rrset_a.get_name());
- self.assertRaises(TypeError, self.rrset_a.set_name, 1)
-
def test_add_rdata(self):
# no iterator to read out yet (TODO: add addition test once implemented)
diff --git a/src/lib/dns/question.cc b/src/lib/dns/question.cc
index 6ccb164..bf39174 100644
--- a/src/lib/dns/question.cc
+++ b/src/lib/dns/question.cc
@@ -40,10 +40,15 @@ Question::Question(InputBuffer& buffer) :
rrclass_ = RRClass(buffer);
}
-string
-Question::toText() const {
- return (name_.toText() + " " + rrclass_.toText() + " " +
- rrtype_.toText() + "\n");
+std::string
+Question::toText(bool newline) const {
+ std::string r(name_.toText() + " " + rrclass_.toText() + " " +
+ rrtype_.toText());
+ if (newline) {
+ r.append("\n");
+ }
+
+ return (r);
}
unsigned int
diff --git a/src/lib/dns/question.h b/src/lib/dns/question.h
index 4b5b233..388cf49 100644
--- a/src/lib/dns/question.h
+++ b/src/lib/dns/question.h
@@ -173,9 +173,9 @@ public:
//@{
/// \brief Convert the Question to a string.
///
- /// Unlike other similar methods of this library, this method terminates
- /// the resulting string with a trailing newline character
- /// (following the BIND9 convention).
+ /// When \c newline argument is \c true, this method terminates the
+ /// resulting string with a trailing newline character (following
+ /// the BIND9 convention).
///
/// This method simply calls the \c %toText() methods of the corresponding
/// \c Name, \c RRType and \c RRClass classes for this \c Question, and
@@ -183,8 +183,12 @@ public:
/// In particular, if resource allocation fails, a corresponding standard
/// exception will be thrown.
///
+ /// \param newline Whether to add a trailing newline. If true, a
+ /// trailing newline is added. If false, no trailing newline is
+ /// added.
+ ///
/// \return A string representation of the \c Question.
- std::string toText() const;
+ std::string toText(bool newline = false) const;
/// \brief Render the Question in the wire format with name compression.
///
diff --git a/src/lib/dns/rdata.cc b/src/lib/dns/rdata.cc
index 081f855..f42c349 100644
--- a/src/lib/dns/rdata.cc
+++ b/src/lib/dns/rdata.cc
@@ -310,6 +310,9 @@ Generic::Generic(const Generic& source) :
{}
Generic&
+// Our check is better than the usual if (this == &source),
+// but cppcheck doesn't recognize it.
+// cppcheck-suppress operatorEqToSelf
Generic::operator=(const Generic& source) {
if (impl_ == source.impl_) {
return (*this);
diff --git a/src/lib/dns/rdata/any_255/tsig_250.cc b/src/lib/dns/rdata/any_255/tsig_250.cc
index ff848fa..3252cfd 100644
--- a/src/lib/dns/rdata/any_255/tsig_250.cc
+++ b/src/lib/dns/rdata/any_255/tsig_250.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -19,26 +19,29 @@
#include <boost/lexical_cast.hpp>
#include <util/buffer.h>
-#include <util/strutil.h>
#include <util/encode/base64.h>
#include <dns/messagerenderer.h>
#include <dns/name.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
+#include <dns/rcode.h>
+#include <dns/tsigkey.h>
#include <dns/tsigerror.h>
+#include <dns/rdata/generic/detail/lexer_util.h>
using namespace std;
using boost::lexical_cast;
using namespace isc::util;
using namespace isc::util::encode;
-using namespace isc::util::str;
+using namespace isc::dns;
+using isc::dns::rdata::generic::detail::createNameFromLexer;
// BEGIN_ISC_NAMESPACE
// BEGIN_RDATA_NAMESPACE
-/// This is a straightforward representation of TSIG RDATA fields.
-struct TSIG::TSIGImpl {
+// straightforward representation of TSIG RDATA fields
+struct TSIGImpl {
TSIGImpl(const Name& algorithm, uint64_t time_signed, uint16_t fudge,
vector<uint8_t>& mac, uint16_t original_id, uint16_t error,
vector<uint8_t>& other_data) :
@@ -68,99 +71,187 @@ struct TSIG::TSIGImpl {
const vector<uint8_t> other_data_;
};
+// helper function for string and lexer constructors
+TSIGImpl*
+TSIG::constructFromLexer(MasterLexer& lexer, const Name* origin) {
+ const Name& algorithm =
+ createNameFromLexer(lexer, origin ? origin : &Name::ROOT_NAME());
+ const Name& canonical_algorithm_name =
+ (algorithm == TSIGKey::HMACMD5_SHORT_NAME()) ?
+ TSIGKey::HMACMD5_NAME() : algorithm;
+
+ const string& time_txt =
+ lexer.getNextToken(MasterToken::STRING).getString();
+ uint64_t time_signed;
+ try {
+ time_signed = boost::lexical_cast<uint64_t>(time_txt);
+ } catch (const boost::bad_lexical_cast&) {
+ isc_throw(InvalidRdataText, "Invalid TSIG Time");
+ }
+ if ((time_signed >> 48) != 0) {
+ isc_throw(InvalidRdataText, "TSIG Time out of range");
+ }
+
+ const uint32_t fudge = lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (fudge > 0xffff) {
+ isc_throw(InvalidRdataText, "TSIG Fudge out of range");
+ }
+ const uint32_t macsize =
+ lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (macsize > 0xffff) {
+ isc_throw(InvalidRdataText, "TSIG MAC Size out of range");
+ }
+
+ const string& mac_txt = (macsize > 0) ?
+ lexer.getNextToken(MasterToken::STRING).getString() : "";
+ vector<uint8_t> mac;
+ decodeBase64(mac_txt, mac);
+ if (mac.size() != macsize) {
+ isc_throw(InvalidRdataText, "TSIG MAC Size and data are inconsistent");
+ }
+
+ const uint32_t orig_id =
+ lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (orig_id > 0xffff) {
+ isc_throw(InvalidRdataText, "TSIG Original ID out of range");
+ }
+
+ const string& error_txt =
+ lexer.getNextToken(MasterToken::STRING).getString();
+ uint32_t error = 0;
+ // XXX: In the initial implementation we hardcode the mnemonics.
+ // We'll soon generalize this.
+ if (error_txt == "NOERROR") {
+ error = Rcode::NOERROR_CODE;
+ } else if (error_txt == "BADSIG") {
+ error = TSIGError::BAD_SIG_CODE;
+ } else if (error_txt == "BADKEY") {
+ error = TSIGError::BAD_KEY_CODE;
+ } else if (error_txt == "BADTIME") {
+ error = TSIGError::BAD_TIME_CODE;
+ } else {
+ /// we cast to uint32_t and range-check, because casting directly to
+ /// uint16_t will convert negative numbers to large positive numbers
+ try {
+ error = boost::lexical_cast<uint32_t>(error_txt);
+ } catch (const boost::bad_lexical_cast&) {
+ isc_throw(InvalidRdataText, "Invalid TSIG Error");
+ }
+ if (error > 0xffff) {
+ isc_throw(InvalidRdataText, "TSIG Error out of range");
+ }
+ }
+
+ const uint32_t otherlen =
+ lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (otherlen > 0xffff) {
+ isc_throw(InvalidRdataText, "TSIG Other Len out of range");
+ }
+ const string otherdata_txt = (otherlen > 0) ?
+ lexer.getNextToken(MasterToken::STRING).getString() : "";
+ vector<uint8_t> other_data;
+ decodeBase64(otherdata_txt, other_data);
+ if (other_data.size() != otherlen) {
+ isc_throw(InvalidRdataText,
+ "TSIG Other Data length does not match Other Len");
+ }
+ // RFC2845 says Other Data is "empty unless Error == BADTIME".
+ // However, we don't enforce that.
+
+ return (new TSIGImpl(canonical_algorithm_name, time_signed, fudge, mac,
+ orig_id, error, other_data));
+}
+
/// \brief Constructor from string.
///
+/// The given string must represent a valid TSIG RDATA. There can be extra
+/// space characters at the beginning or end of the text (which are simply
+/// ignored), but other extra text, including a new line, will make the
+/// construction fail with an exception.
+///
/// \c tsig_str must be formatted as follows:
-/// \code <Alg> <Time> <Fudge> <MACsize> [<MAC>] <OrigID> <Error> <OtherLen> [<OtherData>]
+/// \code <Algorithm Name> <Time Signed> <Fudge> <MAC Size> [<MAC>]
+/// <Original ID> <Error> <Other Len> [<Other Data>]
/// \endcode
-/// where
-/// - <Alg> is a valid textual representation of domain name.
-/// - <Time> is an unsigned 48-bit decimal integer.
-/// - <MACSize>, <OrigID>, and <OtherLen> are an unsigned
-/// 16-bit decimal
-/// integer.
-/// - <Error> is an unsigned 16-bit decimal integer or a valid mnemonic
-/// for the Error field specified in RFC2845. Currently, "BADSIG", "BADKEY",
-/// and "BADTIME" are supported (case sensitive). In future versions
-/// other representations that are compatible with the DNS RCODE will be
-/// supported.
-/// - <MAC> and <OtherData> is a BASE-64 encoded string that does
-/// not contain space characters.
-/// When <MACSize> and <OtherLen> is 0, <MAC> and
-/// <OtherData> must not appear in \c tsig_str, respectively.
-/// - The decoded data of <MAC> is <MACSize> bytes of binary
-/// stream.
-/// - The decoded data of <OtherData> is <OtherLen> bytes of
-/// binary stream.
+///
+/// Note that, since the Algorithm Name field is defined to be "in domain name
+/// syntax", but it is not actually a domain name, it does not have to be
+/// fully qualified.
+///
+/// The Error field is an unsigned 16-bit decimal integer or a valid mnemonic
+/// as specified in RFC2845. Currently, "NOERROR", "BADSIG", "BADKEY", and
+/// "BADTIME" are supported (case sensitive). In future versions other
+/// representations that are compatible with the DNS RCODE may be supported.
+///
+/// The MAC and Other Data fields are base-64 encoded strings that do not
+/// contain space characters.
+/// If the MAC Size field is 0, the MAC field must not appear in \c tsig_str.
+/// If the Other Len field is 0, the Other Data field must not appear in
+/// \c tsig_str.
+/// The decoded data of the MAC field is MAC Size bytes of binary stream.
+/// The decoded data of the Other Data field is Other Len bytes of binary
+/// stream.
///
/// An example of valid string is:
/// \code "hmac-sha256. 853804800 300 3 AAAA 2845 0 0" \endcode
-/// In this example <OtherData> is missing because <OtherLen> is 0.
+/// In this example Other Data is missing because Other Len is 0.
///
/// Note that RFC2845 does not define the standard presentation format
/// of %TSIG RR, so the above syntax is implementation specific.
/// This is, however, compatible with the format acceptable to BIND 9's
/// RDATA parser.
///
-/// <b>Exceptions</b>
+/// \throw Others Exception from the Name constructors.
+/// \throw InvalidRdataText if any fields are out of their valid range,
+/// or are incorrect.
+/// \throw BadValue if MAC or Other Data is not validly encoded in base-64.
///
-/// If <Alg> is not a valid domain name, a corresponding exception from
-/// the \c Name class will be thrown;
-/// if <MAC> or <OtherData> is not validly encoded in BASE-64, an
-/// exception of class \c isc::BadValue will be thrown;
-/// if %any of the other bullet points above is not met, an exception of
-/// class \c InvalidRdataText will be thrown.
-/// This constructor internally involves resource allocation, and if it fails
-/// a corresponding standard exception will be thrown.
+/// \param tsig_str A string containing the RDATA to be created
TSIG::TSIG(const std::string& tsig_str) : impl_(NULL) {
- istringstream iss(tsig_str);
+ // We use auto_ptr here because if there is an exception in this
+ // constructor, the destructor is not called and there could be a
+ // leak of the TSIGImpl that constructFromLexer() returns.
+ std::auto_ptr<TSIGImpl> impl_ptr(NULL);
try {
- const Name algorithm(getToken(iss));
- const int64_t time_signed = tokenToNum<int64_t, 48>(getToken(iss));
- const int32_t fudge = tokenToNum<int32_t, 16>(getToken(iss));
- const int32_t macsize = tokenToNum<int32_t, 16>(getToken(iss));
-
- const string mac_txt = (macsize > 0) ? getToken(iss) : "";
- vector<uint8_t> mac;
- decodeBase64(mac_txt, mac);
- if (mac.size() != macsize) {
- isc_throw(InvalidRdataText, "TSIG MAC size and data are inconsistent");
- }
+ std::istringstream ss(tsig_str);
+ MasterLexer lexer;
+ lexer.pushSource(ss);
- const int32_t orig_id = tokenToNum<int32_t, 16>(getToken(iss));
-
- const string error_txt = getToken(iss);
- int32_t error = 0;
- // XXX: In the initial implementation we hardcode the mnemonics.
- // We'll soon generalize this.
- if (error_txt == "BADSIG") {
- error = 16;
- } else if (error_txt == "BADKEY") {
- error = 17;
- } else if (error_txt == "BADTIME") {
- error = 18;
- } else {
- error = tokenToNum<int32_t, 16>(error_txt);
- }
+ impl_ptr.reset(constructFromLexer(lexer, NULL));
- const int32_t otherlen = tokenToNum<int32_t, 16>(getToken(iss));
- const string otherdata_txt = (otherlen > 0) ? getToken(iss) : "";
- vector<uint8_t> other_data;
- decodeBase64(otherdata_txt, other_data);
-
- if (!iss.eof()) {
- isc_throw(InvalidRdataText, "Unexpected input for TSIG RDATA: " <<
- tsig_str);
+ if (lexer.getNextToken().getType() != MasterToken::END_OF_FILE) {
+ isc_throw(InvalidRdataText,
+ "Extra input text for TSIG: " << tsig_str);
}
+ } catch (const MasterLexer::LexerError& ex) {
+ isc_throw(InvalidRdataText,
+ "Failed to construct TSIG from '" << tsig_str << "': "
+ << ex.what());
+ }
- impl_ = new TSIGImpl(algorithm, time_signed, fudge, mac, orig_id,
- error, other_data);
+ impl_ = impl_ptr.release();
+}
- } catch (const StringTokenError& ste) {
- isc_throw(InvalidRdataText, "Invalid TSIG text: " << ste.what() <<
- ": " << tsig_str);
- }
+/// \brief Constructor with a context of MasterLexer.
+///
+/// The \c lexer should point to the beginning of valid textual
+/// representation of an TSIG RDATA.
+///
+/// See \c TSIG::TSIG(const std::string&) for description of the
+/// expected RDATA fields.
+///
+/// \throw MasterLexer::LexerError General parsing error such as
+/// missing field.
+/// \throw InvalidRdataText if any fields are out of their valid range,
+/// or are incorrect.
+///
+/// \param lexer A \c MasterLexer object parsing a master file for the
+/// RDATA to be created
+TSIG::TSIG(MasterLexer& lexer, const Name* origin,
+ MasterLoader::Options, MasterLoaderCallbacks&) :
+ impl_(constructFromLexer(lexer, origin))
+{
}
/// \brief Constructor from wire-format data.
@@ -183,7 +274,9 @@ TSIG::TSIG(const std::string& tsig_str) : impl_(NULL) {
/// But this constructor does not use this parameter; if necessary, the caller
/// must check consistency between the length parameter and the actual
/// RDATA length.
-TSIG::TSIG(InputBuffer& buffer, size_t) : impl_(NULL) {
+TSIG::TSIG(InputBuffer& buffer, size_t) :
+ impl_(NULL)
+{
Name algorithm(buffer);
uint8_t time_signed_buf[6];
@@ -213,8 +306,11 @@ TSIG::TSIG(InputBuffer& buffer, size_t) : impl_(NULL) {
buffer.readData(&other_data[0], other_len);
}
- impl_ = new TSIGImpl(algorithm, time_signed, fudge, mac, original_id,
- error, other_data);
+ const Name& canonical_algorithm_name =
+ (algorithm == TSIGKey::HMACMD5_SHORT_NAME()) ?
+ TSIGKey::HMACMD5_NAME() : algorithm;
+ impl_ = new TSIGImpl(canonical_algorithm_name, time_signed, fudge, mac,
+ original_id, error, other_data);
}
TSIG::TSIG(const Name& algorithm, uint64_t time_signed, uint16_t fudge,
@@ -235,8 +331,11 @@ TSIG::TSIG(const Name& algorithm, uint64_t time_signed, uint16_t fudge,
isc_throw(InvalidParameter,
"TSIG Other data length and data inconsistent");
}
- impl_ = new TSIGImpl(algorithm, time_signed, fudge, mac_size, mac,
- original_id, error, other_len, other_data);
+ const Name& canonical_algorithm_name =
+ (algorithm == TSIGKey::HMACMD5_SHORT_NAME()) ?
+ TSIGKey::HMACMD5_NAME() : algorithm;
+ impl_ = new TSIGImpl(canonical_algorithm_name, time_signed, fudge, mac_size,
+ mac, original_id, error, other_len, other_data);
}
/// \brief The copy constructor.
@@ -249,7 +348,7 @@ TSIG::TSIG(const TSIG& source) : Rdata(), impl_(new TSIGImpl(*source.impl_))
TSIG&
TSIG::operator=(const TSIG& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
@@ -281,13 +380,13 @@ TSIG::toText() const {
lexical_cast<string>(impl_->time_signed_) + " " +
lexical_cast<string>(impl_->fudge_) + " " +
lexical_cast<string>(impl_->mac_.size()) + " ";
- if (impl_->mac_.size() > 0) {
+ if (!impl_->mac_.empty()) {
result += encodeBase64(impl_->mac_) + " ";
}
result += lexical_cast<string>(impl_->original_id_) + " ";
result += TSIGError(impl_->error_).toText() + " ";
result += lexical_cast<string>(impl_->other_data_.size());
- if (impl_->other_data_.size() > 0) {
+ if (!impl_->other_data_.empty()) {
result += " " + encodeBase64(impl_->other_data_);
}
@@ -298,7 +397,7 @@ TSIG::toText() const {
// toWire().
template <typename Output>
void
-TSIG::TSIGImpl::toWireCommon(Output& output) const {
+TSIGImpl::toWireCommon(Output& output) const {
output.writeUint16(time_signed_ >> 32);
output.writeUint32(time_signed_ & 0xffffffff);
output.writeUint16(fudge_);
@@ -431,7 +530,7 @@ TSIG::getMACSize() const {
const void*
TSIG::getMAC() const {
- if (impl_->mac_.size() > 0) {
+ if (!impl_->mac_.empty()) {
return (&impl_->mac_[0]);
} else {
return (NULL);
@@ -455,7 +554,7 @@ TSIG::getOtherLen() const {
const void*
TSIG::getOtherData() const {
- if (impl_->other_data_.size() > 0) {
+ if (!impl_->other_data_.empty()) {
return (&impl_->other_data_[0]);
} else {
return (NULL);
diff --git a/src/lib/dns/rdata/any_255/tsig_250.h b/src/lib/dns/rdata/any_255/tsig_250.h
index ff24925..62f599a 100644
--- a/src/lib/dns/rdata/any_255/tsig_250.h
+++ b/src/lib/dns/rdata/any_255/tsig_250.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -18,14 +18,9 @@
#include <string>
+#include <dns/name.h>
#include <dns/rdata.h>
-namespace isc {
-namespace dns {
-class Name;
-}
-}
-
// BEGIN_ISC_NAMESPACE
// BEGIN_COMMON_DECLARATIONS
@@ -33,6 +28,8 @@ class Name;
// BEGIN_RDATA_NAMESPACE
+struct TSIGImpl;
+
/// \brief \c rdata::TSIG class represents the TSIG RDATA as defined %in
/// RFC2845.
///
@@ -145,7 +142,8 @@ public:
/// This method never throws an exception.
const void* getOtherData() const;
private:
- struct TSIGImpl;
+ TSIGImpl* constructFromLexer(MasterLexer& lexer, const Name* origin);
+
TSIGImpl* impl_;
};
diff --git a/src/lib/dns/rdata/ch_3/a_1.cc b/src/lib/dns/rdata/ch_3/a_1.cc
index 3d13a9e..5e2852f 100644
--- a/src/lib/dns/rdata/ch_3/a_1.cc
+++ b/src/lib/dns/rdata/ch_3/a_1.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -31,6 +31,12 @@ A::A(const std::string&) {
// TBD
}
+A::A(MasterLexer&, const Name*,
+ MasterLoader::Options, MasterLoaderCallbacks&)
+{
+ // TBD
+}
+
A::A(InputBuffer&, size_t) {
// TBD
}
diff --git a/src/lib/dns/rdata/generic/detail/ds_like.h b/src/lib/dns/rdata/generic/detail/ds_like.h
index a7bebbf..c143cab 100644
--- a/src/lib/dns/rdata/generic/detail/ds_like.h
+++ b/src/lib/dns/rdata/generic/detail/ds_like.h
@@ -188,12 +188,12 @@ public:
/// \brief The copy constructor.
///
/// Trivial for now, we could've used the default one.
- DSLikeImpl(const DSLikeImpl& source) {
- digest_ = source.digest_;
- tag_ = source.tag_;
- algorithm_ = source.algorithm_;
- digest_type_ = source.digest_type_;
- }
+ DSLikeImpl(const DSLikeImpl& source) :
+ tag_(source.tag_),
+ algorithm_(source.algorithm_),
+ digest_type_(source.digest_type_),
+ digest_(source.digest_)
+ {}
/// \brief Convert the DS-like data to a string.
///
diff --git a/src/lib/dns/rdata/generic/dlv_32769.cc b/src/lib/dns/rdata/generic/dlv_32769.cc
index 89a62e1..97b9d1a 100644
--- a/src/lib/dns/rdata/generic/dlv_32769.cc
+++ b/src/lib/dns/rdata/generic/dlv_32769.cc
@@ -62,7 +62,7 @@ DLV::DLV(const DLV& source) :
/// PIMPL-induced logic
DLV&
DLV::operator=(const DLV& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/dnskey_48.cc b/src/lib/dns/rdata/generic/dnskey_48.cc
index 2e9a9f3..3ef6c72 100644
--- a/src/lib/dns/rdata/generic/dnskey_48.cc
+++ b/src/lib/dns/rdata/generic/dnskey_48.cc
@@ -212,7 +212,7 @@ DNSKEY::DNSKEY(const DNSKEY& source) :
DNSKEY&
DNSKEY::operator=(const DNSKEY& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/ds_43.cc b/src/lib/dns/rdata/generic/ds_43.cc
index 3492388..21bf8f3 100644
--- a/src/lib/dns/rdata/generic/ds_43.cc
+++ b/src/lib/dns/rdata/generic/ds_43.cc
@@ -50,7 +50,7 @@ DS::DS(const DS& source) :
DS&
DS::operator=(const DS& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/minfo_14.cc b/src/lib/dns/rdata/generic/minfo_14.cc
index aa5272c..58d6f3c 100644
--- a/src/lib/dns/rdata/generic/minfo_14.cc
+++ b/src/lib/dns/rdata/generic/minfo_14.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -21,10 +21,12 @@
#include <dns/name.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
+#include <dns/rdata/generic/detail/lexer_util.h>
using namespace std;
using namespace isc::dns;
using namespace isc::util;
+using isc::dns::rdata::generic::detail::createNameFromLexer;
// BEGIN_ISC_NAMESPACE
// BEGIN_RDATA_NAMESPACE
@@ -39,12 +41,10 @@ using namespace isc::util;
/// An example of valid string is:
/// \code "rmail.example.com. email.example.com." \endcode
///
-/// <b>Exceptions</b>
-///
-/// \exception InvalidRdataText The number of RDATA fields (must be 2) is
+/// \throw InvalidRdataText The number of RDATA fields (must be 2) is
/// incorrect.
-/// \exception std::bad_alloc Memory allocation for names fails.
-/// \exception Other The constructor of the \c Name class will throw if the
+/// \throw std::bad_alloc Memory allocation for names fails.
+/// \throw Other The constructor of the \c Name class will throw if the
/// names in the string is invalid.
MINFO::MINFO(const std::string& minfo_str) :
// We cannot construct both names in the initialization list due to the
@@ -52,21 +52,44 @@ MINFO::MINFO(const std::string& minfo_str) :
// name and replace them later.
rmailbox_(Name::ROOT_NAME()), emailbox_(Name::ROOT_NAME())
{
- istringstream iss(minfo_str);
- string rmailbox_str, emailbox_str;
- iss >> rmailbox_str >> emailbox_str;
-
- // Validation: A valid MINFO RR must have exactly two fields.
- if (iss.bad() || iss.fail()) {
- isc_throw(InvalidRdataText, "Invalid MINFO text: " << minfo_str);
- }
- if (!iss.eof()) {
- isc_throw(InvalidRdataText, "Invalid MINFO text (redundant field): "
- << minfo_str);
+ try {
+ std::istringstream ss(minfo_str);
+ MasterLexer lexer;
+ lexer.pushSource(ss);
+
+ rmailbox_ = createNameFromLexer(lexer, NULL);
+ emailbox_ = createNameFromLexer(lexer, NULL);
+
+ if (lexer.getNextToken().getType() != MasterToken::END_OF_FILE) {
+ isc_throw(InvalidRdataText, "extra input text for MINFO: "
+ << minfo_str);
+ }
+ } catch (const MasterLexer::LexerError& ex) {
+ isc_throw(InvalidRdataText, "Failed to construct MINFO from '" <<
+ minfo_str << "': " << ex.what());
}
+}
- rmailbox_ = Name(rmailbox_str);
- emailbox_ = Name(emailbox_str);
+/// \brief Constructor with a context of MasterLexer.
+///
+/// The \c lexer should point to the beginning of valid textual representation
+/// of an MINFO RDATA. The RMAILBOX and EMAILBOX fields can be non-absolute
+/// if \c origin is non-NULL, in which case \c origin is used to make them
+/// absolute.
+///
+/// \throw MasterLexer::LexerError General parsing error such as missing field.
+/// \throw Other Exceptions from the Name and constructors if construction of
+/// textual fields as these objects fail.
+///
+/// \param lexer A \c MasterLexer object parsing a master file for the
+/// RDATA to be created
+/// \param origin If non NULL, specifies the origin of SERVER when it
+/// is non-absolute.
+MINFO::MINFO(MasterLexer& lexer, const Name* origin,
+ MasterLoader::Options, MasterLoaderCallbacks&) :
+ rmailbox_(createNameFromLexer(lexer, origin)),
+ emailbox_(createNameFromLexer(lexer, origin))
+{
}
/// \brief Constructor from wire-format data.
@@ -75,8 +98,8 @@ MINFO::MINFO(const std::string& minfo_str) :
/// length) for parsing.
/// If necessary, the caller will check consistency.
///
-/// \exception std::bad_alloc Memory allocation for names fails.
-/// \exception Other The constructor of the \c Name class will throw if the
+/// \throw std::bad_alloc Memory allocation for names fails.
+/// \throw Other The constructor of the \c Name class will throw if the
/// names in the wire is invalid.
MINFO::MINFO(InputBuffer& buffer, size_t) :
rmailbox_(buffer), emailbox_(buffer)
@@ -84,7 +107,7 @@ MINFO::MINFO(InputBuffer& buffer, size_t) :
/// \brief Copy constructor.
///
-/// \exception std::bad_alloc Memory allocation fails in copying internal
+/// \throw std::bad_alloc Memory allocation fails in copying internal
/// member variables (this should be very rare).
MINFO::MINFO(const MINFO& other) :
Rdata(), rmailbox_(other.rmailbox_), emailbox_(other.emailbox_)
@@ -95,7 +118,7 @@ MINFO::MINFO(const MINFO& other) :
/// The output of this method is formatted as described in the "from string"
/// constructor (\c MINFO(const std::string&))).
///
-/// \exception std::bad_alloc Internal resource allocation fails.
+/// \throw std::bad_alloc Internal resource allocation fails.
///
/// \return A \c string object that represents the \c MINFO object.
std::string
@@ -105,7 +128,7 @@ MINFO::toText() const {
/// \brief Render the \c MINFO in the wire format without name compression.
///
-/// \exception std::bad_alloc Internal resource allocation fails.
+/// \throw std::bad_alloc Internal resource allocation fails.
///
/// \param buffer An output buffer to store the wire data.
void
@@ -128,7 +151,7 @@ MINFO::operator=(const MINFO& source) {
/// As specified in RFC3597, TYPE MINFO is "well-known", the rmailbox and
/// emailbox fields (domain names) will be compressed.
///
-/// \exception std::bad_alloc Internal resource allocation fails.
+/// \throw std::bad_alloc Internal resource allocation fails.
///
/// \param renderer DNS message rendering context that encapsulates the
/// output buffer and name compression information.
diff --git a/src/lib/dns/rdata/generic/minfo_14.h b/src/lib/dns/rdata/generic/minfo_14.h
index f3ee1d0..ce4586f 100644
--- a/src/lib/dns/rdata/generic/minfo_14.h
+++ b/src/lib/dns/rdata/generic/minfo_14.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -45,7 +45,7 @@ public:
/// \brief Return the value of the rmailbox field.
///
- /// \exception std::bad_alloc If resource allocation for the returned
+ /// \throw std::bad_alloc If resource allocation for the returned
/// \c Name fails.
///
/// \note
@@ -64,7 +64,7 @@ public:
/// \brief Return the value of the emailbox field.
///
- /// \exception std::bad_alloc If resource allocation for the returned
+ /// \throw std::bad_alloc If resource allocation for the returned
/// \c Name fails.
Name getEmailbox() const { return (emailbox_); }
diff --git a/src/lib/dns/rdata/generic/nsec3_50.cc b/src/lib/dns/rdata/generic/nsec3_50.cc
index fd8f78d..667f4a4 100644
--- a/src/lib/dns/rdata/generic/nsec3_50.cc
+++ b/src/lib/dns/rdata/generic/nsec3_50.cc
@@ -200,7 +200,7 @@ NSEC3::NSEC3(const NSEC3& source) :
NSEC3&
NSEC3::operator=(const NSEC3& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/nsec3param_51.cc b/src/lib/dns/rdata/generic/nsec3param_51.cc
index 494746d..f07c569 100644
--- a/src/lib/dns/rdata/generic/nsec3param_51.cc
+++ b/src/lib/dns/rdata/generic/nsec3param_51.cc
@@ -139,7 +139,7 @@ NSEC3PARAM::NSEC3PARAM(const NSEC3PARAM& source) :
NSEC3PARAM&
NSEC3PARAM::operator=(const NSEC3PARAM& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/nsec_47.cc b/src/lib/dns/rdata/generic/nsec_47.cc
index ffa2c97..c2dca32 100644
--- a/src/lib/dns/rdata/generic/nsec_47.cc
+++ b/src/lib/dns/rdata/generic/nsec_47.cc
@@ -155,7 +155,7 @@ NSEC::NSEC(const NSEC& source) :
NSEC&
NSEC::operator=(const NSEC& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/rp_17.cc b/src/lib/dns/rdata/generic/rp_17.cc
index 781b55d..d3d86fd 100644
--- a/src/lib/dns/rdata/generic/rp_17.cc
+++ b/src/lib/dns/rdata/generic/rp_17.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -21,10 +21,12 @@
#include <dns/name.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
+#include <dns/rdata/generic/detail/lexer_util.h>
using namespace std;
using namespace isc::dns;
using namespace isc::util;
+using isc::dns::rdata::generic::detail::createNameFromLexer;
// BEGIN_ISC_NAMESPACE
// BEGIN_RDATA_NAMESPACE
@@ -36,32 +38,54 @@ using namespace isc::util;
/// \endcode
/// where both fields must represent a valid domain name.
///
-/// \exception InvalidRdataText The number of RDATA fields (must be 2) is
+/// \throw InvalidRdataText The number of RDATA fields (must be 2) is
/// incorrect.
-/// \exception Other The constructor of the \c Name class will throw if the
+/// \throw std::bad_alloc Memory allocation for names fails.
+/// \throw Other The constructor of the \c Name class will throw if the
/// given name is invalid.
-/// \exception std::bad_alloc Memory allocation for names fails.
RP::RP(const std::string& rp_str) :
// We cannot construct both names in the initialization list due to the
// necessary text processing, so we have to initialize them with a dummy
// name and replace them later.
mailbox_(Name::ROOT_NAME()), text_(Name::ROOT_NAME())
{
- istringstream iss(rp_str);
- string mailbox_str, text_str;
- iss >> mailbox_str >> text_str;
-
- // Validation: A valid RP RR must have exactly two fields.
- if (iss.bad() || iss.fail()) {
- isc_throw(InvalidRdataText, "Invalid RP text: " << rp_str);
- }
- if (!iss.eof()) {
- isc_throw(InvalidRdataText, "Invalid RP text (redundant field): "
- << rp_str);
+ try {
+ std::istringstream ss(rp_str);
+ MasterLexer lexer;
+ lexer.pushSource(ss);
+
+ mailbox_ = createNameFromLexer(lexer, NULL);
+ text_ = createNameFromLexer(lexer, NULL);
+
+ if (lexer.getNextToken().getType() != MasterToken::END_OF_FILE) {
+ isc_throw(InvalidRdataText, "extra input text for RP: "
+ << rp_str);
+ }
+ } catch (const MasterLexer::LexerError& ex) {
+ isc_throw(InvalidRdataText, "Failed to construct RP from '" <<
+ rp_str << "': " << ex.what());
}
+}
- mailbox_ = Name(mailbox_str);
- text_ = Name(text_str);
+/// \brief Constructor with a context of MasterLexer.
+///
+/// The \c lexer should point to the beginning of valid textual representation
+/// of an RP RDATA. The MAILBOX and TEXT fields can be non-absolute if \c
+/// origin is non-NULL, in which case \c origin is used to make them absolute.
+///
+/// \throw MasterLexer::LexerError General parsing error such as missing field.
+/// \throw Other Exceptions from the Name and constructors if construction of
+/// textual fields as these objects fail.
+///
+/// \param lexer A \c MasterLexer object parsing a master file for the
+/// RDATA to be created
+/// \param origin If non NULL, specifies the origin of SERVER when it
+/// is non-absolute.
+RP::RP(MasterLexer& lexer, const Name* origin,
+ MasterLoader::Options, MasterLoaderCallbacks&) :
+ mailbox_(createNameFromLexer(lexer, origin)),
+ text_(createNameFromLexer(lexer, origin))
+{
}
/// \brief Constructor from wire-format data.
@@ -70,15 +94,15 @@ RP::RP(const std::string& rp_str) :
/// length) for parsing.
/// If necessary, the caller will check consistency.
///
-/// \exception std::bad_alloc Memory allocation for names fails.
-/// \exception Other The constructor of the \c Name class will throw if the
+/// \throw std::bad_alloc Memory allocation for names fails.
+/// \throw Other The constructor of the \c Name class will throw if the
/// names in the wire is invalid.
RP::RP(InputBuffer& buffer, size_t) : mailbox_(buffer), text_(buffer) {
}
/// \brief Copy constructor.
///
-/// \exception std::bad_alloc Memory allocation fails in copying internal
+/// \throw std::bad_alloc Memory allocation fails in copying internal
/// member variables (this should be very rare).
RP::RP(const RP& other) :
Rdata(), mailbox_(other.mailbox_), text_(other.text_)
@@ -89,7 +113,7 @@ RP::RP(const RP& other) :
/// The output of this method is formatted as described in the "from string"
/// constructor (\c RP(const std::string&))).
///
-/// \exception std::bad_alloc Internal resource allocation fails.
+/// \throw std::bad_alloc Internal resource allocation fails.
///
/// \return A \c string object that represents the \c RP object.
std::string
@@ -97,20 +121,36 @@ RP::toText() const {
return (mailbox_.toText() + " " + text_.toText());
}
+/// \brief Render the \c RP in the wire format without name compression.
+///
+/// \throw std::bad_alloc Internal resource allocation fails.
+///
+/// \param buffer An output buffer to store the wire data.
void
RP::toWire(OutputBuffer& buffer) const {
mailbox_.toWire(buffer);
text_.toWire(buffer);
}
+/// \brief Render the \c RP in the wire format with taking into account
+/// compression.
+///
+// Type RP is not "well-known", and name compression must be disabled
+// per RFC3597.
+///
+/// \throw std::bad_alloc Internal resource allocation fails.
+///
+/// \param renderer DNS message rendering context that encapsulates the
+/// output buffer and name compression information.
void
RP::toWire(AbstractMessageRenderer& renderer) const {
- // Type RP is not "well-known", and name compression must be disabled
- // per RFC3597.
renderer.writeName(mailbox_, false);
renderer.writeName(text_, false);
}
+/// \brief Compare two instances of \c RP RDATA.
+///
+/// See documentation in \c Rdata.
int
RP::compare(const Rdata& other) const {
const RP& other_rp = dynamic_cast<const RP&>(other);
diff --git a/src/lib/dns/rdata/generic/rp_17.h b/src/lib/dns/rdata/generic/rp_17.h
index a90a530..2fb89df 100644
--- a/src/lib/dns/rdata/generic/rp_17.h
+++ b/src/lib/dns/rdata/generic/rp_17.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -49,9 +49,8 @@ public:
/// \brief Return the value of the mailbox field.
///
- /// This method normally does not throw an exception, but if resource
- /// allocation for the returned \c Name object fails, a corresponding
- /// standard exception will be thrown.
+ /// \throw std::bad_alloc If resource allocation for the returned
+ /// \c Name fails.
///
/// \note
/// Unlike the case of some other RDATA classes (such as
@@ -69,9 +68,8 @@ public:
/// \brief Return the value of the text field.
///
- /// This method normally does not throw an exception, but if resource
- /// allocation for the returned \c Name object fails, a corresponding
- /// standard exception will be thrown.
+ /// \throw std::bad_alloc If resource allocation for the returned
+ /// \c Name fails.
Name getText() const { return (text_); }
private:
diff --git a/src/lib/dns/rdata/generic/rrsig_46.cc b/src/lib/dns/rdata/generic/rrsig_46.cc
index 8e826d7..505c388 100644
--- a/src/lib/dns/rdata/generic/rrsig_46.cc
+++ b/src/lib/dns/rdata/generic/rrsig_46.cc
@@ -230,7 +230,7 @@ RRSIG::RRSIG(const RRSIG& source) :
RRSIG&
RRSIG::operator=(const RRSIG& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/spf_99.cc b/src/lib/dns/rdata/generic/spf_99.cc
index 4bf24e9..17c4e3c 100644
--- a/src/lib/dns/rdata/generic/spf_99.cc
+++ b/src/lib/dns/rdata/generic/spf_99.cc
@@ -42,7 +42,7 @@ using namespace isc::util;
/// This method never throws an exception otherwise.
SPF&
SPF::operator=(const SPF& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/generic/sshfp_44.cc b/src/lib/dns/rdata/generic/sshfp_44.cc
index 64b3cc1..2865ed1 100644
--- a/src/lib/dns/rdata/generic/sshfp_44.cc
+++ b/src/lib/dns/rdata/generic/sshfp_44.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -35,122 +35,239 @@ using namespace isc::util::encode;
// BEGIN_ISC_NAMESPACE
// BEGIN_RDATA_NAMESPACE
-SSHFP::SSHFP(InputBuffer& buffer, size_t rdata_len) {
- if (rdata_len < 2) {
- isc_throw(InvalidRdataLength, "SSHFP record too short");
+struct SSHFPImpl {
+ // straightforward representation of SSHFP RDATA fields
+ SSHFPImpl(uint8_t algorithm, uint8_t fingerprint_type,
+ vector<uint8_t>& fingerprint) :
+ algorithm_(algorithm),
+ fingerprint_type_(fingerprint_type),
+ fingerprint_(fingerprint)
+ {}
+
+ uint8_t algorithm_;
+ uint8_t fingerprint_type_;
+ const vector<uint8_t> fingerprint_;
+};
+
+// helper function for string and lexer constructors
+SSHFPImpl*
+SSHFP::constructFromLexer(MasterLexer& lexer) {
+ const uint32_t algorithm =
+ lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (algorithm > 255) {
+ isc_throw(InvalidRdataText, "SSHFP algorithm number out of range");
}
- algorithm_ = buffer.readUint8();
- fingerprint_type_ = buffer.readUint8();
-
- rdata_len -= 2;
- if (rdata_len > 0) {
- fingerprint_.resize(rdata_len);
- buffer.readData(&fingerprint_[0], rdata_len);
+ const uint32_t fingerprint_type =
+ lexer.getNextToken(MasterToken::NUMBER).getNumber();
+ if (fingerprint_type > 255) {
+ isc_throw(InvalidRdataText, "SSHFP fingerprint type out of range");
}
-}
-
-SSHFP::SSHFP(const std::string& sshfp_str) {
- std::istringstream iss(sshfp_str);
- std::stringbuf fingerprintbuf;
- uint32_t algorithm, fingerprint_type;
- iss >> algorithm >> fingerprint_type;
- if (iss.bad() || iss.fail()) {
- isc_throw(InvalidRdataText, "Invalid SSHFP text");
+ std::string fingerprint_str;
+ std::string fingerprint_substr;
+ while (true) {
+ const MasterToken& token =
+ lexer.getNextToken(MasterToken::STRING, true);
+ if ((token.getType() == MasterToken::END_OF_FILE) ||
+ (token.getType() == MasterToken::END_OF_LINE)) {
+ break;
+ }
+ token.getString(fingerprint_substr);
+ fingerprint_str.append(fingerprint_substr);
}
+ lexer.ungetToken();
- if (algorithm > 255) {
- isc_throw(InvalidRdataText, "SSHFP algorithm number out of range");
+ vector<uint8_t> fingerprint;
+ // If fingerprint is missing, it's OK. See the API documentation of the
+ // constructor.
+ if (fingerprint_str.size() > 0) {
+ decodeHex(fingerprint_str, fingerprint);
}
- if (fingerprint_type > 255) {
- isc_throw(InvalidRdataText, "SSHFP fingerprint type out of range");
- }
+ return (new SSHFPImpl(algorithm, fingerprint_type, fingerprint));
+}
+
+/// \brief Constructor from string.
+///
+/// The given string must represent a valid SSHFP RDATA. There can be
+/// extra space characters at the beginning or end of the text (which
+/// are simply ignored), but other extra text, including a new line,
+/// will make the construction fail with an exception.
+///
+/// The Algorithm and Fingerprint Type fields must be within their valid
+/// ranges, but are not constrained to the values defined in RFC4255.
+///
+/// The Fingerprint field may be absent, but if present it must contain a
+/// valid hex encoding of the fingerprint. For compatibility with BIND 9,
+/// whitespace is allowed in the hex text (RFC4255 is silent on the matter).
+///
+/// \throw InvalidRdataText if any fields are missing, out of their valid
+/// ranges, or incorrect.
+///
+/// \param sshfp_str A string containing the RDATA to be created
+SSHFP::SSHFP(const string& sshfp_str) :
+ impl_(NULL)
+{
+ // We use auto_ptr here because if there is an exception in this
+ // constructor, the destructor is not called and there could be a
+ // leak of the SSHFPImpl that constructFromLexer() returns.
+ std::auto_ptr<SSHFPImpl> impl_ptr(NULL);
- iss >> &fingerprintbuf;
try {
- decodeHex(fingerprintbuf.str(), fingerprint_);
+ std::istringstream ss(sshfp_str);
+ MasterLexer lexer;
+ lexer.pushSource(ss);
+
+ impl_ptr.reset(constructFromLexer(lexer));
+
+ if (lexer.getNextToken().getType() != MasterToken::END_OF_FILE) {
+ isc_throw(InvalidRdataText, "extra input text for SSHFP: "
+ << sshfp_str);
+ }
+ } catch (const MasterLexer::LexerError& ex) {
+ isc_throw(InvalidRdataText, "Failed to construct SSHFP from '" <<
+ sshfp_str << "': " << ex.what());
} catch (const isc::BadValue& e) {
isc_throw(InvalidRdataText,
"Bad SSHFP fingerprint: " << e.what());
}
- algorithm_ = algorithm;
- fingerprint_type_ = fingerprint_type;
+ impl_ = impl_ptr.release();
}
-SSHFP::SSHFP(uint8_t algorithm, uint8_t fingerprint_type,
- const std::string& fingerprint)
+/// \brief Constructor with a context of MasterLexer.
+///
+/// The \c lexer should point to the beginning of valid textual representation
+/// of an SSHFP RDATA.
+///
+/// \throw MasterLexer::LexerError General parsing error such as missing field.
+/// \throw InvalidRdataText Fields are out of their valid range, or are
+/// incorrect.
+/// \throw BadValue Fingerprint is not a valid hex string.
+///
+/// \param lexer A \c MasterLexer object parsing a master file for the
+/// RDATA to be created
+SSHFP::SSHFP(MasterLexer& lexer, const Name*,
+ MasterLoader::Options, MasterLoaderCallbacks&) :
+ impl_(constructFromLexer(lexer))
{
- algorithm_ = algorithm;
- fingerprint_type_ = fingerprint_type;
+}
+
+/// \brief Constructor from InputBuffer.
+///
+/// The passed buffer must contain a valid SSHFP RDATA.
+///
+/// The Algorithm and Fingerprint Type fields are not checked for unknown
+/// values. It is okay for the fingerprint data to be missing (see the
+/// description of the constructor from string).
+SSHFP::SSHFP(InputBuffer& buffer, size_t rdata_len) {
+ if (rdata_len < 2) {
+ isc_throw(InvalidRdataLength, "SSHFP record too short");
+ }
+
+ const uint8_t algorithm = buffer.readUint8();
+ const uint8_t fingerprint_type = buffer.readUint8();
+
+ vector<uint8_t> fingerprint;
+ rdata_len -= 2;
+ if (rdata_len > 0) {
+ fingerprint.resize(rdata_len);
+ buffer.readData(&fingerprint[0], rdata_len);
+ }
+
+ impl_ = new SSHFPImpl(algorithm, fingerprint_type, fingerprint);
+}
+SSHFP::SSHFP(uint8_t algorithm, uint8_t fingerprint_type,
+ const string& fingerprint_txt) :
+ impl_(NULL)
+{
+ vector<uint8_t> fingerprint;
try {
- decodeHex(fingerprint, fingerprint_);
+ decodeHex(fingerprint_txt, fingerprint);
} catch (const isc::BadValue& e) {
isc_throw(InvalidRdataText, "Bad SSHFP fingerprint: " << e.what());
}
+
+ impl_ = new SSHFPImpl(algorithm, fingerprint_type, fingerprint);
}
SSHFP::SSHFP(const SSHFP& other) :
- Rdata(), algorithm_(other.algorithm_),
- fingerprint_type_(other.fingerprint_type_),
- fingerprint_(other.fingerprint_)
+ Rdata(), impl_(new SSHFPImpl(*other.impl_))
{}
+SSHFP&
+SSHFP::operator=(const SSHFP& source) {
+ if (this == &source) {
+ return (*this);
+ }
+
+ SSHFPImpl* newimpl = new SSHFPImpl(*source.impl_);
+ delete impl_;
+ impl_ = newimpl;
+
+ return (*this);
+}
+
+SSHFP::~SSHFP() {
+ delete impl_;
+}
+
void
SSHFP::toWire(OutputBuffer& buffer) const {
- buffer.writeUint8(algorithm_);
- buffer.writeUint8(fingerprint_type_);
+ buffer.writeUint8(impl_->algorithm_);
+ buffer.writeUint8(impl_->fingerprint_type_);
- if (!fingerprint_.empty()) {
- buffer.writeData(&fingerprint_[0], fingerprint_.size());
+ if (!impl_->fingerprint_.empty()) {
+ buffer.writeData(&impl_->fingerprint_[0],
+ impl_->fingerprint_.size());
}
}
void
SSHFP::toWire(AbstractMessageRenderer& renderer) const {
- renderer.writeUint8(algorithm_);
- renderer.writeUint8(fingerprint_type_);
+ renderer.writeUint8(impl_->algorithm_);
+ renderer.writeUint8(impl_->fingerprint_type_);
- if (!fingerprint_.empty()) {
- renderer.writeData(&fingerprint_[0], fingerprint_.size());
+ if (!impl_->fingerprint_.empty()) {
+ renderer.writeData(&impl_->fingerprint_[0],
+ impl_->fingerprint_.size());
}
}
string
SSHFP::toText() const {
- return (lexical_cast<string>(static_cast<int>(algorithm_)) +
- " " + lexical_cast<string>(static_cast<int>(fingerprint_type_)) +
- (fingerprint_.empty() ? "" : " " + encodeHex(fingerprint_)));
+ return (lexical_cast<string>(static_cast<int>(impl_->algorithm_)) + " " +
+ lexical_cast<string>(static_cast<int>(impl_->fingerprint_type_)) +
+ (impl_->fingerprint_.empty() ? "" :
+ " " + encodeHex(impl_->fingerprint_)));
}
int
SSHFP::compare(const Rdata& other) const {
const SSHFP& other_sshfp = dynamic_cast<const SSHFP&>(other);
- /* This doesn't really make any sort of sense, but in the name of
- consistency... */
-
- if (algorithm_ < other_sshfp.algorithm_) {
+ if (impl_->algorithm_ < other_sshfp.impl_->algorithm_) {
return (-1);
- } else if (algorithm_ > other_sshfp.algorithm_) {
+ } else if (impl_->algorithm_ > other_sshfp.impl_->algorithm_) {
return (1);
}
- if (fingerprint_type_ < other_sshfp.fingerprint_type_) {
+ if (impl_->fingerprint_type_ < other_sshfp.impl_->fingerprint_type_) {
return (-1);
- } else if (fingerprint_type_ > other_sshfp.fingerprint_type_) {
+ } else if (impl_->fingerprint_type_ >
+ other_sshfp.impl_->fingerprint_type_) {
return (1);
}
- const size_t this_len = fingerprint_.size();
- const size_t other_len = other_sshfp.fingerprint_.size();
+ const size_t this_len = impl_->fingerprint_.size();
+ const size_t other_len = other_sshfp.impl_->fingerprint_.size();
const size_t cmplen = min(this_len, other_len);
if (cmplen > 0) {
- const int cmp = memcmp(&fingerprint_[0], &other_sshfp.fingerprint_[0],
+ const int cmp = memcmp(&impl_->fingerprint_[0],
+ &other_sshfp.impl_->fingerprint_[0],
cmplen);
if (cmp != 0) {
return (cmp);
@@ -168,17 +285,17 @@ SSHFP::compare(const Rdata& other) const {
uint8_t
SSHFP::getAlgorithmNumber() const {
- return (algorithm_);
+ return (impl_->algorithm_);
}
uint8_t
SSHFP::getFingerprintType() const {
- return (fingerprint_type_);
+ return (impl_->fingerprint_type_);
}
size_t
SSHFP::getFingerprintLen() const {
- return (fingerprint_.size());
+ return (impl_->fingerprint_.size());
}
// END_RDATA_NAMESPACE
diff --git a/src/lib/dns/rdata/generic/sshfp_44.h b/src/lib/dns/rdata/generic/sshfp_44.h
index d9ebea4..28ce0f3 100644
--- a/src/lib/dns/rdata/generic/sshfp_44.h
+++ b/src/lib/dns/rdata/generic/sshfp_44.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -28,12 +28,17 @@
// BEGIN_RDATA_NAMESPACE
+struct SSHFPImpl;
+
class SSHFP : public Rdata {
public:
// BEGIN_COMMON_MEMBERS
// END_COMMON_MEMBERS
- SSHFP(uint8_t algorithm, uint8_t fingerprint_type, const std::string& fingerprint);
+ SSHFP(uint8_t algorithm, uint8_t fingerprint_type,
+ const std::string& fingerprint);
+ SSHFP& operator=(const SSHFP& source);
+ ~SSHFP();
///
/// Specialized methods
@@ -43,11 +48,9 @@ public:
size_t getFingerprintLen() const;
private:
- /// Note: this is a prototype version; we may reconsider
- /// this representation later.
- uint8_t algorithm_;
- uint8_t fingerprint_type_;
- std::vector<uint8_t> fingerprint_;
+ SSHFPImpl* constructFromLexer(MasterLexer& lexer);
+
+ SSHFPImpl* impl_;
};
// END_RDATA_NAMESPACE
diff --git a/src/lib/dns/rdata/generic/txt_16.cc b/src/lib/dns/rdata/generic/txt_16.cc
index 1bd2eb1..ff5d0e1 100644
--- a/src/lib/dns/rdata/generic/txt_16.cc
+++ b/src/lib/dns/rdata/generic/txt_16.cc
@@ -33,7 +33,7 @@ using namespace isc::util;
TXT&
TXT::operator=(const TXT& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rdata/hs_4/a_1.cc b/src/lib/dns/rdata/hs_4/a_1.cc
index 3d13a9e..5e2852f 100644
--- a/src/lib/dns/rdata/hs_4/a_1.cc
+++ b/src/lib/dns/rdata/hs_4/a_1.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -31,6 +31,12 @@ A::A(const std::string&) {
// TBD
}
+A::A(MasterLexer&, const Name*,
+ MasterLoader::Options, MasterLoaderCallbacks&)
+{
+ // TBD
+}
+
A::A(InputBuffer&, size_t) {
// TBD
}
diff --git a/src/lib/dns/rdata/in_1/srv_33.cc b/src/lib/dns/rdata/in_1/srv_33.cc
index ac62071..fdb8f22 100644
--- a/src/lib/dns/rdata/in_1/srv_33.cc
+++ b/src/lib/dns/rdata/in_1/srv_33.cc
@@ -190,7 +190,7 @@ SRV::SRV(const SRV& source) :
SRV&
SRV::operator=(const SRV& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
diff --git a/src/lib/dns/rrclass-placeholder.h b/src/lib/dns/rrclass-placeholder.h
index 89dc49d..b4f1851 100644
--- a/src/lib/dns/rrclass-placeholder.h
+++ b/src/lib/dns/rrclass-placeholder.h
@@ -35,16 +35,6 @@ namespace dns {
// forward declarations
class AbstractMessageRenderer;
-class RRClass; // forward declaration to define MaybeRRClass.
-
-/// \brief A shortcut for a compound type to represent RRClass-or-not.
-///
-/// A value of this type can be interpreted in a boolean context, whose
-/// value is \c true if and only if it contains a valid RRClass object.
-/// And, if it contains a valid RRClass object, its value is accessible
-/// using \c operator*, just like a bare pointer to \c RRClass.
-typedef boost::optional<RRClass> MaybeRRClass;
-
///
/// \brief A standard DNS module exception that is thrown if an RRClass object
/// is being constructed from an unrecognized string.
@@ -163,12 +153,8 @@ public:
/// <code>RRClass(const std::string&)</code> constructor.
///
/// If the given text represents a valid RRClass, it returns a
- /// \c MaybeRRClass object that stores a corresponding \c RRClass
- /// object, which is accessible via \c operator*(). In this case
- /// the returned object will be interpreted as \c true in a boolean
- /// context. If the given text does not represent a valid RRClass,
- /// it returns a \c MaybeRRClass object which is interpreted as
- /// \c false in a boolean context.
+ /// pointer to a new \c RRClass object. If the given text does not
+ /// represent a valid RRClass, it returns \c NULL.
///
/// One main purpose of this function is to minimize the overhead
/// when the given text does not represent a valid RR class. For
@@ -183,9 +169,9 @@ public:
/// This function never throws the \c InvalidRRClass exception.
///
/// \param class_str A string representation of the \c RRClass.
- /// \return A MaybeRRClass object either storing an RRClass object
- /// for the given text or a \c false value.
- static MaybeRRClass createFromText(const std::string& class_str);
+ /// \return A new RRClass object for the given text or a \c NULL
+ /// value.
+ static RRClass* createFromText(const std::string& class_str);
///
/// We use the default copy constructor intentionally.
diff --git a/src/lib/dns/rrclass.cc b/src/lib/dns/rrclass.cc
index 76a2e73..81c307c 100644
--- a/src/lib/dns/rrclass.cc
+++ b/src/lib/dns/rrclass.cc
@@ -59,14 +59,14 @@ RRClass::toWire(AbstractMessageRenderer& renderer) const {
renderer.writeUint16(classcode_);
}
-MaybeRRClass
+RRClass*
RRClass::createFromText(const string& class_str) {
uint16_t class_code;
if (RRParamRegistry::getRegistry().textToClassCode(class_str,
class_code)) {
- return (MaybeRRClass(class_code));
+ return (new RRClass(class_code));
}
- return (MaybeRRClass());
+ return (NULL);
}
ostream&
diff --git a/src/lib/dns/rrparamregistry-placeholder.cc b/src/lib/dns/rrparamregistry-placeholder.cc
index 5960759..ae735bc 100644
--- a/src/lib/dns/rrparamregistry-placeholder.cc
+++ b/src/lib/dns/rrparamregistry-placeholder.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -41,35 +41,6 @@ using namespace isc::dns::rdata;
namespace isc {
namespace dns {
-namespace rdata {
-
-RdataPtr
-AbstractRdataFactory::create(MasterLexer& lexer, const Name*,
- MasterLoader::Options,
- MasterLoaderCallbacks&) const
-{
- std::string s;
-
- while (true) {
- const MasterToken& token = lexer.getNextToken();
- if ((token.getType() == MasterToken::END_OF_FILE) ||
- (token.getType() == MasterToken::END_OF_LINE)) {
- lexer.ungetToken(); // let the upper layer handle the end-of token
- break;
- }
-
- if (!s.empty()) {
- s += " ";
- }
-
- s += token.getString();
- }
-
- return (create(s));
-}
-
-} // end of namespace isc::dns::rdata
-
namespace {
///
/// The following function and class are a helper to define case-insensitive
@@ -190,10 +161,8 @@ typedef map<RRTypeClass, RdataFactoryPtr> RdataFactoryMap;
typedef map<RRType, RdataFactoryPtr> GenericRdataFactoryMap;
template <typename T>
-class OldRdataFactory : public AbstractRdataFactory {
+class RdataFactory : public AbstractRdataFactory {
public:
- using AbstractRdataFactory::create;
-
virtual RdataPtr create(const string& rdata_str) const
{
return (RdataPtr(new T(rdata_str)));
@@ -208,16 +177,11 @@ public:
{
return (RdataPtr(new T(dynamic_cast<const T&>(source))));
}
-};
-
-template <typename T>
-class RdataFactory : public OldRdataFactory<T> {
-public:
- using OldRdataFactory<T>::create;
virtual RdataPtr create(MasterLexer& lexer, const Name* origin,
MasterLoader::Options options,
- MasterLoaderCallbacks& callbacks) const {
+ MasterLoaderCallbacks& callbacks) const
+ {
return (RdataPtr(new T(lexer, origin, options, callbacks)));
}
};
diff --git a/src/lib/dns/rrparamregistry.h b/src/lib/dns/rrparamregistry.h
index bf86436..1d59e01 100644
--- a/src/lib/dns/rrparamregistry.h
+++ b/src/lib/dns/rrparamregistry.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -55,11 +55,11 @@ namespace rdata {
/// \brief The \c AbstractRdataFactory class is an abstract base class to
/// encapsulate a set of Rdata factory methods in a polymorphic way.
///
-/// An external developers who want to introduce a new or experimental RR type
-/// are expected to define a corresponding derived class of \c
+/// An external developer who wants to introduce a new or experimental RR type
+/// is expected to define a corresponding derived class of \c
/// AbstractRdataFactory and register it via \c RRParamRegistry.
///
-/// For other users of this API normally do not have to care about this class
+/// Other users of this API normally do not have to care about this class
/// or its derived classes; this class is generally intended to be used
/// as an internal utility of the API implementation.
class AbstractRdataFactory {
@@ -125,16 +125,9 @@ public:
/// of a specific RR type and class for \c RRParamRegistry::createRdata()
/// that uses \c MasterLexer. See its description for the expected
/// behavior and meaning of the parameters.
- ///
- /// \note Right now this is not defined as a pure virtual method and
- /// provides the default implementation. This is an intermediate
- /// workaround until we implement the underlying constructor for all
- /// supported \c Rdata classes; once it's completed the workaround
- /// default implementation should be removed and this method should become
- /// pure virtual.
virtual RdataPtr create(MasterLexer& lexer, const Name* origin,
MasterLoader::Options options,
- MasterLoaderCallbacks& callbacks) const;
+ MasterLoaderCallbacks& callbacks) const = 0;
//@}
};
diff --git a/src/lib/dns/rrset.cc b/src/lib/dns/rrset.cc
index 7ea01d0..8dfe884 100644
--- a/src/lib/dns/rrset.cc
+++ b/src/lib/dns/rrset.cc
@@ -17,6 +17,7 @@
#include <vector>
#include <boost/shared_ptr.hpp>
+#include <boost/foreach.hpp>
#include <util/buffer.h>
#include <dns/messagerenderer.h>
@@ -71,7 +72,10 @@ AbstractRRset::toText() const {
return (s);
}
-namespace {
+namespace { // unnamed namespace
+
+// FIXME: This method's code should somehow be unified with
+// BasicRRsetImpl::toWire() below to avoid duplication.
template <typename T>
inline unsigned int
rrsetToWire(const AbstractRRset& rrset, T& output, const size_t limit) {
@@ -124,7 +128,8 @@ rrsetToWire(const AbstractRRset& rrset, T& output, const size_t limit) {
return (n);
}
-}
+
+} // end of unnamed namespace
unsigned int
AbstractRRset::toWire(OutputBuffer& buffer) const {
@@ -164,6 +169,9 @@ public:
BasicRRsetImpl(const Name& name, const RRClass& rrclass,
const RRType& rrtype, const RRTTL& ttl) :
name_(name), rrclass_(rrclass), rrtype_(rrtype), ttl_(ttl) {}
+
+ unsigned int toWire(AbstractMessageRenderer& renderer, size_t limit) const;
+
Name name_;
RRClass rrclass_;
RRType rrtype_;
@@ -174,6 +182,58 @@ public:
vector<ConstRdataPtr> rdatalist_;
};
+// FIXME: This method's code should somehow be unified with
+// rrsetToWire() above to avoid duplication.
+unsigned int
+BasicRRsetImpl::toWire(AbstractMessageRenderer& renderer, size_t limit) const {
+ if (rdatalist_.empty()) {
+ // empty rrsets are only allowed for classes ANY and NONE
+ if (rrclass_ != RRClass::ANY() &&
+ rrclass_ != RRClass::NONE()) {
+ isc_throw(EmptyRRset, "toWire() is attempted for an empty RRset");
+ }
+
+ // For an empty RRset, write the name, type, class and TTL once,
+ // followed by empty rdata.
+ name_.toWire(renderer);
+ rrtype_.toWire(renderer);
+ rrclass_.toWire(renderer);
+ ttl_.toWire(renderer);
+ renderer.writeUint16(0);
+ // Still counts as 1 'rr'; it does show up in the message
+ return (1);
+ }
+
+ unsigned int n = 0;
+
+ // sort the set of Rdata based on rrset-order and sortlist, and possible
+ // other options. Details to be considered.
+ BOOST_FOREACH(const ConstRdataPtr& rdata, rdatalist_) {
+ const size_t pos0 = renderer.getLength();
+ assert(pos0 < 65536);
+
+ name_.toWire(renderer);
+ rrtype_.toWire(renderer);
+ rrclass_.toWire(renderer);
+ ttl_.toWire(renderer);
+
+ const size_t pos = renderer.getLength();
+ renderer.skip(sizeof(uint16_t)); // leave the space for RDLENGTH
+ rdata->toWire(renderer);
+ renderer.writeUint16At(renderer.getLength() - pos - sizeof(uint16_t),
+ pos);
+
+ if (limit > 0 && renderer.getLength() > limit) {
+ // truncation is needed
+ renderer.trim(renderer.getLength() - pos0);
+ return (n);
+ }
+ ++n;
+ }
+
+ return (n);
+}
+
BasicRRset::BasicRRset(const Name& name, const RRClass& rrclass,
const RRType& rrtype, const RRTTL& ttl)
{
@@ -220,11 +280,6 @@ BasicRRset::getTTL() const {
}
void
-BasicRRset::setName(const Name& name) {
- impl_->name_ = name;
-}
-
-void
BasicRRset::setTTL(const RRTTL& ttl) {
impl_->ttl_ = ttl;
}
@@ -241,7 +296,12 @@ BasicRRset::toWire(OutputBuffer& buffer) const {
unsigned int
BasicRRset::toWire(AbstractMessageRenderer& renderer) const {
- return (AbstractRRset::toWire(renderer));
+ const unsigned int rrs_written = impl_->toWire(renderer,
+ renderer.getLengthLimit());
+ if (impl_->rdatalist_.size() > rrs_written) {
+ renderer.setTruncated();
+ }
+ return (rrs_written);
}
RRset::RRset(const Name& name, const RRClass& rrclass,
@@ -264,15 +324,13 @@ RRset::getRRsigDataCount() const {
unsigned int
RRset::toWire(OutputBuffer& buffer) const {
- unsigned int rrs_written;
-
- rrs_written = rrsetToWire<OutputBuffer>(*this, buffer, 0);
+ unsigned int rrs_written = BasicRRset::toWire(buffer);
if (getRdataCount() > rrs_written) {
return (rrs_written);
}
if (rrsig_) {
- rrs_written += rrsetToWire<OutputBuffer>(*(rrsig_.get()), buffer, 0);
+ rrs_written += rrsig_->toWire(buffer);
}
return (rrs_written);
@@ -280,24 +338,17 @@ RRset::toWire(OutputBuffer& buffer) const {
unsigned int
RRset::toWire(AbstractMessageRenderer& renderer) const {
- unsigned int rrs_written;
-
- rrs_written =
- rrsetToWire<AbstractMessageRenderer>(*this, renderer,
- renderer.getLengthLimit());
+ unsigned int rrs_written = BasicRRset::toWire(renderer);
if (getRdataCount() > rrs_written) {
- renderer.setTruncated();
return (rrs_written);
}
if (rrsig_) {
- rrs_written +=
- rrsetToWire<AbstractMessageRenderer>(*(rrsig_.get()), renderer,
- renderer.getLengthLimit());
- }
+ rrs_written += rrsig_->toWire(renderer);
- if (getRdataCount() + getRRsigDataCount() > rrs_written) {
- renderer.setTruncated();
+ if (getRdataCount() + getRRsigDataCount() > rrs_written) {
+ renderer.setTruncated();
+ }
}
return (rrs_written);
diff --git a/src/lib/dns/rrset.h b/src/lib/dns/rrset.h
index 395cbdd..eb8fa6e 100644
--- a/src/lib/dns/rrset.h
+++ b/src/lib/dns/rrset.h
@@ -230,12 +230,6 @@ public:
/// TTL of the \c RRset.
virtual const RRTTL& getTTL() const = 0;
- /// \brief Updates the owner name of the \c RRset.
- ///
- /// \param name A reference to a \c Name class object to be copied as the
- /// new name.
- virtual void setName(const Name& name) = 0;
-
/// \brief Updates the TTL of the \c RRset.
///
/// \param ttl A reference to a \c RRTTL class object to be copied as the
@@ -680,17 +674,6 @@ public:
/// TTL of the \c RRset.
virtual const RRTTL& getTTL() const;
- /// \brief Updates the owner name of the \c RRset.
- ///
- /// This method normally does not throw an exception, but could throw
- /// some standard exception on resource allocation failure if the
- /// internal copy of the \c name involves resource allocation and it
- /// fails.
- ///
- /// \param name A reference to a \c Name class object to be copied as the
- /// new name.
- virtual void setName(const Name& name);
-
/// \brief Updates the TTL of the \c RRset.
///
/// This method never throws an exception.
@@ -842,14 +825,6 @@ public:
virtual unsigned int toWire(isc::util::OutputBuffer& buffer) const;
/// \brief Updates the owner name of the \c RRset, including RRSIGs if any
- virtual void setName(const Name& n) {
- BasicRRset::setName(n);
- if (rrsig_) {
- rrsig_->setName(n);
- }
- }
-
- /// \brief Updates the owner name of the \c RRset, including RRSIGs if any
virtual void setTTL(const RRTTL& ttl) {
BasicRRset::setTTL(ttl);
if (rrsig_) {
diff --git a/src/lib/dns/rrttl.cc b/src/lib/dns/rrttl.cc
index e7f8441..73ac259 100644
--- a/src/lib/dns/rrttl.cc
+++ b/src/lib/dns/rrttl.cc
@@ -74,10 +74,10 @@ parseTTLString(const string& ttlstr, uint32_t& ttlval, string* error_txt) {
const string::const_iterator end = ttlstr.end();
string::const_iterator pos = ttlstr.begin();
- // When we detect we have some units
- bool units_mode = false;
-
try {
+ // When we detect we have some units
+ bool units_mode = false;
+
while (pos != end) {
// Find the first unit, if there's any.
const string::const_iterator unit = find_if(pos, end, myIsalpha);
@@ -166,13 +166,13 @@ RRTTL::RRTTL(const std::string& ttlstr) {
}
}
-MaybeRRTTL
+RRTTL*
RRTTL::createFromText(const string& ttlstr) {
uint32_t ttlval;
if (parseTTLString(ttlstr, ttlval, NULL)) {
- return (MaybeRRTTL(ttlval));
+ return (new RRTTL(ttlval));
}
- return (MaybeRRTTL());
+ return (NULL);
}
RRTTL::RRTTL(InputBuffer& buffer) {
diff --git a/src/lib/dns/rrttl.h b/src/lib/dns/rrttl.h
index 23d57f4..35403b6 100644
--- a/src/lib/dns/rrttl.h
+++ b/src/lib/dns/rrttl.h
@@ -32,16 +32,6 @@ namespace dns {
// forward declarations
class AbstractMessageRenderer;
-class RRTTL; // forward declaration to define MaybeRRTTL
-
-/// \brief A shortcut for a compound type to represent RRTTL-or-not.
-///
-/// A value of this type can be interpreted in a boolean context, whose
-/// value is \c true if and only if it contains a valid RRTTL object.
-/// And, if it contains a valid RRTTL object, its value is accessible
-/// using \c operator*, just like a bare pointer to \c RRTTL.
-typedef boost::optional<RRTTL> MaybeRRTTL;
-
///
/// \brief A standard DNS module exception that is thrown if an RRTTL object
/// is being constructed from an unrecognized string.
@@ -123,12 +113,9 @@ public:
/// possible exception handling. This version is provided for such
/// purpose.
///
- /// If the given text represents a valid RRTTL, it returns a \c MaybeRRTTL
- /// object that stores a corresponding \c RRTTL object, which is
- /// accessible via \c operator*(). In this case the returned object will
- /// be interpreted as \c true in a boolean context. If the given text
- /// does not represent a valid RRTTL, it returns a \c MaybeRRTTL object
- /// which is interpreted as \c false in a boolean context.
+ /// If the given text represents a valid RRTTL, it returns a pointer
+ /// to a new RRTTL object. If the given text does not represent a
+ /// valid RRTTL, it returns \c NULL..
///
/// One main purpose of this function is to minimize the overhead
/// when the given text does not represent a valid RR TTL. For this
@@ -142,9 +129,8 @@ public:
/// This function never throws the \c InvalidRRTTL exception.
///
/// \param ttlstr A string representation of the \c RRTTL.
- /// \return An MaybeRRTTL object either storing an RRTTL object for
- /// the given text or a \c false value.
- static MaybeRRTTL createFromText(const std::string& ttlstr);
+ /// \return A new RRTTL object for the given text or a \c NULL value.
+ static RRTTL* createFromText(const std::string& ttlstr);
///
//@}
diff --git a/src/lib/dns/serial.h b/src/lib/dns/serial.h
index 678fb22..0cd6833 100644
--- a/src/lib/dns/serial.h
+++ b/src/lib/dns/serial.h
@@ -59,7 +59,10 @@ public:
/// \brief Direct assignment from other Serial
///
/// \param other The Serial to assign the value from
- void operator=(const Serial& other) { value_ = other.getValue(); }
+ Serial& operator=(const Serial& other) {
+ value_ = other.getValue();
+ return (*this);
+ }
/// \brief Direct assignment from value
///
diff --git a/src/lib/dns/tests/masterload_unittest.cc b/src/lib/dns/tests/masterload_unittest.cc
index dfd901a..51c999d 100644
--- a/src/lib/dns/tests/masterload_unittest.cc
+++ b/src/lib/dns/tests/masterload_unittest.cc
@@ -267,6 +267,15 @@ TEST_F(MasterLoadTest, loadRRNoComment) {
"\"aaa;bbb\"")));
}
+TEST_F(MasterLoadTest, nonAbsoluteOwner) {
+ // If the owner name is not absolute, the zone origin is assumed to be
+ // its origin.
+ rr_stream << "example.com 3600 IN A 192.0.2.1";
+ masterLoad(rr_stream, origin, zclass, callback);
+ EXPECT_EQ(1, results.size());
+ EXPECT_EQ(results[0]->getName(), Name("example.com.example.com"));
+}
+
TEST_F(MasterLoadTest, loadRREmptyAndComment) {
// There's no RDATA (invalid in this case) but a comment. This position
// shouldn't cause any disruption and should be treated as a normal error.
@@ -356,11 +365,6 @@ TEST_F(MasterLoadTest, loadBadRRText) {
stringstream rr_stream6("example.com. 3600 IN A");
EXPECT_THROW(masterLoad(rr_stream6, origin, zclass, callback),
MasterLoadError);
-
- // owner name is not absolute
- stringstream rr_stream7("example.com 3600 IN A 192.0.2.1");
- EXPECT_THROW(masterLoad(rr_stream7, origin, zclass, callback),
- MasterLoadError);
}
// This is a helper callback to test the case the input stream becomes bad
diff --git a/src/lib/dns/tests/question_unittest.cc b/src/lib/dns/tests/question_unittest.cc
index 54d0942..d1214a1 100644
--- a/src/lib/dns/tests/question_unittest.cc
+++ b/src/lib/dns/tests/question_unittest.cc
@@ -86,8 +86,14 @@ TEST_F(QuestionTest, fromWire) {
}
TEST_F(QuestionTest, toText) {
- EXPECT_EQ("foo.example.com. IN NS\n", test_question1.toText());
- EXPECT_EQ("bar.example.com. CH A\n", test_question2.toText());
+ EXPECT_EQ("foo.example.com. IN NS", test_question1.toText());
+ EXPECT_EQ("bar.example.com. CH A", test_question2.toText());
+
+ EXPECT_EQ("foo.example.com. IN NS", test_question1.toText(false));
+ EXPECT_EQ("bar.example.com. CH A", test_question2.toText(false));
+
+ EXPECT_EQ("foo.example.com. IN NS\n", test_question1.toText(true));
+ EXPECT_EQ("bar.example.com. CH A\n", test_question2.toText(true));
}
TEST_F(QuestionTest, toWireBuffer) {
diff --git a/src/lib/dns/tests/rdata_ds_like_unittest.cc b/src/lib/dns/tests/rdata_ds_like_unittest.cc
index c797199..ae6a360 100644
--- a/src/lib/dns/tests/rdata_ds_like_unittest.cc
+++ b/src/lib/dns/tests/rdata_ds_like_unittest.cc
@@ -47,15 +47,15 @@ template<> RRTYPE<generic::DLV>::RRTYPE() : RRType(RRType::DLV()) {}
template <class DS_LIKE>
class Rdata_DS_LIKE_Test : public RdataTest {
protected:
- static DS_LIKE const rdata_ds_like;
+ Rdata_DS_LIKE_Test() :
+ ds_like_txt("12892 5 2 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5"),
+ rdata_ds_like(ds_like_txt)
+ {}
+ const string ds_like_txt;
+ const DS_LIKE rdata_ds_like;
};
-string ds_like_txt("12892 5 2 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
- "5F0EB5C777586DE18DA6B5");
-
-template <class DS_LIKE>
-DS_LIKE const Rdata_DS_LIKE_Test<DS_LIKE>::rdata_ds_like(ds_like_txt);
-
// The list of types we want to test.
typedef testing::Types<generic::DS, generic::DLV> Implementations;
@@ -70,7 +70,7 @@ TYPED_TEST(Rdata_DS_LIKE_Test, createFromText) {
}
TYPED_TEST(Rdata_DS_LIKE_Test, toText_DS_LIKE) {
- EXPECT_EQ(ds_like_txt, this->rdata_ds_like.toText());
+ EXPECT_EQ(this->ds_like_txt, this->rdata_ds_like.toText());
}
TYPED_TEST(Rdata_DS_LIKE_Test, badText_DS_LIKE) {
@@ -96,7 +96,7 @@ TYPED_TEST(Rdata_DS_LIKE_Test, createFromWire_DS_LIKE) {
TYPED_TEST(Rdata_DS_LIKE_Test, createFromLexer_DS_LIKE) {
EXPECT_EQ(0, this->rdata_ds_like.compare(
*test::createRdataUsingLexer(RRTYPE<TypeParam>(), RRClass::IN(),
- ds_like_txt)));
+ this->ds_like_txt)));
// Whitespace is okay
EXPECT_EQ(0, this->rdata_ds_like.compare(
@@ -121,13 +121,13 @@ TYPED_TEST(Rdata_DS_LIKE_Test, createFromLexer_DS_LIKE) {
}
TYPED_TEST(Rdata_DS_LIKE_Test, assignment_DS_LIKE) {
- TypeParam copy((string(ds_like_txt)));
+ TypeParam copy(this->ds_like_txt);
copy = this->rdata_ds_like;
EXPECT_EQ(0, copy.compare(this->rdata_ds_like));
// Check if the copied data is valid even after the original is deleted
TypeParam* copy2 = new TypeParam(this->rdata_ds_like);
- TypeParam copy3((string(ds_like_txt)));
+ TypeParam copy3(this->ds_like_txt);
copy3 = *copy2;
delete copy2;
EXPECT_EQ(0, copy3.compare(this->rdata_ds_like));
@@ -143,7 +143,7 @@ TYPED_TEST(Rdata_DS_LIKE_Test, getTag_DS_LIKE) {
TYPED_TEST(Rdata_DS_LIKE_Test, toWireRenderer) {
Rdata_DS_LIKE_Test<TypeParam>::renderer.skip(2);
- TypeParam rdata_ds_like(ds_like_txt);
+ TypeParam rdata_ds_like(this->ds_like_txt);
rdata_ds_like.toWire(this->renderer);
vector<unsigned char> data;
@@ -156,7 +156,7 @@ TYPED_TEST(Rdata_DS_LIKE_Test, toWireRenderer) {
}
TYPED_TEST(Rdata_DS_LIKE_Test, toWireBuffer) {
- TypeParam rdata_ds_like(ds_like_txt);
+ TypeParam rdata_ds_like(this->ds_like_txt);
rdata_ds_like.toWire(this->obuffer);
}
@@ -179,8 +179,33 @@ string ds_like_txt6("12892 5 2 F2E184C0E1D615D20EB3C223ACED3B03C773DD952D"
"5F0EB5C777586DE18DA6B555");
TYPED_TEST(Rdata_DS_LIKE_Test, compare) {
+ const string ds_like_txt1(
+ "12892 5 2 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5");
+ // different tag
+ const string ds_like_txt2(
+ "12893 5 2 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5");
+ // different algorithm
+ const string ds_like_txt3(
+ "12892 6 2 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5");
+ // different digest type
+ const string ds_like_txt4(
+ "12892 5 3 F1E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5");
+ // different digest
+ const string ds_like_txt5(
+ "12892 5 2 F2E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B5");
+ // different digest length
+ const string ds_like_txt6(
+ "12892 5 2 F2E184C0E1D615D20EB3C223ACED3B03C773DD952D"
+ "5F0EB5C777586DE18DA6B555");
+
// trivial case: self equivalence
- EXPECT_EQ(0, TypeParam(ds_like_txt).compare(TypeParam(ds_like_txt)));
+ EXPECT_EQ(0, TypeParam(this->ds_like_txt).
+ compare(TypeParam(this->ds_like_txt)));
// non-equivalence tests
EXPECT_LT(TypeParam(ds_like_txt1).compare(TypeParam(ds_like_txt2)), 0);
diff --git a/src/lib/dns/tests/rdata_minfo_unittest.cc b/src/lib/dns/tests/rdata_minfo_unittest.cc
index 2f717fe..3ce6a6c 100644
--- a/src/lib/dns/tests/rdata_minfo_unittest.cc
+++ b/src/lib/dns/tests/rdata_minfo_unittest.cc
@@ -1,6 +1,6 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
-// Permission to use, copy, modify, and/or distribute this software for generic
+// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
@@ -12,6 +12,8 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include <string>
+
#include <util/buffer.h>
#include <dns/exceptions.h>
#include <dns/messagerenderer.h>
@@ -27,22 +29,62 @@
using isc::UnitTestUtil;
using namespace std;
-using namespace isc::dns;
using namespace isc::util;
+using namespace isc::dns;
using namespace isc::dns::rdata;
-// minfo text
-const char* const minfo_txt = "rmailbox.example.com. emailbox.example.com.";
-const char* const minfo_txt2 = "root.example.com. emailbox.example.com.";
-const char* const too_long_label = "01234567890123456789012345678901234567"
- "89012345678901234567890123";
-
namespace {
class Rdata_MINFO_Test : public RdataTest {
-public:
+protected:
Rdata_MINFO_Test():
- rdata_minfo(string(minfo_txt)), rdata_minfo2(string(minfo_txt2)) {}
-
+ minfo_txt("rmailbox.example.com. emailbox.example.com."),
+ minfo_txt2("root.example.com. emailbox.example.com."),
+ too_long_label("01234567890123456789012345678901234567"
+ "89012345678901234567890123."),
+ rdata_minfo(minfo_txt),
+ rdata_minfo2(minfo_txt2)
+ {}
+
+ void checkFromText_None(const string& rdata_str) {
+ checkFromText<generic::MINFO, isc::Exception, isc::Exception>(
+ rdata_str, rdata_minfo, false, false);
+ }
+
+ void checkFromText_LexerError(const string& rdata_str) {
+ checkFromText
+ <generic::MINFO, InvalidRdataText, MasterLexer::LexerError>(
+ rdata_str, rdata_minfo, true, true);
+ }
+
+ void checkFromText_TooLongLabel(const string& rdata_str) {
+ checkFromText<generic::MINFO, TooLongLabel, TooLongLabel>(
+ rdata_str, rdata_minfo, true, true);
+ }
+
+ void checkFromText_BadString(const string& rdata_str) {
+ checkFromText<generic::MINFO, InvalidRdataText, isc::Exception>(
+ rdata_str, rdata_minfo, true, false);
+ }
+
+ void checkFromText_EmptyLabel(const string& rdata_str) {
+ checkFromText<generic::MINFO, EmptyLabel, EmptyLabel>(
+ rdata_str, rdata_minfo, true, true);
+ }
+
+ void checkFromText_MissingOrigin(const string& rdata_str) {
+ checkFromText
+ <generic::MINFO, MissingNameOrigin, MissingNameOrigin>(
+ rdata_str, rdata_minfo, true, true);
+ }
+
+ void checkFromText_Origin(const string& rdata_str, const Name* origin) {
+ checkFromText<generic::MINFO, MissingNameOrigin, isc::Exception>(
+ rdata_str, rdata_minfo, true, false, origin);
+ }
+
+ const string minfo_txt;
+ const string minfo_txt2;
+ const string too_long_label;
const generic::MINFO rdata_minfo;
const generic::MINFO rdata_minfo2;
};
@@ -54,24 +96,35 @@ TEST_F(Rdata_MINFO_Test, createFromText) {
EXPECT_EQ(Name("root.example.com."), rdata_minfo2.getRmailbox());
EXPECT_EQ(Name("emailbox.example.com."), rdata_minfo2.getEmailbox());
+
+ checkFromText_None(minfo_txt);
+
+ // origin defined for lexer constructor, but not string constructor
+ const Name origin("example.com");
+ checkFromText_Origin("rmailbox emailbox", &origin);
+
+ // lexer constructor accepts extra text, but string constructor doesn't
+ checkFromText_BadString("rmailbox.example.com. emailbox.example.com. "
+ "extra.example.com.");
}
TEST_F(Rdata_MINFO_Test, badText) {
- // incomplete text
- EXPECT_THROW(generic::MINFO("root.example.com."),
- InvalidRdataText);
- // number of fields (must be 2) is incorrect
- EXPECT_THROW(generic::MINFO("root.example.com emailbox.example.com. "
- "example.com."),
- InvalidRdataText);
- // bad rmailbox name
- EXPECT_THROW(generic::MINFO("root.example.com. emailbox.example.com." +
- string(too_long_label)),
- TooLongLabel);
- // bad emailbox name
- EXPECT_THROW(generic::MINFO("root.example.com." +
- string(too_long_label) + " emailbox.example.com."),
- TooLongLabel);
+ // too long names
+ checkFromText_TooLongLabel("root.example.com." + too_long_label +
+ " emailbox.example.com.");
+ checkFromText_TooLongLabel("root.example.com. emailbox.example.com." +
+ too_long_label);
+
+ // invalid names
+ checkFromText_EmptyLabel("root..example.com. emailbox.example.com.");
+ checkFromText_EmptyLabel("root.example.com. emailbox..example.com.");
+
+ // missing name
+ checkFromText_LexerError("root.example.com.");
+
+ // missing origin
+ checkFromText_MissingOrigin("root.example.com emailbox.example.com.");
+ checkFromText_MissingOrigin("root.example.com. emailbox.example.com");
}
TEST_F(Rdata_MINFO_Test, createFromWire) {
@@ -103,12 +156,6 @@ TEST_F(Rdata_MINFO_Test, createFromWire) {
DNSMessageFORMERR);
}
-TEST_F(Rdata_MINFO_Test, createFromLexer) {
- EXPECT_EQ(0, rdata_minfo.compare(
- *test::createRdataUsingLexer(RRType::MINFO(), RRClass::IN(),
- minfo_txt)));
-}
-
TEST_F(Rdata_MINFO_Test, assignment) {
generic::MINFO copy((string(minfo_txt2)));
copy = rdata_minfo;
diff --git a/src/lib/dns/tests/rdata_rp_unittest.cc b/src/lib/dns/tests/rdata_rp_unittest.cc
index 5508d9c..38bec04 100644
--- a/src/lib/dns/tests/rdata_rp_unittest.cc
+++ b/src/lib/dns/tests/rdata_rp_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -41,6 +41,38 @@ protected:
obuffer(0)
{}
+ void checkFromText_None(const string& rdata_str) {
+ checkFromText<generic::RP, isc::Exception, isc::Exception>(
+ rdata_str, rdata_rp, false, false);
+ }
+
+ void checkFromText_LexerError(const string& rdata_str) {
+ checkFromText
+ <generic::RP, InvalidRdataText, MasterLexer::LexerError>(
+ rdata_str, rdata_rp, true, true);
+ }
+
+ void checkFromText_BadString(const string& rdata_str) {
+ checkFromText<generic::RP, InvalidRdataText, isc::Exception>(
+ rdata_str, rdata_rp, true, false);
+ }
+
+ void checkFromText_EmptyLabel(const string& rdata_str) {
+ checkFromText<generic::RP, EmptyLabel, EmptyLabel>(
+ rdata_str, rdata_rp, true, true);
+ }
+
+ void checkFromText_MissingOrigin(const string& rdata_str) {
+ checkFromText
+ <generic::RP, MissingNameOrigin, MissingNameOrigin>(
+ rdata_str, rdata_rp, true, true);
+ }
+
+ void checkFromText_Origin(const string& rdata_str, const Name* origin) {
+ checkFromText<generic::RP, MissingNameOrigin, isc::Exception>(
+ rdata_str, rdata_rp, true, false, origin);
+ }
+
const Name mailbox_name, text_name;
const generic::RP rdata_rp; // commonly used test RDATA
OutputBuffer obuffer;
@@ -52,17 +84,28 @@ TEST_F(Rdata_RP_Test, createFromText) {
EXPECT_EQ(mailbox_name, rdata_rp.getMailbox());
EXPECT_EQ(text_name, rdata_rp.getText());
- // Invalid textual input cases follow:
- // names are invalid
- EXPECT_THROW(generic::RP("bad..name. rp-text.example.com"), EmptyLabel);
- EXPECT_THROW(generic::RP("mailbox.example.com. bad..name"), EmptyLabel);
+ checkFromText_None("root.example.com. rp-text.example.com.");
+
+ // origin defined for lexer constructor, but not string constructor
+ const Name origin("example.com");
+ checkFromText_Origin("root rp-text", &origin);
+
+ // lexer constructor accepts extra text, but string constructor doesn't
+ checkFromText_BadString("root.example.com. rp-text.example.com. "
+ "extra.example.com.");
+}
+
+TEST_F(Rdata_RP_Test, badText) {
+ // invalid names
+ checkFromText_EmptyLabel("root..example.com. rp-text.example.com.");
+ checkFromText_EmptyLabel("root.example.com. rp-text..example.com.");
// missing field
- EXPECT_THROW(generic::RP("mailbox.example.com."), InvalidRdataText);
+ checkFromText_LexerError("root.example.com.");
- // redundant field
- EXPECT_THROW(generic::RP("mailbox.example.com. rp-text.example.com. "
- "redundant.example."), InvalidRdataText);
+ // missing origin
+ checkFromText_MissingOrigin("root.example.com rp-text.example.com.");
+ checkFromText_MissingOrigin("root.example.com. rp-text.example.com");
}
TEST_F(Rdata_RP_Test, createFromWire) {
@@ -106,17 +149,6 @@ TEST_F(Rdata_RP_Test, createFromParams) {
EXPECT_EQ(text_name, generic::RP(mailbox_name, text_name).getText());
}
-TEST_F(Rdata_RP_Test, createFromLexer) {
- EXPECT_EQ(0, rdata_rp.compare(
- *test::createRdataUsingLexer(RRType::RP(), RRClass::IN(),
- "root.example.com. "
- "rp-text.example.com.")));
-
- // Exceptions cause NULL to be returned.
- EXPECT_FALSE(test::createRdataUsingLexer(RRType::RP(), RRClass::IN(),
- "mailbox.example.com."));
-}
-
TEST_F(Rdata_RP_Test, toWireBuffer) {
// construct expected data
UnitTestUtil::readWireData("rdata_rp_toWire1.wire", expected_wire);
diff --git a/src/lib/dns/tests/rdata_sshfp_unittest.cc b/src/lib/dns/tests/rdata_sshfp_unittest.cc
index 6c13ad9..b85dfa5 100644
--- a/src/lib/dns/tests/rdata_sshfp_unittest.cc
+++ b/src/lib/dns/tests/rdata_sshfp_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -30,17 +30,50 @@
using isc::UnitTestUtil;
using namespace std;
+using namespace isc;
using namespace isc::dns;
using namespace isc::util;
using namespace isc::dns::rdata;
namespace {
class Rdata_SSHFP_Test : public RdataTest {
- // there's nothing to specialize
+protected:
+ Rdata_SSHFP_Test() :
+ sshfp_txt("2 1 123456789abcdef67890123456789abcdef67890"),
+ rdata_sshfp(sshfp_txt)
+ {}
+
+ void checkFromText_None(const string& rdata_str) {
+ checkFromText<generic::SSHFP, isc::Exception, isc::Exception>(
+ rdata_str, rdata_sshfp, false, false);
+ }
+
+ void checkFromText_InvalidText(const string& rdata_str) {
+ checkFromText<generic::SSHFP, InvalidRdataText, InvalidRdataText>(
+ rdata_str, rdata_sshfp, true, true);
+ }
+
+ void checkFromText_LexerError(const string& rdata_str) {
+ checkFromText
+ <generic::SSHFP, InvalidRdataText, MasterLexer::LexerError>(
+ rdata_str, rdata_sshfp, true, true);
+ }
+
+ void checkFromText_BadValue(const string& rdata_str) {
+ checkFromText<generic::SSHFP, InvalidRdataText, BadValue>(
+ rdata_str, rdata_sshfp, true, true);
+ }
+
+ void checkFromText_BadString(const string& rdata_str) {
+ checkFromText
+ <generic::SSHFP, InvalidRdataText, isc::Exception>(
+ rdata_str, rdata_sshfp, true, false);
+ }
+
+ const string sshfp_txt;
+ const generic::SSHFP rdata_sshfp;
};
-const string sshfp_txt("2 1 123456789abcdef67890123456789abcdef67890");
-const generic::SSHFP rdata_sshfp(2, 1, "123456789abcdef67890123456789abcdef67890");
const uint8_t rdata_sshfp_wiredata[] = {
// algorithm
0x02,
@@ -56,22 +89,23 @@ const uint8_t rdata_sshfp_wiredata[] = {
TEST_F(Rdata_SSHFP_Test, createFromText) {
// Basic test
- const generic::SSHFP rdata_sshfp2(sshfp_txt);
- EXPECT_EQ(0, rdata_sshfp2.compare(rdata_sshfp));
+ checkFromText_None(sshfp_txt);
// With different spacing
- const generic::SSHFP rdata_sshfp3("2 1 123456789abcdef67890123456789abcdef67890");
- EXPECT_EQ(0, rdata_sshfp3.compare(rdata_sshfp));
+ checkFromText_None("2 1 123456789abcdef67890123456789abcdef67890");
// Combination of lowercase and uppercase
- const generic::SSHFP rdata_sshfp4("2 1 123456789ABCDEF67890123456789abcdef67890");
- EXPECT_EQ(0, rdata_sshfp4.compare(rdata_sshfp));
-}
+ checkFromText_None("2 1 123456789ABCDEF67890123456789abcdef67890");
-TEST_F(Rdata_SSHFP_Test, createFromLexer) {
- EXPECT_EQ(0, rdata_sshfp.compare(
- *test::createRdataUsingLexer(RRType::SSHFP(), RRClass::IN(),
- "2 1 123456789abcdef67890123456789abcdef67890")));
+ // spacing in the fingerprint field
+ checkFromText_None("2 1 123456789abcdef67890 123456789abcdef67890");
+
+ // multi-line fingerprint field
+ checkFromText_None("2 1 ( 123456789abcdef67890\n 123456789abcdef67890 )");
+
+ // string constructor throws if there's extra text,
+ // but lexer constructor doesn't
+ checkFromText_BadString(sshfp_txt + "\n" + sshfp_txt);
}
TEST_F(Rdata_SSHFP_Test, algorithmTypes) {
@@ -101,13 +135,30 @@ TEST_F(Rdata_SSHFP_Test, algorithmTypes) {
}
TEST_F(Rdata_SSHFP_Test, badText) {
- EXPECT_THROW(const generic::SSHFP rdata_sshfp("1"), InvalidRdataText);
- EXPECT_THROW(const generic::SSHFP rdata_sshfp("BUCKLE MY SHOES"), InvalidRdataText);
- EXPECT_THROW(const generic::SSHFP rdata_sshfp("1 2 foo bar"), InvalidRdataText);
+ checkFromText_LexerError("1");
+ checkFromText_LexerError("ONE 2 123456789abcdef67890123456789abcdef67890");
+ checkFromText_LexerError("1 TWO 123456789abcdef67890123456789abcdef67890");
+ checkFromText_BadValue("1 2 BUCKLEMYSHOE");
+ checkFromText_BadValue(sshfp_txt + " extra text");
+
+ // yes, these are redundant to the last test cases in algorithmTypes
+ checkFromText_InvalidText(
+ "2345 1 123456789abcdef67890123456789abcdef67890");
+ checkFromText_InvalidText(
+ "2 1234 123456789abcdef67890123456789abcdef67890");
+
+ // negative values are trapped in the lexer rather than the constructor
+ checkFromText_LexerError("-2 1 123456789abcdef67890123456789abcdef67890");
+ checkFromText_LexerError("2 -1 123456789abcdef67890123456789abcdef67890");
}
-TEST_F(Rdata_SSHFP_Test, copy) {
- const generic::SSHFP rdata_sshfp2(rdata_sshfp);
+TEST_F(Rdata_SSHFP_Test, copyAndAssign) {
+ // Copy construct
+ generic::SSHFP rdata_sshfp2(rdata_sshfp);
+ EXPECT_EQ(0, rdata_sshfp.compare(rdata_sshfp2));
+
+ // Assignment, mainly to confirm it doesn't cause disruption.
+ rdata_sshfp2 = rdata_sshfp;
EXPECT_EQ(0, rdata_sshfp.compare(rdata_sshfp2));
}
@@ -160,6 +211,12 @@ TEST_F(Rdata_SSHFP_Test, createFromWire) {
InvalidBufferPosition);
}
+TEST_F(Rdata_SSHFP_Test, createFromParams) {
+ const generic::SSHFP rdata_sshfp2(
+ 2, 1, "123456789abcdef67890123456789abcdef67890");
+ EXPECT_EQ(0, rdata_sshfp2.compare(rdata_sshfp));
+}
+
TEST_F(Rdata_SSHFP_Test, toText) {
EXPECT_TRUE(boost::iequals(sshfp_txt, rdata_sshfp.toText()));
diff --git a/src/lib/dns/tests/rdata_tsig_unittest.cc b/src/lib/dns/tests/rdata_tsig_unittest.cc
index df35842..270a1b2 100644
--- a/src/lib/dns/tests/rdata_tsig_unittest.cc
+++ b/src/lib/dns/tests/rdata_tsig_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -23,6 +23,7 @@
#include <dns/rdataclass.h>
#include <dns/rrclass.h>
#include <dns/rrtype.h>
+#include <dns/tsigerror.h>
#include <gtest/gtest.h>
@@ -31,34 +32,84 @@
using isc::UnitTestUtil;
using namespace std;
+using namespace isc;
using namespace isc::dns;
using namespace isc::util;
using namespace isc::dns::rdata;
namespace {
+
class Rdata_TSIG_Test : public RdataTest {
protected:
- vector<uint8_t> expect_data;
-};
+ Rdata_TSIG_Test() :
+ // no MAC or Other Data
+ valid_text1("hmac-md5.sig-alg.reg.int. 1286779327 300 "
+ "0 16020 BADKEY 0"),
+ // MAC but no Other Data
+ valid_text2("hmac-sha256. 1286779327 300 12 "
+ "FAKEFAKEFAKEFAKE 16020 BADSIG 0"),
+ // MAC and Other Data
+ valid_text3("hmac-sha1. 1286779327 300 12 "
+ "FAKEFAKEFAKEFAKE 16020 BADTIME 6 FAKEFAKE"),
+ // MAC and Other Data (with Error that doesn't expect Other Data)
+ valid_text4("hmac-sha1. 1286779327 300 12 "
+ "FAKEFAKEFAKEFAKE 16020 BADSIG 6 FAKEFAKE"),
+ // numeric error code
+ valid_text5("hmac-sha256. 1286779327 300 12 "
+ "FAKEFAKEFAKEFAKE 16020 2845 0"),
+ rdata_tsig(valid_text1)
+ {}
+
+ void checkFromText_None(const string& rdata_str) {
+ checkFromText<any::TSIG, isc::Exception, isc::Exception>(
+ rdata_str, rdata_tsig, false, false);
+ }
+
+ void checkFromText_InvalidText(const string& rdata_str) {
+ checkFromText<any::TSIG, InvalidRdataText, InvalidRdataText>(
+ rdata_str, rdata_tsig, true, true);
+ }
+
+ void checkFromText_BadValue(const string& rdata_str) {
+ checkFromText<any::TSIG, BadValue, BadValue>(
+ rdata_str, rdata_tsig, true, true);
+ }
+
+ void checkFromText_LexerError(const string& rdata_str) {
+ checkFromText
+ <any::TSIG, InvalidRdataText, MasterLexer::LexerError>(
+ rdata_str, rdata_tsig, true, true);
+ }
+
+ void checkFromText_TooLongLabel(const string& rdata_str) {
+ checkFromText<any::TSIG, TooLongLabel, TooLongLabel>(
+ rdata_str, rdata_tsig, true, true);
+ }
-const char* const valid_text1 = "hmac-md5.sig-alg.reg.int. 1286779327 300 "
- "0 16020 BADKEY 0";
-const char* const valid_text2 = "hmac-sha256. 1286779327 300 12 "
- "FAKEFAKEFAKEFAKE 16020 BADSIG 0";
+ void checkFromText_EmptyLabel(const string& rdata_str) {
+ checkFromText<any::TSIG, EmptyLabel, EmptyLabel>(
+ rdata_str, rdata_tsig, true, true);
+ }
+
+ void checkFromText_BadString(const string& rdata_str) {
+ checkFromText
+ <any::TSIG, InvalidRdataText, isc::Exception>(
+ rdata_str, rdata_tsig, true, false);
+ }
-const char* const valid_text3 = "hmac-sha1. 1286779327 300 12 "
- "FAKEFAKEFAKEFAKE 16020 BADTIME 6 FAKEFAKE";
-const char* const valid_text4 = "hmac-sha1. 1286779327 300 12 "
- "FAKEFAKEFAKEFAKE 16020 BADSIG 6 FAKEFAKE";
-const char* const valid_text5 = "hmac-sha256. 1286779327 300 12 "
- "FAKEFAKEFAKEFAKE 16020 2845 0"; // using numeric error code
-const char* const too_long_label = "012345678901234567890123456789"
- "0123456789012345678901234567890123";
+ template <typename Output>
+ void toWireCommonChecks(Output& output) const;
-// commonly used test RDATA
-const any::TSIG rdata_tsig((string(valid_text1)));
+ const string valid_text1;
+ const string valid_text2;
+ const string valid_text3;
+ const string valid_text4;
+ const string valid_text5;
+ vector<uint8_t> expect_data;
+ const any::TSIG rdata_tsig; // commonly used test RDATA
+};
-TEST_F(Rdata_TSIG_Test, createFromText) {
+TEST_F(Rdata_TSIG_Test, fromText) {
// normal case. it also tests getter methods.
EXPECT_EQ(Name("hmac-md5.sig-alg.reg.int"), rdata_tsig.getAlgorithm());
EXPECT_EQ(1286779327, rdata_tsig.getTimeSigned());
@@ -66,59 +117,84 @@ TEST_F(Rdata_TSIG_Test, createFromText) {
EXPECT_EQ(0, rdata_tsig.getMACSize());
EXPECT_EQ(static_cast<void*>(NULL), rdata_tsig.getMAC());
EXPECT_EQ(16020, rdata_tsig.getOriginalID());
- EXPECT_EQ(17, rdata_tsig.getError()); // TODO: use constant
+ EXPECT_EQ(TSIGError::BAD_KEY_CODE, rdata_tsig.getError());
EXPECT_EQ(0, rdata_tsig.getOtherLen());
EXPECT_EQ(static_cast<void*>(NULL), rdata_tsig.getOtherData());
- any::TSIG tsig2((string(valid_text2)));
+ any::TSIG tsig2(valid_text2);
EXPECT_EQ(12, tsig2.getMACSize());
- EXPECT_EQ(16, tsig2.getError()); // TODO: use constant
+ EXPECT_EQ(TSIGError::BAD_SIG_CODE, tsig2.getError());
- any::TSIG tsig3((string(valid_text3)));
+ any::TSIG tsig3(valid_text3);
EXPECT_EQ(6, tsig3.getOtherLen());
// The other data is unusual, but we don't reject it.
- EXPECT_NO_THROW(any::TSIG(string(valid_text4)));
+ EXPECT_NO_THROW(any::TSIG tsig4(valid_text4));
// numeric representation of TSIG error
- any::TSIG tsig5((string(valid_text5)));
+ any::TSIG tsig5(valid_text5);
EXPECT_EQ(2845, tsig5.getError());
- //
- // invalid cases
- //
- // there's a garbage parameter at the end
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 BADKEY 0 0"), InvalidRdataText);
- // input is too short
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 BADKEY"), InvalidRdataText);
+ // not fully qualified algorithm name
+ any::TSIG tsig1("hmac-md5.sig-alg.reg.int 1286779327 300 "
+ "0 16020 BADKEY 0");
+ EXPECT_EQ(0, tsig1.compare(rdata_tsig));
+
+ // multi-line rdata
+ checkFromText_None("hmac-md5.sig-alg.reg.int. ( 1286779327 300 \n"
+ "0 16020 BADKEY 0 )");
+
+ // short-form HMAC-MD5 name
+ const any::TSIG tsig6("hmac-md5. 1286779327 300 0 16020 BADKEY 0");
+ EXPECT_EQ(0, tsig6.compare(rdata_tsig));
+};
+
+TEST_F(Rdata_TSIG_Test, badText) {
+ // too many fields
+ checkFromText_BadString(valid_text1 + " 0 0");
+ // not enough fields
+ checkFromText_LexerError("foo 0 0 0 0 BADKEY");
// bad domain name
- EXPECT_THROW(any::TSIG(string(too_long_label) + "0 0 0 0 BADKEY 0"),
- TooLongLabel);
+ checkFromText_TooLongLabel(
+ "0123456789012345678901234567890123456789012345678901234567890123"
+ " 0 0 0 0 BADKEY 0");
+ checkFromText_EmptyLabel("foo..bar 0 0 0 0 BADKEY");
// time is too large (2814...6 is 2^48)
- EXPECT_THROW(any::TSIG("foo 281474976710656 0 0 0 BADKEY 0"),
- InvalidRdataText);
+ checkFromText_InvalidText("foo 281474976710656 0 0 0 BADKEY 0");
// invalid time (negative)
- EXPECT_THROW(any::TSIG("foo -1 0 0 0 BADKEY 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo -1 0 0 0 BADKEY 0");
+ // invalid time (not a number)
+ checkFromText_InvalidText("foo TIME 0 0 0 BADKEY 0");
// fudge is too large
- EXPECT_THROW(any::TSIG("foo 0 65536 0 0 BADKEY 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 65536 0 0 BADKEY 0");
// invalid fudge (negative)
- EXPECT_THROW(any::TSIG("foo 0 -1 0 0 BADKEY 0"), InvalidRdataText);
+ checkFromText_LexerError("foo 0 -1 0 0 BADKEY 0");
+ // invalid fudge (not a number)
+ checkFromText_LexerError("foo 0 FUDGE 0 0 BADKEY 0");
// MAC size is too large
- EXPECT_THROW(any::TSIG("foo 0 0 65536 0 BADKEY 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 65536 0 BADKEY 0");
+ // invalide MAC size (negative)
+ checkFromText_LexerError("foo 0 0 -1 0 BADKEY 0");
+ // invalid MAC size (not a number)
+ checkFromText_LexerError("foo 0 0 MACSIZE 0 BADKEY 0");
// MAC size and MAC mismatch
- EXPECT_THROW(any::TSIG("foo 0 0 9 FAKE 0 BADKEY 0"), InvalidRdataText);
- EXPECT_THROW(any::TSIG("foo 0 0 0 FAKE 0 BADKEY 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 9 FAKE 0 BADKEY 0");
// MAC is bad base64
- EXPECT_THROW(any::TSIG("foo 0 0 3 FAK= 0 BADKEY 0"), isc::BadValue);
+ checkFromText_BadValue("foo 0 0 3 FAK= 0 BADKEY 0");
// Unknown error code
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 TEST 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 0 0 TEST 0");
// Numeric error code is too large
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 65536 0"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 0 0 65536 0");
+ // Numeric error code is negative
+ checkFromText_InvalidText("foo 0 0 0 0 -1 0");
// Other len is too large
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 NOERROR 65536 FAKE"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 0 0 NOERROR 65536 FAKE");
+ // Other len is negative
+ checkFromText_LexerError("foo 0 0 0 0 NOERROR -1 FAKE");
+ // invalid Other len
+ checkFromText_LexerError("foo 0 0 0 0 NOERROR LEN FAKE");
// Other len and data mismatch
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 NOERROR 9 FAKE"), InvalidRdataText);
- EXPECT_THROW(any::TSIG("foo 0 0 0 0 NOERROR 0 FAKE"), InvalidRdataText);
+ checkFromText_InvalidText("foo 0 0 0 0 NOERROR 9 FAKE");
}
void
@@ -221,12 +297,12 @@ TEST_F(Rdata_TSIG_Test, createFromParams) {
const uint8_t fake_data[] = { 0x14, 0x02, 0x84, 0x14, 0x02, 0x84,
0x14, 0x02, 0x84, 0x14, 0x02, 0x84 };
- EXPECT_EQ(0, any::TSIG((string(valid_text2))).compare(
+ EXPECT_EQ(0, any::TSIG(valid_text2).compare(
any::TSIG(Name("hmac-sha256"), 1286779327, 300, 12,
fake_data, 16020, 16, 0, NULL)));
const uint8_t fake_data2[] = { 0x14, 0x02, 0x84, 0x14, 0x02, 0x84 };
- EXPECT_EQ(0, any::TSIG((string(valid_text3))).compare(
+ EXPECT_EQ(0, any::TSIG(valid_text3).compare(
any::TSIG(Name("hmac-sha1"), 1286779327, 300, 12,
fake_data, 16020, 18, 6, fake_data2)));
@@ -247,24 +323,14 @@ TEST_F(Rdata_TSIG_Test, createFromParams) {
isc::InvalidParameter);
}
-TEST_F(Rdata_TSIG_Test, createFromLexer) {
- EXPECT_EQ(0, rdata_tsig.compare(
- *test::createRdataUsingLexer(RRType::TSIG(), RRClass::ANY(),
- valid_text1)));
-
- // Exceptions cause NULL to be returned.
- EXPECT_FALSE(test::createRdataUsingLexer(RRType::TSIG(), RRClass::ANY(),
- "foo 0 0 0 0 BADKEY 0 0"));
-}
-
TEST_F(Rdata_TSIG_Test, assignment) {
- any::TSIG copy((string(valid_text2)));
+ any::TSIG copy(valid_text2);
copy = rdata_tsig;
EXPECT_EQ(0, copy.compare(rdata_tsig));
// Check if the copied data is valid even after the original is deleted
any::TSIG* copy2 = new any::TSIG(rdata_tsig);
- any::TSIG copy3((string(valid_text2)));
+ any::TSIG copy3(valid_text2);
copy3 = *copy2;
delete copy2;
EXPECT_EQ(0, copy3.compare(rdata_tsig));
@@ -276,7 +342,7 @@ TEST_F(Rdata_TSIG_Test, assignment) {
template <typename Output>
void
-toWireCommonChecks(Output& output) {
+Rdata_TSIG_Test::toWireCommonChecks(Output& output) const {
vector<uint8_t> expect_data;
output.clear();
@@ -291,7 +357,7 @@ toWireCommonChecks(Output& output) {
expect_data.clear();
output.clear();
- any::TSIG(string(valid_text2)).toWire(output);
+ any::TSIG(valid_text2).toWire(output);
UnitTestUtil::readWireData("rdata_tsig_toWire2.wire", expect_data);
expect_data.erase(expect_data.begin(), expect_data.begin() + 2);
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
@@ -300,7 +366,7 @@ toWireCommonChecks(Output& output) {
expect_data.clear();
output.clear();
- any::TSIG(string(valid_text3)).toWire(output);
+ any::TSIG(valid_text3).toWire(output);
UnitTestUtil::readWireData("rdata_tsig_toWire3.wire", expect_data);
expect_data.erase(expect_data.begin(), expect_data.begin() + 2);
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData,
@@ -339,10 +405,10 @@ TEST_F(Rdata_TSIG_Test, toWireRenderer) {
}
TEST_F(Rdata_TSIG_Test, toText) {
- EXPECT_EQ(string(valid_text1), rdata_tsig.toText());
- EXPECT_EQ(string(valid_text2), any::TSIG(string(valid_text2)).toText());
- EXPECT_EQ(string(valid_text3), any::TSIG(string(valid_text3)).toText());
- EXPECT_EQ(string(valid_text5), any::TSIG(string(valid_text5)).toText());
+ EXPECT_EQ(valid_text1, rdata_tsig.toText());
+ EXPECT_EQ(valid_text2, any::TSIG(valid_text2).toText());
+ EXPECT_EQ(valid_text3, any::TSIG(valid_text3).toText());
+ EXPECT_EQ(valid_text5, any::TSIG(valid_text5).toText());
}
TEST_F(Rdata_TSIG_Test, compare) {
diff --git a/src/lib/dns/tests/rrclass_unittest.cc b/src/lib/dns/tests/rrclass_unittest.cc
index 17af873..b79aedf 100644
--- a/src/lib/dns/tests/rrclass_unittest.cc
+++ b/src/lib/dns/tests/rrclass_unittest.cc
@@ -20,10 +20,13 @@
#include <dns/tests/unittest_util.h>
+#include <boost/scoped_ptr.hpp>
+
using namespace std;
using namespace isc;
using namespace isc::dns;
using namespace isc::util;
+using boost::scoped_ptr;
namespace {
class RRClassTest : public ::testing::Test {
@@ -97,11 +100,12 @@ TEST_F(RRClassTest, toText) {
}
TEST_F(RRClassTest, createFromText) {
- const MaybeRRClass rrclass("IN");
- EXPECT_TRUE(rrclass);
- EXPECT_EQ("IN", rrclass->toText());
- EXPECT_TRUE(RRClass::createFromText("CH"));
- EXPECT_FALSE(RRClass::createFromText("ZZ"));
+ scoped_ptr<RRClass> chclass(RRClass::createFromText("CH"));
+ EXPECT_TRUE(chclass);
+ EXPECT_EQ("CH", chclass->toText());
+
+ scoped_ptr<RRClass> zzclass(RRClass::createFromText("ZZ"));
+ EXPECT_FALSE(zzclass);
}
TEST_F(RRClassTest, toWireBuffer) {
diff --git a/src/lib/dns/tests/rrparamregistry_unittest.cc b/src/lib/dns/tests/rrparamregistry_unittest.cc
index 08e0af1..b0d43a8 100644
--- a/src/lib/dns/tests/rrparamregistry_unittest.cc
+++ b/src/lib/dns/tests/rrparamregistry_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2010-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -108,13 +108,16 @@ TEST_F(RRParamRegistryTest, addError) {
class TestRdataFactory : public AbstractRdataFactory {
public:
- using AbstractRdataFactory::create;
virtual RdataPtr create(const string& rdata_str) const
{ return (RdataPtr(new in::A(rdata_str))); }
virtual RdataPtr create(InputBuffer& buffer, size_t rdata_len) const
{ return (RdataPtr(new in::A(buffer, rdata_len))); }
virtual RdataPtr create(const Rdata& source) const
{ return (RdataPtr(new in::A(dynamic_cast<const in::A&>(source)))); }
+ virtual RdataPtr create(MasterLexer& lexer, const Name* origin,
+ MasterLoader::Options options,
+ MasterLoaderCallbacks& callbacks) const
+ { return (RdataPtr(new in::A(lexer, origin, options, callbacks))); }
};
TEST_F(RRParamRegistryTest, addRemoveFactory) {
diff --git a/src/lib/dns/tests/rrset_unittest.cc b/src/lib/dns/tests/rrset_unittest.cc
index a605caf..da3eb52 100644
--- a/src/lib/dns/tests/rrset_unittest.cc
+++ b/src/lib/dns/tests/rrset_unittest.cc
@@ -114,11 +114,6 @@ TEST_F(RRsetTest, setTTL) {
EXPECT_EQ(RRTTL(0), rrset_a.getTTL());
}
-TEST_F(RRsetTest, setName) {
- rrset_a.setName(test_nsname);
- EXPECT_EQ(test_nsname, rrset_a.getName());
-}
-
TEST_F(RRsetTest, isSameKind) {
RRset rrset_w(test_name, RRClass::IN(), RRType::A(), RRTTL(3600));
RRset rrset_x(test_name, RRClass::IN(), RRType::A(), RRTTL(3600));
@@ -216,11 +211,13 @@ TEST_F(RRsetTest, toWireBuffer) {
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
buffer.getLength(), &wiredata[0], wiredata.size());
- // toWire() cannot be performed for an empty RRset.
+ // toWire() cannot be performed for an empty RRset except when
+ // class=ANY or class=NONE.
buffer.clear();
EXPECT_THROW(rrset_a_empty.toWire(buffer), EmptyRRset);
- // Unless it is type ANY or None
+ // When class=ANY or class=NONE, toWire() can also be performed for
+ // an empty RRset.
buffer.clear();
rrset_any_a_empty.toWire(buffer);
wiredata.clear();
@@ -245,25 +242,26 @@ TEST_F(RRsetTest, toWireRenderer) {
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, renderer.getData(),
renderer.getLength(), &wiredata[0], wiredata.size());
- // toWire() cannot be performed for an empty RRset.
- buffer.clear();
- EXPECT_THROW(rrset_a_empty.toWire(buffer), EmptyRRset);
+ // toWire() cannot be performed for an empty RRset except when
+ // class=ANY or class=NONE.
+ renderer.clear();
+ EXPECT_THROW(rrset_a_empty.toWire(renderer), EmptyRRset);
- // Unless it is type ANY or None
- // toWire() can also be performed for an empty RRset.
- buffer.clear();
- rrset_any_a_empty.toWire(buffer);
+ // When class=ANY or class=NONE, toWire() can also be performed for
+ // an empty RRset.
+ renderer.clear();
+ rrset_any_a_empty.toWire(renderer);
wiredata.clear();
UnitTestUtil::readWireData("rrset_toWire3", wiredata);
- EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
- buffer.getLength(), &wiredata[0], wiredata.size());
+ EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, renderer.getData(),
+ renderer.getLength(), &wiredata[0], wiredata.size());
- buffer.clear();
- rrset_none_a_empty.toWire(buffer);
+ renderer.clear();
+ rrset_none_a_empty.toWire(renderer);
wiredata.clear();
UnitTestUtil::readWireData("rrset_toWire4", wiredata);
- EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, buffer.getData(),
- buffer.getLength(), &wiredata[0], wiredata.size());
+ EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, renderer.getData(),
+ renderer.getLength(), &wiredata[0], wiredata.size());
}
// test operator<<. We simply confirm it appends the result of toText().
diff --git a/src/lib/dns/tests/rrttl_unittest.cc b/src/lib/dns/tests/rrttl_unittest.cc
index 8897102..c849c44 100644
--- a/src/lib/dns/tests/rrttl_unittest.cc
+++ b/src/lib/dns/tests/rrttl_unittest.cc
@@ -26,6 +26,7 @@ using namespace std;
using namespace isc;
using namespace isc::dns;
using namespace isc::util;
+using boost::scoped_ptr;
namespace {
class RRTTLTest : public ::testing::Test {
@@ -75,6 +76,12 @@ TEST_F(RRTTLTest, getValue) {
EXPECT_EQ(0xffffffff, ttl_max.getValue());
}
+TEST_F(RRTTLTest, copyConstruct) {
+ const RRTTL ttl1(3600);
+ const RRTTL ttl2(ttl1);
+ EXPECT_EQ(ttl1.getValue(), ttl2.getValue());
+}
+
TEST_F(RRTTLTest, fromText) {
// Border cases
EXPECT_EQ(0, RRTTL("0").getValue());
@@ -88,14 +95,14 @@ TEST_F(RRTTLTest, fromText) {
}
TEST_F(RRTTLTest, createFromText) {
- // It returns an actual RRTT iff the given text is recognized as a
+ // It returns an actual RRTTL iff the given text is recognized as a
// valid RR TTL.
- MaybeRRTTL maybe_ttl = RRTTL::createFromText("3600");
- EXPECT_TRUE(maybe_ttl);
- EXPECT_EQ(RRTTL(3600), *maybe_ttl);
+ scoped_ptr<RRTTL> good_ttl(RRTTL::createFromText("3600"));
+ EXPECT_TRUE(good_ttl);
+ EXPECT_EQ(RRTTL(3600), *good_ttl);
- maybe_ttl = RRTTL::createFromText("bad");
- EXPECT_FALSE(maybe_ttl);
+ scoped_ptr<RRTTL> bad_ttl(RRTTL::createFromText("bad"));
+ EXPECT_FALSE(bad_ttl);
}
void
diff --git a/src/lib/dns/tests/tsigkey_unittest.cc b/src/lib/dns/tests/tsigkey_unittest.cc
index c1367be..eaf4040 100644
--- a/src/lib/dns/tests/tsigkey_unittest.cc
+++ b/src/lib/dns/tests/tsigkey_unittest.cc
@@ -38,6 +38,7 @@ protected:
TEST_F(TSIGKeyTest, algorithmNames) {
EXPECT_EQ(Name("hmac-md5.sig-alg.reg.int"), TSIGKey::HMACMD5_NAME());
+ EXPECT_EQ(Name("hmac-md5"), TSIGKey::HMACMD5_SHORT_NAME());
EXPECT_EQ(Name("hmac-sha1"), TSIGKey::HMACSHA1_NAME());
EXPECT_EQ(Name("hmac-sha256"), TSIGKey::HMACSHA256_NAME());
EXPECT_EQ(Name("hmac-sha224"), TSIGKey::HMACSHA224_NAME());
@@ -47,6 +48,9 @@ TEST_F(TSIGKeyTest, algorithmNames) {
// Also check conversion to cryptolink definitions
EXPECT_EQ(isc::cryptolink::MD5, TSIGKey(key_name, TSIGKey::HMACMD5_NAME(),
NULL, 0).getAlgorithm());
+ EXPECT_EQ(isc::cryptolink::MD5,
+ TSIGKey(key_name, TSIGKey::HMACMD5_SHORT_NAME(),
+ NULL, 0).getAlgorithm());
EXPECT_EQ(isc::cryptolink::SHA1, TSIGKey(key_name, TSIGKey::HMACSHA1_NAME(),
NULL, 0).getAlgorithm());
EXPECT_EQ(isc::cryptolink::SHA256, TSIGKey(key_name,
@@ -71,6 +75,13 @@ TEST_F(TSIGKeyTest, construct) {
EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, secret.c_str(),
secret.size(), key.getSecret(), key.getSecretLength());
+ TSIGKey key_short_md5(key_name, TSIGKey::HMACMD5_SHORT_NAME(),
+ secret.c_str(), secret.size());
+ EXPECT_EQ(key_name, key.getKeyName());
+ EXPECT_EQ(Name("hmac-md5.sig-alg.reg.int"), key.getAlgorithmName());
+ EXPECT_PRED_FORMAT4(UnitTestUtil::matchWireData, secret.c_str(),
+ secret.size(), key.getSecret(), key.getSecretLength());
+
// "unknown" algorithm is only accepted with empty secret.
EXPECT_THROW(TSIGKey(key_name, Name("unknown-alg"),
secret.c_str(), secret.size()),
diff --git a/src/lib/dns/tsigkey.cc b/src/lib/dns/tsigkey.cc
index e55cce3..24a6f57 100644
--- a/src/lib/dns/tsigkey.cc
+++ b/src/lib/dns/tsigkey.cc
@@ -36,6 +36,9 @@ namespace {
if (name == TSIGKey::HMACMD5_NAME()) {
return (isc::cryptolink::MD5);
}
+ if (name == TSIGKey::HMACMD5_SHORT_NAME()) {
+ return (isc::cryptolink::MD5);
+ }
if (name == TSIGKey::HMACSHA1_NAME()) {
return (isc::cryptolink::SHA1);
}
@@ -68,6 +71,9 @@ TSIGKey::TSIGKeyImpl {
{
// Convert the key and algorithm names to the canonical form.
key_name_.downcase();
+ if (algorithm == isc::cryptolink::MD5) {
+ algorithm_name_ = TSIGKey::HMACMD5_NAME();
+ }
algorithm_name_.downcase();
}
Name key_name_;
@@ -148,7 +154,7 @@ TSIGKey::TSIGKey(const TSIGKey& source) : impl_(new TSIGKeyImpl(*source.impl_))
TSIGKey&
TSIGKey::operator=(const TSIGKey& source) {
- if (impl_ == source.impl_) {
+ if (this == &source) {
return (*this);
}
@@ -206,6 +212,12 @@ Name& TSIGKey::HMACMD5_NAME() {
}
const
+Name& TSIGKey::HMACMD5_SHORT_NAME() {
+ static Name alg_name("hmac-md5");
+ return (alg_name);
+}
+
+const
Name& TSIGKey::HMACSHA1_NAME() {
static Name alg_name("hmac-sha1");
return (alg_name);
diff --git a/src/lib/dns/tsigkey.h b/src/lib/dns/tsigkey.h
index b10660c..e623be9 100644
--- a/src/lib/dns/tsigkey.h
+++ b/src/lib/dns/tsigkey.h
@@ -203,6 +203,7 @@ public:
/// We'll add others as we see the need for them.
//@{
static const Name& HMACMD5_NAME(); ///< HMAC-MD5 (RFC2845)
+ static const Name& HMACMD5_SHORT_NAME();
static const Name& HMACSHA1_NAME(); ///< HMAC-SHA1 (RFC4635)
static const Name& HMACSHA256_NAME(); ///< HMAC-SHA256 (RFC4635)
static const Name& HMACSHA224_NAME(); ///< HMAC-SHA256 (RFC4635)
diff --git a/src/lib/hooks/.gitignore b/src/lib/hooks/.gitignore
new file mode 100644
index 0000000..89adbe1
--- /dev/null
+++ b/src/lib/hooks/.gitignore
@@ -0,0 +1,3 @@
+/hooks_messages.cc
+/hooks_messages.h
+/s-messages
diff --git a/src/lib/hooks/Makefile.am b/src/lib/hooks/Makefile.am
new file mode 100644
index 0000000..a8d7af1
--- /dev/null
+++ b/src/lib/hooks/Makefile.am
@@ -0,0 +1,67 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+
+# Define rule to build logging source files from message file
+hooks_messages.h hooks_messages.cc: s-messages
+
+s-messages: hooks_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/hooks/hooks_messages.mes
+ touch $@
+
+# Tell automake that the message files are built as part of the build process
+# (so that they are built before the main library is built).
+BUILT_SOURCES = hooks_messages.h hooks_messages.cc
+
+# Ensure that the message file is included in the distribution
+EXTRA_DIST = hooks_messages.mes
+
+# Get rid of generated message files on a clean
+CLEANFILES = *.gcno *.gcda hooks_messages.h hooks_messages.cc s-messages
+
+lib_LTLIBRARIES = libb10-hooks.la
+libb10_hooks_la_SOURCES =
+libb10_hooks_la_SOURCES += callout_handle.cc callout_handle.h
+libb10_hooks_la_SOURCES += callout_manager.cc callout_manager.h
+libb10_hooks_la_SOURCES += hooks.h hooks.cc
+libb10_hooks_la_SOURCES += hooks_log.cc hooks_log.h
+libb10_hooks_la_SOURCES += hooks_manager.cc hooks_manager.h
+libb10_hooks_la_SOURCES += library_handle.cc library_handle.h
+libb10_hooks_la_SOURCES += library_manager.cc library_manager.h
+libb10_hooks_la_SOURCES += library_manager_collection.cc library_manager_collection.h
+libb10_hooks_la_SOURCES += pointer_converter.h
+libb10_hooks_la_SOURCES += server_hooks.cc server_hooks.h
+
+nodist_libb10_hooks_la_SOURCES = hooks_messages.cc hooks_messages.h
+
+libb10_hooks_la_CXXFLAGS = $(AM_CXXFLAGS)
+libb10_hooks_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libb10_hooks_la_LDFLAGS = $(AM_LDFLAGS)
+libb10_hooks_la_LIBADD =
+libb10_hooks_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
+libb10_hooks_la_LIBADD += $(top_builddir)/src/lib/util/libb10-util.la
+libb10_hooks_la_LIBADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+
+# Specify the headers for copying into the installation directory tree. User-
+# written libraries only need the definitions from the headers for the
+# CalloutHandle and LibraryHandle objects.
+libb10_hooks_includedir = $(pkgincludedir)/hooks
+libb10_hooks_include_HEADERS = \
+ callout_handle.h \
+ library_handle.h \
+ hooks.h
+
+if USE_CLANGPP
+# Disable unused parameter warning caused by some of the
+# Boost headers when compiling with clang.
+libb10_hooks_la_CXXFLAGS += -Wno-unused-parameter
+endif
diff --git a/src/lib/hooks/callout_handle.cc b/src/lib/hooks/callout_handle.cc
new file mode 100644
index 0000000..cbd992c
--- /dev/null
+++ b/src/lib/hooks/callout_handle.cc
@@ -0,0 +1,163 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/server_hooks.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace std;
+
+namespace isc {
+namespace hooks {
+
+// Constructor.
+CalloutHandle::CalloutHandle(const boost::shared_ptr<CalloutManager>& manager,
+ const boost::shared_ptr<LibraryManagerCollection>& lmcoll)
+ : lm_collection_(lmcoll), arguments_(), context_collection_(),
+ manager_(manager), server_hooks_(ServerHooks::getServerHooks()),
+ skip_(false) {
+
+ // Call the "context_create" hook. We should be OK doing this - although
+ // the constructor has not finished running, all the member variables
+ // have been created.
+ manager_->callCallouts(ServerHooks::CONTEXT_CREATE, *this);
+}
+
+// Destructor
+CalloutHandle::~CalloutHandle() {
+
+ // Call the "context_destroy" hook. We should be OK doing this - although
+ // the destructor is being called, all the member variables are still in
+ // existence.
+ manager_->callCallouts(ServerHooks::CONTEXT_DESTROY, *this);
+
+ // Explicitly clear the argument and context objects. This should free up
+ // all memory that could have been allocated by libraries that were loaded.
+ arguments_.clear();
+ context_collection_.clear();
+
+ // Normal destruction of the remaining variables will include the
+ // destruction of lm_collection_, an action that decrements the reference
+ // count on the library manager collection (which holds the libraries that
+ // could have allocated memory in the argument and context members.) When
+ // that goes to zero, the libraries will be unloaded: at that point nothing
+ // in the hooks framework will be pointing to memory in the libraries'
+ // address space.
+ //
+ // It is possible that some other data structure in the server (the program
+ // using the hooks library) still references the address space and attempts
+ // to access it causing a segmentation fault. That issue is outside the
+ // scope of this framework and is not addressed by it.
+}
+
+// Return the name of all argument items.
+
+vector<string>
+CalloutHandle::getArgumentNames() const {
+
+ vector<string> names;
+ for (ElementCollection::const_iterator i = arguments_.begin();
+ i != arguments_.end(); ++i) {
+ names.push_back(i->first);
+ }
+
+ return (names);
+}
+
+// Return the library handle allowing the callout to access the CalloutManager
+// registration/deregistration functions.
+
+LibraryHandle&
+CalloutHandle::getLibraryHandle() const {
+ return (manager_->getLibraryHandle());
+}
+
+// Return the context for the currently pointed-to library. This version is
+// used by the "setContext()" method and creates a context for the current
+// library if it does not exist.
+
+CalloutHandle::ElementCollection&
+CalloutHandle::getContextForLibrary() {
+ int libindex = manager_->getLibraryIndex();
+
+ // Access a reference to the element collection for the given index,
+ // creating a new element collection if necessary, and return it.
+ return (context_collection_[libindex]);
+}
+
+// The "const" version of the above, used by the "getContext()" method. If
+// the context for the current library doesn't exist, throw an exception.
+
+const CalloutHandle::ElementCollection&
+CalloutHandle::getContextForLibrary() const {
+ int libindex = manager_->getLibraryIndex();
+
+ ContextCollection::const_iterator libcontext =
+ context_collection_.find(libindex);
+ if (libcontext == context_collection_.end()) {
+ isc_throw(NoSuchCalloutContext, "unable to find callout context "
+ "associated with the current library index (" << libindex <<
+ ")");
+ }
+
+ // Return a reference to the context's element collection.
+ return (libcontext->second);
+}
+
+// Return the name of all items in the context associated with the current]
+// library.
+
+vector<string>
+CalloutHandle::getContextNames() const {
+
+ vector<string> names;
+
+ const ElementCollection& elements = getContextForLibrary();
+ for (ElementCollection::const_iterator i = elements.begin();
+ i != elements.end(); ++i) {
+ names.push_back(i->first);
+ }
+
+ return (names);
+}
+
+// Return name of current hook (the hook to which the current callout is
+// attached) or the empty string if not called within the context of a
+// callout.
+
+string
+CalloutHandle::getHookName() const {
+ // Get the current hook index.
+ int index = manager_->getHookIndex();
+
+ // ... and look up the hook.
+ string hook = "";
+ try {
+ hook = server_hooks_.getName(index);
+ } catch (const NoSuchHook&) {
+ // Hook index is invalid, so this methods probably called from outside
+ // a callout being executed via a call to CalloutManager::callCallouts.
+ // In this case, the empty string is returned.
+ }
+
+ return (hook);
+}
+
+} // namespace util
+} // namespace isc
diff --git a/src/lib/hooks/callout_handle.h b/src/lib/hooks/callout_handle.h
new file mode 100644
index 0000000..91d7f23
--- /dev/null
+++ b/src/lib/hooks/callout_handle.h
@@ -0,0 +1,390 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef CALLOUT_HANDLE_H
+#define CALLOUT_HANDLE_H
+
+#include <exceptions/exceptions.h>
+#include <hooks/library_handle.h>
+
+#include <boost/any.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace hooks {
+
+class ServerHooks;
+
+/// @brief No such argument
+///
+/// Thrown if an attempt is made access an argument that does not exist.
+
+class NoSuchArgument : public Exception {
+public:
+ NoSuchArgument(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief No such callout context item
+///
+/// Thrown if an attempt is made to get an item of data from this callout's
+/// context and either the context or an item in the context with that name
+/// does not exist.
+
+class NoSuchCalloutContext : public Exception {
+public:
+ NoSuchCalloutContext(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+// Forward declaration of the library handle and related collection classes.
+
+class CalloutManager;
+class LibraryHandle;
+class LibraryManagerCollection;
+
+/// @brief Per-packet callout handle
+///
+/// An object of this class is associated with every packet (or request)
+/// processed by the server. It forms the principle means of passing data
+/// between the server and the user-library callouts.
+///
+/// The class allows access to the following information:
+///
+/// - Arguments. When the callouts associated with a hook are called, they
+/// are passed information by the server (and can return information to it)
+/// through name/value pairs. Each of these pairs is an argument and the
+/// information is accessed through the {get,set}Argument() methods.
+///
+/// - Per-packet context. Each packet has a context associated with it, this
+/// context being on a per-library basis. In other words, As a packet passes
+/// through the callouts associated with a given library, the callouts can
+/// associate and retrieve information with the packet. The per-library
+/// nature of the context means that the callouts within a given library can
+/// pass packet-specific information between one another, but they cannot pass
+/// information to callous within another library. Typically such context
+/// is created in the "context_create" callout and destroyed in the
+/// "context_destroy" callout. The information is accessed through the
+/// {get,set}Context() methods.
+///
+/// - Per-library handle (LibraryHandle). The library handle allows the
+/// callout to dynamically register and deregister callouts. In the latter
+/// case, only functions registered by functions in the same library as the
+/// callout doing the deregistration can be removed: callouts registered by
+/// other libraries cannot be modified.
+
+class CalloutHandle {
+public:
+
+ /// Typedef to allow abbreviation of iterator specification in methods.
+ /// The std::string is the argument name and the "boost::any" is the
+ /// corresponding value associated with it.
+ typedef std::map<std::string, boost::any> ElementCollection;
+
+ /// Typedef to allow abbreviations in specifications when accessing
+ /// context. The ElementCollection is the name/value collection for
+ /// a particular context. The "int" corresponds to the index of an
+ /// associated library - there is a 1:1 correspondence between libraries
+ /// and a name.value collection.
+ ///
+ /// The collection of contexts is stored in a map, as not every library
+ /// will require creation of a context associated with each packet. In
+ /// addition, the structure is more flexible in that the size does not
+ /// need to be set when the CalloutHandle is constructed.
+ typedef std::map<int, ElementCollection> ContextCollection;
+
+ /// @brief Constructor
+ ///
+ /// Creates the object and calls the callouts on the "context_create"
+ /// hook.
+ ///
+ /// Of the two arguments passed, only the pointer to the callout manager is
+ /// actively used. The second argument, the pointer to the library manager
+ /// collection, is used for lifetime control: after use, the callout handle
+ /// may contain pointers to memory allocated by the loaded libraries. The
+ /// used of a shared pointer to the collection of library managers means
+ /// that the libraries that could have allocated memory in a callout handle
+ /// will not be unloaded until all such handles have been destroyed. This
+ /// issue is discussed in more detail in the documentation for
+ /// isc::hooks::LibraryManager.
+ ///
+ /// @param manager Pointer to the callout manager object.
+ /// @param lmcoll Pointer to the library manager collection. This has a
+ /// null default for testing purposes.
+ CalloutHandle(const boost::shared_ptr<CalloutManager>& manager,
+ const boost::shared_ptr<LibraryManagerCollection>& lmcoll =
+ boost::shared_ptr<LibraryManagerCollection>());
+
+ /// @brief Destructor
+ ///
+ /// Calls the context_destroy callback to release any per-packet context.
+ /// It also clears stored data to avoid problems during member destruction.
+ ~CalloutHandle();
+
+ /// @brief Set argument
+ ///
+ /// Sets the value of an argument. The argument is created if it does not
+ /// already exist.
+ ///
+ /// @param name Name of the argument.
+ /// @param value Value to set. That can be of any data type.
+ template <typename T>
+ void setArgument(const std::string& name, T value) {
+ arguments_[name] = value;
+ }
+
+ /// @brief Get argument
+ ///
+ /// Gets the value of an argument.
+ ///
+ /// @param name Name of the element in the argument list to get.
+ /// @param value [out] Value to set. The type of "value" is important:
+ /// it must match the type of the value set.
+ ///
+ /// @throw NoSuchArgument No argument with the given name is present.
+ /// @throw boost::bad_any_cast An argument with the given name is present,
+ /// but the data type of the value is not the same as the type of
+ /// the variable provided to receive the value.
+ template <typename T>
+ void getArgument(const std::string& name, T& value) const {
+ ElementCollection::const_iterator element_ptr = arguments_.find(name);
+ if (element_ptr == arguments_.end()) {
+ isc_throw(NoSuchArgument, "unable to find argument with name " <<
+ name);
+ }
+
+ value = boost::any_cast<T>(element_ptr->second);
+ }
+
+ /// @brief Get argument names
+ ///
+ /// Returns a vector holding the names of arguments in the argument
+ /// vector.
+ ///
+ /// @return Vector of strings reflecting argument names.
+ std::vector<std::string> getArgumentNames() const;
+
+ /// @brief Delete argument
+ ///
+ /// Deletes an argument of the given name. If an argument of that name
+ /// does not exist, the method is a no-op.
+ ///
+ /// N.B. If the element is a raw pointer, the pointed-to data is NOT deleted
+ /// by this method.
+ ///
+ /// @param name Name of the element in the argument list to set.
+ void deleteArgument(const std::string& name) {
+ static_cast<void>(arguments_.erase(name));
+ }
+
+ /// @brief Delete all arguments
+ ///
+ /// Deletes all arguments associated with this context.
+ ///
+ /// N.B. If any elements are raw pointers, the pointed-to data is NOT
+ /// deleted by this method.
+ void deleteAllArguments() {
+ arguments_.clear();
+ }
+
+ /// @brief Set skip flag
+ ///
+ /// Sets the "skip" variable in the callout handle. This variable is
+ /// interrogated by the server to see if the remaining callouts associated
+ /// with the current hook should be bypassed.
+ ///
+ /// @param skip New value of the "skip" flag.
+ void setSkip(bool skip) {
+ skip_ = skip;
+ }
+
+ /// @brief Get skip flag
+ ///
+ /// Gets the current value of the "skip" flag.
+ ///
+ /// @return Current value of the skip flag.
+ bool getSkip() const {
+ return (skip_);
+ }
+
+ /// @brief Access current library handle
+ ///
+ /// Returns a reference to the current library handle. This function is
+ /// only available when called by a callout (which in turn is called
+ /// through the "callCallouts" method), as it is only then that the current
+ /// library index is valid. A callout uses the library handle to
+ /// dynamically register or deregister callouts.
+ ///
+ /// @return Reference to the library handle.
+ ///
+ /// @throw InvalidIndex thrown if this method is called when the current
+ /// library index is invalid (typically if it is called outside of
+ /// the active callout).
+ LibraryHandle& getLibraryHandle() const;
+
+ /// @brief Set context
+ ///
+ /// Sets an element in the context associated with the current library. If
+ /// an element of the name is already present, it is replaced.
+ ///
+ /// @param name Name of the element in the context to set.
+ /// @param value Value to set.
+ template <typename T>
+ void setContext(const std::string& name, T value) {
+ getContextForLibrary()[name] = value;
+ }
+
+ /// @brief Get context
+ ///
+ /// Gets an element from the context associated with the current library.
+ ///
+ /// @param name Name of the element in the context to get.
+ /// @param value [out] Value to set. The type of "value" is important:
+ /// it must match the type of the value set.
+ ///
+ /// @throw NoSuchCalloutContext Thrown if no context element with the name
+ /// "name" is present.
+ /// @throw boost::bad_any_cast Thrown if the context element is present
+ /// but the type of the data is not the same as the type of the
+ /// variable provided to receive its value.
+ template <typename T>
+ void getContext(const std::string& name, T& value) const {
+ const ElementCollection& lib_context = getContextForLibrary();
+
+ ElementCollection::const_iterator element_ptr = lib_context.find(name);
+ if (element_ptr == lib_context.end()) {
+ isc_throw(NoSuchCalloutContext, "unable to find callout context "
+ "item " << name << " in the context associated with "
+ "current library");
+ }
+
+ value = boost::any_cast<T>(element_ptr->second);
+ }
+
+ /// @brief Get context names
+ ///
+ /// Returns a vector holding the names of items in the context associated
+ /// with the current library.
+ ///
+ /// @return Vector of strings reflecting the names of items in the callout
+ /// context associated with the current library.
+ std::vector<std::string> getContextNames() const;
+
+ /// @brief Delete context element
+ ///
+ /// Deletes an item of the given name from the context associated with the
+ /// current library. If an item of that name does not exist, the method is
+ /// a no-op.
+ ///
+ /// N.B. If the element is a raw pointer, the pointed-to data is NOT deleted
+ /// by this.
+ ///
+ /// @param name Name of the context item to delete.
+ void deleteContext(const std::string& name) {
+ static_cast<void>(getContextForLibrary().erase(name));
+ }
+
+ /// @brief Delete all context items
+ ///
+ /// Deletes all items from the context associated with the current library.
+ ///
+ /// N.B. If any elements are raw pointers, the pointed-to data is NOT
+ /// deleted by this.
+ void deleteAllContext() {
+ getContextForLibrary().clear();
+ }
+
+ /// @brief Get hook name
+ ///
+ /// Get the name of the hook to which the current callout is attached.
+ /// This can be the null string if the CalloutHandle is being accessed
+ /// outside of the CalloutManager's "callCallouts" method.
+ ///
+ /// @return Name of the current hook or the empty string if none.
+ std::string getHookName() const;
+
+private:
+ /// @brief Check index
+ ///
+ /// Gets the current library index, throwing an exception if it is not set
+ /// or is invalid for the current library collection.
+ ///
+ /// @return Current library index, valid for this library collection.
+ ///
+ /// @throw InvalidIndex current library index is not valid for the library
+ /// handle collection.
+ int getLibraryIndex() const;
+
+ /// @brief Return reference to context for current library
+ ///
+ /// Called by all context-setting functions, this returns a reference to
+ /// the callout context for the current library, creating a context if it
+ /// does not exist.
+ ///
+ /// @return Reference to the collection of name/value pairs associated
+ /// with the current library.
+ ///
+ /// @throw InvalidIndex current library index is not valid for the library
+ /// handle collection.
+ ElementCollection& getContextForLibrary();
+
+ /// @brief Return reference to context for current library (const version)
+ ///
+ /// Called by all context-accessing functions, this a reference to the
+ /// callout context for the current library. An exception is thrown if
+ /// it does not exist.
+ ///
+ /// @return Reference to the collection of name/value pairs associated
+ /// with the current library.
+ ///
+ /// @throw NoSuchCalloutContext Thrown if there is no ElementCollection
+ /// associated with the current library.
+ const ElementCollection& getContextForLibrary() const;
+
+ // Member variables
+
+ /// Pointer to the collection of libraries for which this handle has been
+ /// created.
+ boost::shared_ptr<LibraryManagerCollection> lm_collection_;
+
+ /// Collection of arguments passed to the callouts
+ ElementCollection arguments_;
+
+ /// Context collection - there is one entry per library context.
+ ContextCollection context_collection_;
+
+ /// Callout manager.
+ boost::shared_ptr<CalloutManager> manager_;
+
+ /// Reference to the singleton ServerHooks object. See the
+ /// @ref hooksmgMaintenanceGuide for information as to why the class holds
+ /// a reference instead of accessing the singleton within the code.
+ ServerHooks& server_hooks_;
+
+ /// "Skip" flag, indicating if the caller should bypass remaining callouts.
+ bool skip_;
+};
+
+/// A shared pointer to a CalloutHandle object.
+typedef boost::shared_ptr<CalloutHandle> CalloutHandlePtr;
+
+} // namespace hooks
+} // namespace isc
+
+
+#endif // CALLOUT_HANDLE_H
diff --git a/src/lib/hooks/callout_manager.cc b/src/lib/hooks/callout_manager.cc
new file mode 100644
index 0000000..9dd5c60
--- /dev/null
+++ b/src/lib/hooks/callout_manager.cc
@@ -0,0 +1,259 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/hooks_log.h>
+#include <hooks/pointer_converter.h>
+
+#include <boost/static_assert.hpp>
+
+#include <algorithm>
+#include <climits>
+#include <functional>
+#include <utility>
+
+using namespace std;
+
+namespace isc {
+namespace hooks {
+
+// Constructor
+CalloutManager::CalloutManager(int num_libraries)
+ : server_hooks_(ServerHooks::getServerHooks()),
+ current_hook_(-1), current_library_(-1),
+ hook_vector_(ServerHooks::getServerHooks().getCount()),
+ library_handle_(this), pre_library_handle_(this, 0),
+ post_library_handle_(this, INT_MAX), num_libraries_(num_libraries)
+{
+ if (num_libraries < 0) {
+ isc_throw(isc::BadValue, "number of libraries passed to the "
+ "CalloutManager must be >= 0");
+ }
+}
+
+// Check that the index of a library is valid. It can range from 1 - n
+// (n is the number of libraries), 0 (pre-user library callouts), or INT_MAX
+// (post-user library callouts). It can also be -1 to indicate an invalid
+// value.
+
+void
+CalloutManager::checkLibraryIndex(int library_index) const {
+ if (((library_index >= -1) && (library_index <= num_libraries_)) ||
+ (library_index == INT_MAX)) {
+ return;
+ }
+
+ isc_throw(NoSuchLibrary, "library index " << library_index <<
+ " is not valid for the number of loaded libraries (" <<
+ num_libraries_ << ")");
+}
+
+// Register a callout for the current library.
+
+void
+CalloutManager::registerCallout(const std::string& name, CalloutPtr callout) {
+ // Note the registration.
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_CALLOUT_REGISTRATION)
+ .arg(current_library_).arg(name);
+
+ // Sanity check that the current library index is set to a valid value.
+ checkLibraryIndex(current_library_);
+
+ // Get the index associated with this hook (validating the name in the
+ // process).
+ int hook_index = server_hooks_.getIndex(name);
+
+ // Iterate through the callout vector for the hook from start to end,
+ // looking for the first entry where the library index is greater than
+ // the present index.
+ for (CalloutVector::iterator i = hook_vector_[hook_index].begin();
+ i != hook_vector_[hook_index].end(); ++i) {
+ if (i->first > current_library_) {
+ // Found an element whose library index number is greater than the
+ // current index, so insert the new element ahead of this one.
+ hook_vector_[hook_index].insert(i, make_pair(current_library_,
+ callout));
+ return;
+ }
+ }
+
+ // Reached the end of the vector, so there is no element in the (possibly
+ // empty) set of callouts with a library index greater than the current
+ // library index. Inset the callout at the end of the list.
+ hook_vector_[hook_index].push_back(make_pair(current_library_, callout));
+}
+
+// Check if callouts are present for a given hook index.
+
+bool
+CalloutManager::calloutsPresent(int hook_index) const {
+ // Validate the hook index.
+ if ((hook_index < 0) || (hook_index >= hook_vector_.size())) {
+ isc_throw(NoSuchHook, "hook index " << hook_index <<
+ " is not valid for the list of registered hooks");
+ }
+
+ // Valid, so are there any callouts associated with that hook?
+ return (!hook_vector_[hook_index].empty());
+}
+
+// Call all the callouts for a given hook.
+
+void
+CalloutManager::callCallouts(int hook_index, CalloutHandle& callout_handle) {
+
+ // Clear the "skip" flag so we don't carry state from a previous call.
+ // This is done regardless of whether callouts are present to avoid passing
+ // any state from the previous call of callCallouts().
+ callout_handle.setSkip(false);
+
+ // Only initialize and iterate if there are callouts present. This check
+ // also catches the case of an invalid index.
+ if (calloutsPresent(hook_index)) {
+
+ // Set the current hook index. This is used should a callout wish to
+ // determine to what hook it is attached.
+ current_hook_ = hook_index;
+
+ // Duplicate the callout vector for this hook and work through that.
+ // This step is needed because we allow dynamic registration and
+ // deregistration of callouts. If a callout attached to a hook modified
+ // the list of callouts on that hook, the underlying CalloutVector would
+ // change and potentially affect the iteration through that vector.
+ CalloutVector callouts(hook_vector_[hook_index]);
+
+ // Call all the callouts.
+ for (CalloutVector::const_iterator i = callouts.begin();
+ i != callouts.end(); ++i) {
+ // In case the callout tries to register or deregister a callout,
+ // set the current library index to the index associated with the
+ // library that registered the callout being called.
+ current_library_ = i->first;
+
+ // Call the callout
+ try {
+ int status = (*i->second)(callout_handle);
+ if (status == 0) {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_EXTENDED_CALLS,
+ HOOKS_CALLOUT_CALLED).arg(current_library_)
+ .arg(server_hooks_.getName(current_hook_))
+ .arg(PointerConverter(i->second).dlsymPtr());
+ } else {
+ LOG_ERROR(hooks_logger, HOOKS_CALLOUT_ERROR)
+ .arg(current_library_)
+ .arg(server_hooks_.getName(current_hook_))
+ .arg(PointerConverter(i->second).dlsymPtr());
+ }
+ } catch (const std::exception& e) {
+ // Any exception, not just ones based on isc::Exception
+ LOG_ERROR(hooks_logger, HOOKS_CALLOUT_EXCEPTION)
+ .arg(current_library_)
+ .arg(server_hooks_.getName(current_hook_))
+ .arg(PointerConverter(i->second).dlsymPtr())
+ .arg(e.what());
+ }
+
+ }
+
+ // Reset the current hook and library indexs to an invalid value to
+ // catch any programming errors.
+ current_hook_ = -1;
+ current_library_ = -1;
+ }
+}
+
+// Deregister a callout registered by the current library on a particular hook.
+
+bool
+CalloutManager::deregisterCallout(const std::string& name, CalloutPtr callout) {
+ // Sanity check that the current library index is set to a valid value.
+ checkLibraryIndex(current_library_);
+
+ // Get the index associated with this hook (validating the name in the
+ // process).
+ int hook_index = server_hooks_.getIndex(name);
+
+ /// Construct a CalloutEntry matching the current library and the callout
+ /// we want to remove.
+ CalloutEntry target(current_library_, callout);
+
+ /// To decide if any entries were removed, we'll record the initial size
+ /// of the callout vector for the hook, and compare it with the size after
+ /// the removal.
+ size_t initial_size = hook_vector_[hook_index].size();
+
+ // The next bit is standard STL (see "Item 33" in "Effective STL" by
+ // Scott Meyers).
+ //
+ // remove_if reorders the hook vector so that all items not matching
+ // the predicate are at the start of the vector and returns a pointer
+ // to the next element. (In this case, the predicate is that the item
+ // is equal to the value of the passed callout.) The erase() call
+ // removes everything from that element to the end of the vector, i.e.
+ // all the matching elements.
+ hook_vector_[hook_index].erase(remove_if(hook_vector_[hook_index].begin(),
+ hook_vector_[hook_index].end(),
+ bind1st(equal_to<CalloutEntry>(),
+ target)),
+ hook_vector_[hook_index].end());
+
+ // Return an indication of whether anything was removed.
+ bool removed = initial_size != hook_vector_[hook_index].size();
+ if (removed) {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_EXTENDED_CALLS,
+ HOOKS_CALLOUT_DEREGISTERED).arg(current_library_).arg(name);
+ }
+
+ return (removed);
+}
+
+// Deregister all callouts on a given hook.
+
+bool
+CalloutManager::deregisterAllCallouts(const std::string& name) {
+
+ // Get the index associated with this hook (validating the name in the
+ // process).
+ int hook_index = server_hooks_.getIndex(name);
+
+ /// Construct a CalloutEntry matching the current library (the callout
+ /// pointer is NULL as we are not checking that).
+ CalloutEntry target(current_library_, NULL);
+
+ /// To decide if any entries were removed, we'll record the initial size
+ /// of the callout vector for the hook, and compare it with the size after
+ /// the removal.
+ size_t initial_size = hook_vector_[hook_index].size();
+
+ // Remove all callouts matching this library.
+ hook_vector_[hook_index].erase(remove_if(hook_vector_[hook_index].begin(),
+ hook_vector_[hook_index].end(),
+ bind1st(CalloutLibraryEqual(),
+ target)),
+ hook_vector_[hook_index].end());
+
+ // Return an indication of whether anything was removed.
+ bool removed = initial_size != hook_vector_[hook_index].size();
+ if (removed) {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_EXTENDED_CALLS,
+ HOOKS_ALL_CALLOUTS_DEREGISTERED).arg(current_library_)
+ .arg(name);
+ }
+
+ return (removed);
+}
+
+} // namespace util
+} // namespace isc
diff --git a/src/lib/hooks/callout_manager.h b/src/lib/hooks/callout_manager.h
new file mode 100644
index 0000000..8ca275b
--- /dev/null
+++ b/src/lib/hooks/callout_manager.h
@@ -0,0 +1,384 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef CALLOUT_MANAGER_H
+#define CALLOUT_MANAGER_H
+
+#include <exceptions/exceptions.h>
+#include <hooks/library_handle.h>
+#include <hooks/server_hooks.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <climits>
+#include <map>
+#include <string>
+
+namespace isc {
+namespace hooks {
+
+/// @brief No such library
+///
+/// Thrown if an attempt is made to set the current library index to a value
+/// that is invalid for the number of loaded libraries.
+class NoSuchLibrary : public Exception {
+public:
+ NoSuchLibrary(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Callout Manager
+///
+/// This class manages the registration, deregistration and execution of the
+/// library callouts. It is part of the hooks framework used by the BIND 10
+/// server, and is not for use by user-written code in a hooks library.
+///
+/// In operation, the class needs to know two items of data:
+///
+/// - The list of server hooks, which is used in two ways. Firstly, when a
+/// callout registers or deregisters a hook, it does so by name: the
+/// @ref isc::util::ServerHooks object supplies the names of registered
+/// hooks. Secondly, when the callouts associated with a hook are called by
+/// the server, the server supplies the index of the relevant hook: this is
+/// validated by reference to the list of hook.
+///
+/// - The number of loaded libraries. Each callout registered by a user
+/// library is associated with that library, the callout manager storing both
+/// a pointer to the callout and the index of the library in the list of
+/// loaded libraries. Callouts are allowed to dynamically register and
+/// deregister callouts in the same library (including themselves): they
+/// cannot affect callouts registered by another library. When calling a
+/// callout, the callout manager maintains the idea of a "current library
+/// index": if the callout calls one of the callout registration functions
+/// (indirectly via the @ref LibraryHandle object), the registration
+/// functions use the "current library index" in their processing.
+///
+/// These two items of data are supplied when an object of this class is
+/// constructed. The latter (number of libraries) can be updated after the
+/// class is constructed. (Such an update is used during library loading where
+/// the CalloutManager has to be constructed before the libraries are loaded,
+/// but one of the libraries subsequently fails to load.)
+///
+/// The library index is important because it determines in what order callouts
+/// on a particular hook are called. For each hook, the CalloutManager
+/// maintains a vector of callouts ordered by library index. When a callout
+/// is added to the list, it is added at the end of the callouts associated
+/// with the current library. To clarify this further, suppose that three
+/// libraries are loaded, A (assigned an index 1), B (assigned an index 2) and
+/// C (assigned an index 3). Suppose A registers two callouts on a given hook,
+/// A1 and A2 (in that order) B registers B1 and B2 (in that order) and C
+/// registers C1 and C2 (in that order). Internally, the callouts are stored
+/// in the order A1, A2, B1, B2, C1, and C2: this is also the order in which
+/// the are called. If B now registers another callout (B3), it is added to
+/// the vector after the list of callouts associated with B: the new order is
+/// therefore A1, A2, B1, B2, B3, C1 and C2.
+///
+/// Indexes range between 1 and n (where n is the number of the libraries
+/// loaded) and are assigned to libraries based on the order the libraries
+/// presented to the hooks framework for loading (something that occurs in the
+/// isc::util::HooksManager) class. However, two other indexes are recognised,
+/// 0 and INT_MAX. These are used when the server itself registers callouts -
+/// the server is able to register callouts that get called before any
+/// user-library callouts, and ones that get called after user-library callouts.
+/// In other words, assuming the callouts on a hook are A1, A2, B1, B2, B3, C2,
+/// C2 as before, and that the server registers S1 (to run before the
+/// user-registered callouts) and S2 (to run after them), the callouts are
+/// stored (and executed) in the order S1, A1, A2, B1, B2, B3, C2, C2, S2. In
+/// summary, the recognised index values are:
+///
+/// - < 0: invalid.
+/// - 0: used for server-registered callouts that are called before
+/// user-registered callouts.
+/// - 1 - n: callouts from user libraries.
+/// - INT_MAX: used for server-registered callouts called after
+/// user-registered callouts.
+///
+/// Note that the callout functions do not access the CalloutManager: instead,
+/// they use a LibraryHandle object. This contains an internal pointer to
+/// the CalloutManager, but provides a restricted interface. In that way,
+/// callouts are unable to affect callouts supplied by other libraries.
+
+class CalloutManager {
+private:
+
+ // Private typedefs
+
+ /// Element in the vector of callouts. The elements in the pair are the
+ /// index of the library from which this callout was registered, and a#
+ /// pointer to the callout itself.
+ typedef std::pair<int, CalloutPtr> CalloutEntry;
+
+ /// An element in the hook vector. Each element is a vector of callouts
+ /// associated with a given hook.
+ typedef std::vector<CalloutEntry> CalloutVector;
+
+public:
+
+ /// @brief Constructor
+ ///
+ /// Initializes member variables, in particular sizing the hook vector
+ /// (the vector of callout vectors) to the appropriate size.
+ ///
+ /// @param num_libraries Number of loaded libraries.
+ ///
+ /// @throw isc::BadValue if the number of libraries is less than 0,
+ CalloutManager(int num_libraries = 0);
+
+ /// @brief Register a callout on a hook for the current library
+ ///
+ /// Registers a callout function for the current library with a given hook
+ /// (the index of the "current library" being given by the current_library_
+ /// member). The callout is added to the end of the callouts for this
+ /// library that are associated with that hook.
+ ///
+ /// @param name Name of the hook to which the callout is added.
+ /// @param callout Pointer to the callout function to be registered.
+ ///
+ /// @throw NoSuchHook The hook name is unrecognised.
+ /// @throw Unexpected The hook name is valid but an internal data structure
+ /// is of the wrong size.
+ void registerCallout(const std::string& name, CalloutPtr callout);
+
+ /// @brief De-Register a callout on a hook for the current library
+ ///
+ /// Searches through the functions registered by the the current library
+ /// (the index of the "current library" being given by the current_library_
+ /// member) with the named hook and removes all entries matching the
+ /// callout.
+ ///
+ /// @param name Name of the hook from which the callout is removed.
+ /// @param callout Pointer to the callout function to be removed.
+ ///
+ /// @return true if a one or more callouts were deregistered.
+ ///
+ /// @throw NoSuchHook The hook name is unrecognised.
+ /// @throw Unexpected The hook name is valid but an internal data structure
+ /// is of the wrong size.
+ bool deregisterCallout(const std::string& name, CalloutPtr callout);
+
+ /// @brief Removes all callouts on a hook for the current library
+ ///
+ /// Removes all callouts associated with a given hook that were registered
+ /// by the current library (the index of the "current library" being given
+ /// by the current_library_ member).
+ ///
+ /// @param name Name of the hook from which the callouts are removed.
+ ///
+ /// @return true if one or more callouts were deregistered.
+ ///
+ /// @throw NoSuchHook Thrown if the hook name is unrecognised.
+ bool deregisterAllCallouts(const std::string& name);
+
+ /// @brief Checks if callouts are present on a hook
+ ///
+ /// Checks all loaded libraries and returns true if at least one callout
+ /// has been registered by any of them for the given hook.
+ ///
+ /// @param hook_index Hook index for which callouts are checked.
+ ///
+ /// @return true if callouts are present, false if not.
+ ///
+ /// @throw NoSuchHook Given index does not correspond to a valid hook.
+ bool calloutsPresent(int hook_index) const;
+
+ /// @brief Calls the callouts for a given hook
+ ///
+ /// Iterates through the libray handles and calls the callouts associated
+ /// with the given hook index.
+ ///
+ /// @note This method invalidates the current library index set with
+ /// setLibraryIndex().
+ ///
+ /// @param hook_index Index of the hook to call.
+ /// @param callout_handle Reference to the CalloutHandle object for the
+ /// current object being processed.
+ void callCallouts(int hook_index, CalloutHandle& callout_handle);
+
+ /// @brief Get current hook index
+ ///
+ /// Made available during callCallouts, this is the index of the hook
+ /// on which callouts are being called.
+ int getHookIndex() const {
+ return (current_hook_);
+ }
+
+ /// @brief Get number of libraries
+ ///
+ /// Returns the number of libraries that this CalloutManager is expected
+ /// to serve. This is the number passed to its constructor.
+ ///
+ /// @return Number of libraries served by this CalloutManager.
+ int getNumLibraries() const {
+ return (num_libraries_);
+ }
+
+ /// @brief Get current library index
+ ///
+ /// Returns the index of the "current" library. This the index associated
+ /// with the currently executing callout when callCallouts is executing.
+ /// When callCallouts() is not executing (as is the case when the load()
+ /// function in a user-library is called during the library load process),
+ /// the index can be set by setLibraryIndex().
+ ///
+ /// @note The value set by this method is lost after a call to
+ /// callCallouts.
+ ///
+ /// @return Current library index.
+ int getLibraryIndex() const {
+ return (current_library_);
+ }
+
+ /// @brief Set current library index
+ ///
+ /// Sets the current library index. This has the following valid values:
+ ///
+ /// - -1: invalidate current index.
+ /// - 0: pre-user library callout.
+ /// - 1 - numlib: user-library callout (where "numlib" is the number of
+ /// libraries loaded in the system, this figure being passed to this
+ /// object at construction time).
+ /// - INT_MAX: post-user library callout.
+ ///
+ /// @param library_index New library index.
+ ///
+ /// @throw NoSuchLibrary if the index is not valid.
+ void setLibraryIndex(int library_index) {
+ checkLibraryIndex(library_index);
+ current_library_ = library_index;
+ }
+
+ /// @defgroup calloutManagerLibraryHandles Callout manager library handles
+ ///
+ /// The CalloutManager offers three library handles:
+ ///
+ /// - a "standard" one, used to register and deregister callouts for
+ /// the library index that is marked as current in the CalloutManager.
+ /// When a callout is called, it is passed this one.
+ /// - a pre-library callout handle, used by the server to register
+ // callouts to run prior to user-library callouts.
+ /// - a post-library callout handle, used by the server to register
+ /// callouts to run after the user-library callouts.
+ //@{
+
+ /// @brief Return library handle
+ ///
+ /// The library handle is available to the user callout via the callout
+ /// handle object. It provides a cut-down view of the CalloutManager,
+ /// allowing the callout to register and deregister callouts in the
+ /// library of which it is part, whilst denying access to anything that
+ /// may affect other libraries.
+ ///
+ /// @return Reference to library handle for this manager
+ LibraryHandle& getLibraryHandle() {
+ return (library_handle_);
+ }
+
+ /// @brief Return pre-user callouts library handle
+ ///
+ /// The LibraryHandle to affect callouts that will run before the
+ /// user-library callouts.
+ ///
+ /// @return Reference to pre-user library handle for this manager
+ LibraryHandle& getPreLibraryHandle() {
+ return (pre_library_handle_);
+ }
+
+ /// @brief Return post-user callouts library handle
+ ///
+ /// The LibraryHandle to affect callouts that will run before the
+ /// user-library callouts.
+ ///
+ /// @return Reference to post-user library handle for this manager
+ LibraryHandle& getPostLibraryHandle() {
+ return (post_library_handle_);
+ }
+
+ //@}
+
+private:
+ /// @brief Check library index
+ ///
+ /// Ensures that the current library index is valid. This is called by
+ /// the hook registration functions.
+ ///
+ /// @param library_index Value to check for validity as a library index.
+ /// Valid values are 0 - numlib+1 and -1: see @ref setLibraryIndex
+ /// for the meaning of the various values.
+ ///
+ /// @throw NoSuchLibrary Library index is not valid.
+ void checkLibraryIndex(int library_index) const;
+
+ /// @brief Compare two callout entries for library equality
+ ///
+ /// This is used in callout removal code when all callouts on a hook for a
+ /// given library are being removed. It checks whether two callout entries
+ /// have the same library index.
+ ///
+ /// @param ent1 First callout entry to check
+ /// @param ent2 Second callout entry to check
+ ///
+ /// @return bool true if the library entries are the same
+ class CalloutLibraryEqual :
+ public std::binary_function<CalloutEntry, CalloutEntry, bool> {
+ public:
+ bool operator()(const CalloutEntry& ent1,
+ const CalloutEntry& ent2) const {
+ return (ent1.first == ent2.first);
+ }
+ };
+
+ // Member variables
+
+ /// Reference to the singleton ServerHooks object. See the
+ /// @ref hooksmgMaintenanceGuide for information as to why the class holds
+ /// a reference instead of accessing the singleton within the code.
+ ServerHooks& server_hooks_;
+
+ /// Current hook. When a call is made to callCallouts, this holds the
+ /// index of the current hook. It is set to an invalid value (-1)
+ /// otherwise.
+ int current_hook_;
+
+ /// Current library index. When a call is made to any of the callout
+ /// registration methods, this variable indicates the index of the user
+ /// library that should be associated with the call.
+ int current_library_;
+
+ /// Vector of callout vectors. There is one entry in this outer vector for
+ /// each hook. Each element is itself a vector, with one entry for each
+ /// callout registered for that hook.
+ std::vector<CalloutVector> hook_vector_;
+
+ /// LibraryHandle object user by the callout to access the callout
+ /// registration methods on this CalloutManager object. The object is set
+ /// such that the index of the library associated with any operation is
+ /// whatever is currently set in the CalloutManager.
+ LibraryHandle library_handle_;
+
+ /// LibraryHandle for callouts to be registered as being called before
+ /// the user-registered callouts.
+ LibraryHandle pre_library_handle_;
+
+ /// LibraryHandle for callouts to be registered as being called after
+ /// the user-registered callouts.
+ LibraryHandle post_library_handle_;
+
+ /// Number of libraries.
+ int num_libraries_;
+};
+
+} // namespace util
+} // namespace isc
+
+#endif // CALLOUT_MANAGER_H
diff --git a/src/lib/hooks/hooks.cc b/src/lib/hooks/hooks.cc
new file mode 100644
index 0000000..e5645d5
--- /dev/null
+++ b/src/lib/hooks/hooks.cc
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/hooks.h>
+#include <log/logger_support.h>
+
+#include <string>
+
+
+namespace isc {
+namespace hooks {
+
+// Load the logging message dictionary if not already loaded
+
+void
+hooksStaticLinkInit() {
+ if (!isc::log::isLoggingInitialized()) {
+ isc::log::initLogger(std::string("userlib"));
+ }
+}
+
+} // namespace hooks
+} // namespace isc
diff --git a/src/lib/hooks/hooks.h b/src/lib/hooks/hooks.h
new file mode 100644
index 0000000..42dfaca
--- /dev/null
+++ b/src/lib/hooks/hooks.h
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef HOOKS_H
+#define HOOKS_H
+
+#include <config.h>
+#include <hooks/callout_handle.h>
+#include <hooks/library_handle.h>
+
+namespace {
+
+// Version 1 of the hooks framework.
+const int BIND10_HOOKS_VERSION = 1;
+
+// Names of the framework functions.
+const char* const LOAD_FUNCTION_NAME = "load";
+const char* const UNLOAD_FUNCTION_NAME = "unload";
+const char* const VERSION_FUNCTION_NAME = "version";
+
+// Typedefs for pointers to the framework functions.
+typedef int (*version_function_ptr)();
+typedef int (*load_function_ptr)(isc::hooks::LibraryHandle&);
+typedef int (*unload_function_ptr)();
+
+} // Anonymous namespace
+
+namespace isc {
+namespace hooks {
+
+/// @brief User-Library Initialization for Statically-Linked BIND 10
+///
+/// If BIND 10 is statically-linked, a user-created hooks library will not be
+/// able to access symbols in it. In particular, it will not be able to access
+/// singleton objects.
+///
+/// The hooks framework handles some of this. For example, although there is
+/// a singleton ServerHooks object, hooks framework objects store a reference
+/// to it when they are created. When the user library needs to register a
+/// callout (which requires access to the ServerHooks information), it accesses
+/// the ServerHooks object through a pointer passed from the BIND 10 image.
+///
+/// The logging framework is more problematical. Here the code is partly
+/// statically linked (the BIND 10 logging library) and partly shared (the
+/// log4cplus). The state of the former is not accessible to the user library,
+/// but the state of the latter is. So within the user library, we need to
+/// initialize the BIND 10 logging library but not initialize the log4cplus
+/// code. Some of the initialization is done when the library is loaded, but
+/// other parts are done at run-time.
+///
+/// This function - to be called by the user library code in its load() function
+/// when running against a statically linked BIND 10 - initializes the BIND 10
+/// logging library. In particular, it loads the message dictionary with the
+/// text of the BIND 10 messages.
+///
+/// @note This means that the virtual address space is loaded with two copies
+/// of the message dictionary. Depending on how the user libraries are linked,
+/// loading multiple user libraries may involve loading one message dictionary
+/// per library.
+
+void hooksStaticLinkInit();
+
+} // namespace hooks
+} // namespace isc
+
+#endif // HOOKS_H
diff --git a/src/lib/hooks/hooks_component_developer.dox b/src/lib/hooks/hooks_component_developer.dox
new file mode 100644
index 0000000..777ae2e
--- /dev/null
+++ b/src/lib/hooks/hooks_component_developer.dox
@@ -0,0 +1,483 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/**
+ at page hooksComponentDeveloperGuide Guide to Hooks for the BIND 10 Component Developer
+
+ at section hooksComponentIntroduction Introduction
+
+The hooks framework is a BIND 10 system that simplifies the way that
+users can write code to modify the behavior of BIND 10. Instead of
+altering the BIND 10 source code, they write functions that are compiled
+and linked into a shared library. The library is specified in the BIND 10
+configuration database and run time, BIND 10 dynamically loads the library
+into its address space. At various points in the processing, the component
+"calls out" to functions in the library, passing to them the data is it
+currently working on. They can examine and modify the data as required.
+
+This guide is aimed at BIND 10 developers who want to write or modify a
+BIND 10 component to use hooks. It shows how the component should be written
+to load a shared library at run-time and how to call functions in it.
+
+For information about writing a hooks library containing functions called by BIND 10
+during its execution, see the document @ref hooksdgDevelopersGuide.
+
+ at subsection hooksComponentTerminology Terminology
+
+In the remainder of this guide, the following terminology is used:
+
+- Component - a BIND 10 process, e.g. the authoritative DNS server or the
+DHCPv4 server.
+
+- Hook/Hook Point - used interchageably, this is a point in the code at
+which a call to user-written functions is made. Each hook has a name and
+each hook can have any number (including 0) of user-written functions
+attached to it.
+
+- Callout - a user-written function called by the component at a hook
+point. This is so-named because the component "calls out" to the library
+to execute a user-written function.
+
+- User code/user library - non-BIND 10 code that is compiled into a
+shared library and loaded by BIND 10 into its address space. Multiple
+user libraries can be loaded at the same time, each containing callouts for
+the same hooks. The hooks framework calls these libraries one after the
+other. (See the document @ref hooksdgDevelopersGuide for more details.)
+
+ at subsection hooksComponentLanguages Languages
+
+The core of BIND 10 is written in C++ with some parts in Python. While it is
+the intention to provide the hooks framework for all languages, the initial
+version is for C++. All examples in this guide are in that language.
+
+ at section hooksComponentBasicIdeas Basic Ideas
+
+From the point of view of the component author, the basic ideas of the hooks
+framework are quite simple:
+
+- The location of hook points in the code need to be determined.
+
+- Name the hook points and register them.
+
+- At each hook point, the component needs to complete the following steps to
+ execute callouts registered by the user-library:
+ -# copy data into the object used to pass information to the callout.
+ -# call the callout.
+ -# copy data back from the object used to exchange information.
+ -# take action based on information returned.
+
+Of course, to set up the system the libraries need to be loaded in the first
+place. The component also needs to:
+
+- Define the configuration item that specifies the user libraries for this
+component.
+
+- Handle configuration changes and load/unload the user libraries.
+
+The following sections will describe these tasks in more detail.
+
+ at section hooksComponentDefinition Determing the Hook Points
+
+Before any other action takes place, the location of the hook points
+in the code need to be determined. This of course depends on the
+component but as a general guideline, hook locations should be chosen
+where a callout is able to obtain useful information from BIND 10 and/or
+affect processing. Typically this means at the start or end of a major
+step in the processing of a request, at a point where either useful
+information can be passed to a callout and/or the callout can affect
+the processing of the component. The latter is achieved in either or both
+of the following eays:
+
+- Setting the "skip" flag. This is a boolean flag that the callout can set
+ and is a quick way of passing information back to the component. It is used
+ to indicate that the component should skip the processing step associated with
+ the hook. The exact action is up to the component, but is likely to be one
+ of skipping the processing step (probably because the callout has
+ done its own processing for the action) or dropping the current packet
+ and starting on a new request.
+
+- Modifying data passed to it. The component should be prepared to continue
+ processing with the data returned by the callout. It is up to the component
+ author whether the data is validated before being used, but doing so will
+ have performance implications.
+
+ at section hooksComponentRegistration Naming and Registering the Hooks Points
+
+Once the location of the hook point has been determined, it should be
+given a name. This name should be unique amongst all hook points and is
+subject to certain restrictions (see below).
+
+Before the callouts at any hook point are called and any user libraries
+loaded - so typically during component initialization - the component must
+register the names of all the hooks. The registration is done using
+the static method isc::hooks::HooksManager::registerHook():
+
+ at code
+
+#include <hooks/hooks_manager.h>
+ :
+ int example_index = HooksManager::registerHook("lease_allocate");
+ at endcode
+
+The name of the hook is passed as the sole argument to the registerHook()
+method. The value returned is the index of that hook point and should
+be retained - it is needed to call the callouts attached to that hook.
+
+Note that a hook only needs to be registered once. There is no mechanism for
+unregistering a hook and there is no need to do so.
+
+ at subsection hooksComponentAutomaticRegistration Automatic Registration of Hooks
+
+In some components, it may be convenient to set up a single initialization
+function that registers all hooks. For others, it may be more convenient
+for each module within the component to perform its own initialization.
+Since the isc::hooks::HooksManager object is a singleton and is created when first
+accessed, a useful trick is to automatically register the hooks when
+the module is loaded.
+
+This technique involves declaring an object outside of any execution
+unit in the module. When the module is loaded, the object's constructor
+is run. By placing the hook registration calls in the constructor,
+the hooks in the module are defined at load time, before any function in
+the module is run. The code for such an initialization sequence would
+be similar to:
+
+ at code
+#include <hooks/hooks_manager.h>
+
+namespace {
+
+// Declare structure to perform initialization and store the hook indexes.
+//
+struct MyHooks {
+ int pkt_rcvd; // Index of "packet received" hook
+ int pkt_sent; // Index of "packet sent" hook
+
+ // Constructor
+ MyHooks() {
+ pkt_rcvd = HooksManager::registerHook("pkt_rcvd");
+ pkt_sent = HooksManager::registerHook("pkt_sent");
+ }
+};
+
+// Declare a "MyHooks" object. As this is outside any function or method, it
+// will be instantiated (and the constructor run) when the module is loaded.
+// As a result, the hook indexes will be defined before any method in this
+// module is called.
+MyHooks my_hooks;
+
+} // Anonymous namespace
+
+void Someclass::someFunction() {
+ :
+ // Check if any callouts are defined on the pkt_rcvd hook.
+ if (HooksManager::calloutPresent(my_hooks.pkt_rcvd)) {
+ :
+ }
+ :
+}
+ at endcode
+
+ at subsection hooksComponentHookNames Hook Names
+
+Hook names are strings and in principle, any string can be used as the
+name of a hook, even one containing spaces and non-printable characters.
+However, the following guidelines should be observed:
+
+- The names <b>context_create</b> and <b>context_destroy</b> are reserved to
+the hooks system and are automatically registered: an attempt to register
+one of these will lead to a isc::hooks::DuplicateHook exception being thrown.
+
+- The hook name should be a valid "C" function name. If a user gives a
+callout the same name as one of the hooks, the hooks framework will
+automatically load that callout and attach it to the hook: the user does not
+have to explicitly register it.
+
+- The hook name should not conflict with the name of a function in any of
+the system libraries (e.g. naming a hook "sqrt" could lead to the
+square-root function in the system's maths library being attached to the hook
+as a callout).
+
+- Although hook names can be in any case (including mixed case), the BIND 10
+convention is that they are lower-case.
+
+ at section hooksComponentCallingCallouts Calling Callouts on a Hook
+
+ at subsection hooksComponentArgument The Callout Handle
+
+Before describing how to call user code at a hook point, we must first consider
+how to pass data to it.
+
+Each user callout has the signature:
+ at code
+int callout_name(isc::hooks::CalloutHandle& handle);
+ at endcode
+
+The isc::hooks::CalloutHandle object is the object used to pass data to
+and from the callout. This holds the data as a set of name/value pairs,
+each pair being considered an argument to the callout. If there are
+multiple callouts attached to a hook, the CalloutHandle is passed to
+each in turn. Should a callout modify an argument, the updated data is
+passed subsequent callouts (each of which could also modify it) before
+being returned to the component.
+
+Two methods are provided to get and set the arguments passed to
+the callout called (naturally enough) getArgument and SetArgument.
+Their usage is illustrated by the following code snippets.
+
+ at code
+ int count = 10;
+ boost::shared_ptr<Pkt4> pktptr = ... // Set to appropriate value
+
+ // Assume that "handle_ptr" has been created and is a pointer to a
+ // CalloutHandle.
+ handle_ptr->setArgument("data_count", count);
+ handle_ptr->setArgument("inpacket", pktptr);
+
+ // Call the hook code. lease_assigned_index is the value returned from
+ // HooksManager::registerHook() when the hook was registered.
+ HooksManager::callCallouts(lease_assigned_index, *handle_ptr);
+
+ // Retrieve the modified values
+ handle_ptr->getArgument("data_count", count);
+ handle_ptr->getArgument("inpacket", pktptr);
+ at endcode
+
+As can be seen "getArgument" is used to retrieve data from the
+CalloutHandle, and "setArgument" used to put data into it. If a callout
+wishes to alter data and pass it back to the component, it should retrieve
+the data with getArgument, modify it, and call setArgument to send
+it back.
+
+There are a couple points to be aware of:
+
+- The data type of the variable in the call to getArgument must
+match the data type of the variable passed to the corresponding
+setArgument <B>exactly</B>: using what would normally be considered
+to be a "compatible" type is not enough. For example, if the callout
+passed an argument back to the component as an "int" and the component
+attempted to retrieve it as a "long", an exception would be thrown even
+though any value that can be stored in an "int" will fit into a "long".
+This restriction also applies the "const" attribute but only as applied to
+data pointed to by pointers, e.g. if an argument is defined as a "char*",
+an exception will be thrown if an attempt is made to retrieve it into
+a variable of type "const char*". (However, if an argument is set as a
+"const int", it can be retrieved into an "int".) The documentation of
+a hook point should detail the exact data type of each argument.
+
+- If a pointer to an object is passed to a callout (either a "raw"
+pointer, or a boost smart pointer (as in the example above), and the
+underlying object is altered through that pointer, the change will be
+reflected in the component even if the callout makes no call to setArgument.
+This can be avoided by passing a pointer to a "const" object.
+
+ at subsection hooksComponentSkipFlag The Skip Flag
+
+Although information is passed back to the component from callouts through
+CalloutHandle arguments, a common action for callouts is to inform the component
+that its flow of control should be altered. For example:
+
+- In the DHCP servers, there is a hook at the point at which a lease is
+ about to be assigned. Callouts attached to this hooks may handle the
+ lease assignment in special cases, in which case they set the skip flag
+ to indicate that the server should not perform lease assignment in this
+ case.
+- A server may define a hook just after a packet is received. A callout
+ attached to the hook might inspect the source address and compare it
+ against a blacklist. If the address is on the list, the callout could set
+ the skip flag to indicate to the server that the packet should be dropped.
+
+For ease of processing, the CalloutHandle contains
+two methods, isc::hooks::CalloutHandle::getSkip() and
+isc::hooks::CalloutHandle::setSkip(). It is only meaningful for the
+component to use the "get" method. The skip flag is cleared by the hooks
+framework when the component requests that callouts be executed, so any
+value set by the component is lost. Callouts can both inspect the flag (it
+might have been set by callouts earlier in the callout list for the hook)
+and set it. Note that the setting of the flag by a callout does not
+prevent callouts later in the list from being called: the skip flag is
+just a boolean flag - the only significance comes from its interpretation
+by the component.
+
+An example of use could be:
+ at code
+// Set up arguments for DHCP lease assignment.
+handle->setArgument("query", query);
+handle->setArgument("response", response);
+HooksManager::callCallouts(lease_hook_index, *handle_ptr);
+if (! handle_ptr->getSkip()) {
+ // Skip flag not set, do the address allocation
+ :
+}
+ at endcode
+
+
+ at subsection hooksComponentGettingHandle Getting the Callout Handle
+
+The CalloutHandle object is linked to the loaded libraries
+for lifetime reasons (described below). Components
+should retrieve a isc::hooks::CalloutHandle using
+isc::hooks::HooksManager::createCalloutHandle():
+ at code
+ CalloutHandlePtr handle_ptr = HooksManager::createCalloutHandle();
+ at endcode
+(isc::hooks::CalloutHandlePtr is a typedef for a Boost shared pointer to a
+CalloutHandle.) The CalloutHandle so retrieved may be used for as
+long as the libraries are loaded.
+
+The handle is deleted by resetting the pointer:
+ at code
+ handle_ptr.reset();
+ at endcode
+... or by letting the handle pointer go out of scope. The actual deletion
+occurs when the CallHandle's reference count goes to zero. (The
+current version of the hooks framework does not maintain any other
+pointer to the returned CalloutHandle, so it gets destroyed when the
+shared pointer to it is cleared or destroyed. However, this may change
+in a future version.)
+
+ at subsection hooksComponentCallingCallout Calling the Callout
+
+Calling the callout is a simple matter of executing the
+isc::hooks::HooksManager::callCallouts() method for the hook index in
+question. For example, with the hook index pkt_sent defined as above,
+the hook can be executed by:
+ at code
+ HooksManager::callCallouts(pkt_sent, *handle_ptr);
+ at endcode
+... where "*handle_ptr" is a reference (note: not a pointer) to the
+isc::hooks::CalloutHandle object holding the arguments. No status code
+is returned. If a component needs to get data returned (other than that
+provided by the "skip" flag), it should define an argument through which
+the callout can do so.
+
+ at subsubsection hooksComponentConditionalCallout Conditionally Calling Hook Callouts
+
+Most hooks in a component will not have callouts attached to them. To
+avoid the overhead of setting up arguments in the CalloutHandle, a
+component can check for callouts before doing that processing using
+isc::hooks::HooksManager::calloutsPresent(). Taking the index of a
+hook as its sole argument, the function returns true if there are any
+callouts attached to the hook and false otherwise.
+
+With this check, the code in the component for calling a hook would look
+something like:
+ at code
+if (HooksManager::calloutsPresent(lease_hook_index)) {
+ // Set up arguments for lease assignment
+ handle->setArgument("query", query);
+ handle->setArgument("response", response);
+ HooksManager::callCallouts(lease_hook_index, *handle);
+ if (! handle->getSkip()) {
+ // Skip flag not set, do the address allocation
+ :
+ }
+}
+ at endcode
+
+ at section hooksComponentLoadLibraries Loading the User Libraries
+
+Once hooks are defined, all the hooks code described above will
+work, even if no libraries are loaded (and even if the library
+loading method is not called). The CalloutHandle returned by
+isc::hooks::HooksManager::createCalloutHandle() will be valid,
+isc::hooks::HooksManager::calloutsPresent() will return false for every
+index, and isc::hooks::HooksManager::callCallouts() will be a no-op.
+
+However, if user libraries are specified in the BIND 10 configuration,
+the component should load them. (Note the term "libraries": the hooks
+framework allows multiple user libraries to be loaded.) This should take
+place after the component's configuration has been read, and is achieved
+by the isc::hooks::HooksManager::loadLibraries() method. The method is
+passed a vector of strings, each giving the full file specification of
+a user library:
+ at code
+ std::vector<std::string> libraries = ... // Get array of libraries
+ bool success = HooksManager::loadLibraries(libraries);
+ at endcode
+loadLibraries() returns a boolean status which is true if all libraries
+loaded successfully or false if one or more failed to load. Appropriate
+error messages will have been logged in the latter case, the status
+being more to allow the developer to decide whether the execution
+should proceed in such circumstances.
+
+If loadLibraries() is called a second or subsequent time (as a result
+of a reconfiguration), all existing libraries are unloaded and a new
+set loaded. Libraries can be explicitly unloaded either by calling
+isc::hooks::HooksManager::unloadLibraries() or by calling
+loadLibraries() with an empty vector as an argument.
+
+ at subsection hooksComponentUnloadIssues Unload and Reload Issues
+
+Unloading a shared library works by unmapping the part of the process's
+virtual address space in which the library lies. This may lead to
+problems if there are still references to that address space elsewhere
+in the process.
+
+In many operating systems, heap storage allowed by a shared library will
+lie in the virtual address allocated to the library. This has implications
+in the hooks framework because:
+
+- Argument information stored in a CalloutHandle by a callout in a library
+may lie in the library's address space.
+- Data modified in objects passed as arguments may lie in the address
+space. For example, it is common for a DHCP callout to add "options"
+to a packet: the memory allocated for those options will most likely
+lie in library address space.
+
+The problem really arises because of the extensive use by BIND 10 of boost
+smart pointers. When the pointer is destroyed, the pointed-to memory is
+deallocated. If the pointer points to address space that is unmapped because
+a library has been unloaded, the deletion causes a segmentation fault.
+
+The hooks framework addresses the issue for CalloutHandles by keeping in
+that object a shared pointer to the object controlling library unloading.
+Although a library can be unloaded at any time, it is only when all
+CalloutHandles that could possibly reference address space in the library
+have been deleted that the library will actually be unloaded and the
+address space unmapped.
+
+The hooks framework cannot solve the second issue as the objects in
+question are under control of the component. It is up to the component
+developer to ensure that all such objects have been destroyed before
+libraries are reloaded. In extreme cases this may mean the component
+suspending all processing of incoming requests until all currently
+executing requests have completed and data object destroyed, reloading
+the libraries, then resuming processing.
+
+ at section hooksComponentCallouts Component-Defined Callouts
+
+Previous sections have discussed callout registration by user libraries.
+It is possible for a component to register its own functions (i.e. within
+its own address space) as hook callouts. These functions are called
+in eactly the same way as user callouts, being passed their arguments
+though a CalloutHandle object. (Guidelines for writing callouts can be
+found in @ref hooksdgDevelopersGuide.)
+
+A component can associate with a hook callouts that run either before
+user-registered callouts or after them. Registration is done via a
+isc::hooks::LibraryHandle object, a reference to one being obtained
+through the methods isc::hooks::HooksManager::preCalloutLibraryHandle()
+(for a handle to register callouts to run before the user library
+callouts) or isc::hooks::HooksManager::postCalloutLibraryHandle() (for
+a handle to register callouts to run after the user callouts). Use of
+the LibraryHandle to register and deregister callouts is described in
+ at ref hooksdgLibraryHandle.
+
+Finally, it should be noted that callouts registered in this way only
+remain registered until the next call to isc::hooks::loadLibraries().
+It is up to the component to re-register the callouts after this
+method has been called.
+
+*/
diff --git a/src/lib/hooks/hooks_log.cc b/src/lib/hooks/hooks_log.cc
new file mode 100644
index 0000000..360394c
--- /dev/null
+++ b/src/lib/hooks/hooks_log.cc
@@ -0,0 +1,26 @@
+// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// Defines the logger used by the NSAS
+
+#include "hooks/hooks_log.h"
+
+namespace isc {
+namespace hooks {
+
+isc::log::Logger hooks_logger("hooks");
+
+} // namespace hooks
+} // namespace isc
+
diff --git a/src/lib/hooks/hooks_log.h b/src/lib/hooks/hooks_log.h
new file mode 100644
index 0000000..92d429a
--- /dev/null
+++ b/src/lib/hooks/hooks_log.h
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef HOOKS_LOG_H
+#define HOOKS_LOG_H
+
+#include <log/macros.h>
+#include <hooks/hooks_messages.h>
+
+namespace isc {
+namespace hooks {
+
+/// @brief Hooks debug Logging levels
+///
+/// Defines the levels used to output debug messages in the Hooks framework.
+/// Note that higher numbers equate to more verbose (and detailed) output.
+
+// The first level traces normal operations,
+const int HOOKS_DBG_TRACE = DBGLVL_TRACE_BASIC;
+
+// The next level traces each call to hook code.
+const int HOOKS_DBG_CALLS = DBGLVL_TRACE_BASIC_DATA;
+
+// Additional information on the calls. Report each call to a callout (even
+// if there are multiple callouts on a hook) and each status return.
+const int HOOKS_DBG_EXTENDED_CALLS = DBGLVL_TRACE_DETAIL_DATA;
+
+
+/// @brief Hooks Logger
+///
+/// Define the logger used to log messages. We could define it in multiple
+/// modules, but defining in a single module and linking to it saves time and
+/// space.
+extern isc::log::Logger hooks_logger;
+
+} // namespace hooks
+} // namespace isc
+
+#endif // HOOKS_LOG_H
diff --git a/src/lib/hooks/hooks_maintenance.dox b/src/lib/hooks/hooks_maintenance.dox
new file mode 100644
index 0000000..51a07dc
--- /dev/null
+++ b/src/lib/hooks/hooks_maintenance.dox
@@ -0,0 +1,382 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Note: the prefix "hooksmg" to all labels is an abbreviation for "Hooks
+// Maintenance Guide" and is used to prevent a clash with symbols in any
+// other Doxygen file.
+
+/**
+ @page hooksmgMaintenanceGuide Hooks Maintenance Guide
+
+ @section hooksmgIntroduction Introduction
+
+ This document is aimed at BIND 10 maintainers responsible for the hooks
+ system. It provides an overview of the classes that make up the hooks
+ framework and notes important aspects of processing. More detailed
+ information can be found in the source code.
+
+ It is assumed that the reader is familiar with the contents of the @ref
+ hooksdgDevelopersGuide and the @ref hooksComponentDeveloperGuide.
+
+ @section hooksmgObjects Hooks Framework Objects
+
+ The relationships between the various objects in the hooks framework
+ is shown below:
+
+ @image html HooksUml.png "High-Level Class Diagram of the Hooks Framework"
+
+ (To avoid clutter, the @ref hooksmgServerHooks object, used to pass
+ information about registered hooks to the components, is not shown on
+ the diagram.)
+
+ The hooks framework objects can be split into user-side objects and
+ server-side objects. The former are those objects used or referenced
+ by user-written hooks libraries. The latter are those objects used in
+ the hooks framework.
+
+ @subsection hooksmgUserObjects User-Side Objects
+
+ The user-side code is able to access two objects in the framework,
+ the @ref hooksmgCalloutHandle and the @ref hooksmgLibraryHandle.
+ The @ref hooksmgCalloutHandle is used to pass data between the BIND 10
+ component and the loaded library; the @ref hooksmgLibraryHandle is used
+ for registering callouts.
+
+ @subsubsection hooksmgCalloutHandle Callout Handle
+
+ The @ref isc::hooks::CalloutHandle has two functions: passing arguments
+ between the BIND 10 component and the user-written library, and storing
+ per-request context between library calls. In both cases the data is
+ stored in a std::map structure, keyed by argument (or context item) name.
+ The actual data is stored in a boost::any object, which allows any
+ data type to be stored, although a penalty for this flexibility is
+ the restriction (mentioned in the @ref hooksdgDevelopersGuide) that
+ the type of data retrieved must be identical (and not just compatible)
+ with that stored.
+
+ The storage of context data is slightly complex because there is
+ separate context for each user library. For this reason, the @ref
+ hooksmgCalloutHandle has multiple maps, one for each library loaded.
+ The maps are stored in another map, the appropriate map being identified
+ by the "current library index" (this index is explained further below).
+ The reason for the second map (rather than a structure such as a vector)
+ is to avoid creating individual context maps unless needed; given the
+ key to the map (in this case the current library index) accessing an
+ element in a map using the operator[] method returns the element in
+ question if it exists, or creates a new one (and stores it in the map)
+ if its doesn't.
+
+ @subsubsection hooksmgLibraryHandle Library Handle
+
+ Little more than a restricted interface to the @ref
+ hooksmgCalloutManager, the @ref isc::hooks::LibraryHandle allows a
+ callout to register and deregister callouts. However, there are some
+ quirks to callout registration which, although the processing involved
+ is in the @ref hooksmgCalloutManager, are best described here.
+
+ Firstly, a callout can be deregistered by a function within a user
+ library only if it was registered by a function within that library. That
+ is to say, if library A registers the callout A_func() on hook "alpha"
+ and library B registers B_func(), functions within library A are only
+ able to remove A_func() (and functions in library B remove B_func()).
+ The restriction - here to prevent one library interfering with the
+ callouts of another - is enforced by means of the current library index.
+ As described below, each entry in the vector of callouts associated with
+ a hook is a pair object, comprising a pointer to the callout and
+ the index of the library with which it is associated. A callout
+ can only modify entries in that vector where the current library index
+ matches the index element of the pair.
+
+ A second quirk is that when dynamically modifying the list of callouts,
+ the change only takes effect when the current call out from the server
+ completes. To clarify this, suppose that functions A_func(), B_func()
+ and C_func() are registered on a hook, and the server executes a callout
+ on the hook. Suppose also during this call, A_func() removes the callout
+ C_func() and that B_func() adds D_func(). As changes only take effect
+ when the current call out completes, the user callouts executed will be
+ A_func(), B_func() then C_func(). When the server calls the hook callouts
+ again, the functions executed will be A_func(), B_func() and D_func().
+
+ This restriction is down to implementation. When a set of callouts on a hook
+ is being called, the @ref hooksmgCalloutManager iterates through a
+ vector (the "callout vector") of (index, callout pointer) pairs. Since
+ registration or deregistration of a callout on that hook would change the
+ vector (and so potentially invalidate the iterators used to access the it),
+ a copy of the vector is taken before the iteration starts. The @ref
+ hooksmgCalloutManager iterates over this copy while any changes made
+ by the callout registration functions affect the relevant callout vector.
+ Such approach was chosen because of performance considerations.
+
+ @subsection hooksmgServerObjects Server-Side Objects
+
+ Those objects are not accessible by user libraries. Please do not
+ attempt to use them if you are developing user callouts.
+
+ @subsubsection hooksmgServerHooks Server Hooks
+
+ The singleton @ref isc::hooks::ServerHooks object is used to register
+ hooks. It is little more than a wrapper around a map of (hook index,
+ hook name), generating a unique number (the hook index) for each
+ hook registered. It also handles the registration of the pre-defined
+ context_create and context_destroy hooks.
+
+ In operation, the @ref hooksmgHooksManager provides a thin wrapper
+ around it, so that the BIND 10 component developer does not have to
+ worry about another object.
+
+ @subsubsection hooksmgLibraryManager Library Manager
+
+ An @ref isc::hooks::LibraryManager is created by the @ref
+ hooksmgHooksManager object for each shared library loaded. It
+ controls the loading and unloading of the library and in essence
+ represents the library in the hooks framework. It also handles the
+ registration of the standard callouts (functions in the library with
+ the same name as the hook name).
+
+ Of particular importance is the "library's index", a number associated
+ with the library. This is passed to the LibraryManager at creation
+ time and is used to tag the callout pointers. It is discussed
+ further below.
+
+ As the LibraryManager provides all the methods needed to manage the
+ shared library, it is the natural home for the static validateLibrary()
+ method. The function called the parsing of the BIND 10 configuration, when
+ the "hooks-libraries" element is processed. It checks that shared library
+ exists, that it can be opened, that it contains the "version()" function
+ and that that function returns a valid value. It then closes the shared
+ library and returns an appropriate indication as to the library status.
+
+ @subsubsection hooksmgLibraryManagerCollection Library Manager Collection
+
+ The hooks framework can handle multiple libraries and as
+ a result will create a @ref hooksmgLibraryManager for each
+ of them. The collection of LibraryManagers is managed by the
+ @ref isc::hooks::LibraryManagerCollection object which, in most
+ cases has a method corresponding to a @ref hooksmgLibraryManager
+ method, e.g. it has a loadLibraries() that corresponds to the @ref
+ hooksmgLibraryManager's loadLibrary() call. As would be expected, methods
+ on the LibraryManagerCollection iterate through all specified libraries,
+ calling the corresponding LibraryManager method for each library.
+
+ One point of note is that LibraryManagerCollection operates on an "all
+ or none" principle. When loadLibraries() is called, on exit either all
+ libraries have been successfully opened or none of them have. There
+ is no use-case in BIND 10 where, after a user has specified the shared
+ libraries they want to load, the system will operate with only some of
+ them loaded.
+
+ The LibraryManagerCollection is the place where each library's index is set.
+ Each library is assigned a number ranging from 1 through to the number
+ of libraries being loaded. As mentioned in the previous section, this
+ index is used to tag callout pointers, something that is discussed
+ in the next section.
+
+ (Whilst on the subject of library index numbers, two additional
+ numbers - 0 and INT_MAX - are also valid as "current library index".
+ For flexibility, the BIND 10 component is able to register its own
+ functions as hook callouts. It does this by obtaining a suitable @ref
+ hooksmgLibraryHandle from the @ref hooksmgHooksManager. A choice
+ of two is available: one @ref hooksmgLibraryHandle (with an index
+ of 0) can be used to register a callout on a hook to execute before
+ any user-supplied callouts. The second (with an index of INT_MAX)
+ is used to register a callout to run after user-specified callouts.
+ Apart from the index number, the hooks framework does not treat these
+ callouts any differently from user-supplied ones.)
+
+ @subsubsection hooksmgCalloutManager Callout Manager
+
+ The @ref isc::hooks::CalloutManager is the core of the framework insofar
+ as the registration and calling of callouts is concerned.
+
+ It maintains a "hook vector" - a vector with one element for
+ each registered hook. Each element in this vector is itself a
+ vector (the callout vector), each element of which is a pair of
+ (library index, callback pointer). When a callout is registered, the
+ CalloutManager's current library index is used to supply the "library
+ index" part of the pair. The library index is set explicitly by the
+ @ref hooksmgLibraryManager prior to calling the user library's load()
+ function (and prior to registering the standard callbacks).
+
+ The situation is slightly more complex when a callout is executing. In
+ order to execute a callout, the CalloutManager's callCallouts()
+ method must be called. This iterates through the callout vector for
+ a hook and for each element in the vector, uses the "library index"
+ part of the pair to set the "current library index" before calling the
+ callout function recorded in the second part of the pair. In most cases,
+ the setting of the library index has no effect on the callout. However,
+ if the callout wishes to dynamically register or deregister a callout,
+ the @ref hooksmgLibraryHandle (see above) calls a method on the
+ @ref hooksmgCalloutManager which in turn uses that information.
+
+ @subsubsection hooksmgHooksManager Hooks Manager
+
+ The @ref isc::hooks::HooksManager is the main object insofar as the
+ server is concerned. It controls the creation of the library-related
+ objects and provides the framework in which they interact. It also
+ provides a shell around objects such as @ref hooksmgServerHooks so that all
+ interaction with the hooks framework by the server is through the
+ HooksManager object. Apart from this, it supplies no functionality to
+ the hooks framework.
+
+ @section hooksmgOtherIssues Other Issues
+
+ @subsection hooksmgMemoryAllocation Memory Allocation
+
+ Unloading a shared library works by unmapping the part of the process's
+ virtual address space in which the library lies. This may lead to
+ problems if there are still references to that address space elsewhere
+ in the process.
+
+ In many operating systems, heap storage allowed by a shared library
+ will lie in the virtual address allocated to the library. This has
+ implications in the hooks framework because:
+
+ - Argument information stored in a @ref hooksmgCalloutHandle by a
+ callout in a library may lie in the library's address space.
+
+ - Data modified in objects passed as arguments may lie in the address
+ space. For example, it is common for a DHCP callout to add "options"
+ to a packet: the memory allocated for those options will most likely
+ lie in library address space.
+
+ The problem really arises because of the extensive use by BIND 10 of
+ boost smart pointers. When the pointer is destroyed, the pointed-to
+ memory is deallocated. If the pointer points to address space that is
+ unmapped because a library has been unloaded, the deletion causes a
+ segmentation fault.
+
+ The hooks framework addresses the issue for the @ref hooksmgCalloutHandle
+ by keeping in that object a shared pointer to the object controlling
+ library unloading (the @ref hooksmgLibraryManagerCollection). Although
+ the libraries can be unloaded at any time, it is only when every
+ @ref hooksmgCalloutHandle that could possibly reference address space in the
+ library have been deleted that the library will actually be unloaded
+ and the address space unmapped.
+
+ The hooks framework cannot solve the second issue as the objects in
+ question are under control of the BIND 10 server incorporating the
+ hooks. It is up to the server developer to ensure that all such objects
+ have been destroyed before libraries are reloaded. In extreme cases
+ this may mean the server suspending all processing of incoming requests
+ until all currently executing requests have completed and data object
+ destroyed, reloading the libraries, then resuming processing.
+
+ @subsection hooksmgStaticLinking Hooks and Statically-Linked BIND 10
+
+ BIND 10 has the configuration option to allow static linking. What this
+ means is that it links against the static BIND 10 libraries and not
+ the shareable ones - although it links against the shareable system
+ libraries like "libc" and "libstdc++" and well as the sharable libraries
+ for third-party packages such as log4cplus and MySql.
+
+ Static linking poses a problem for dynamically-loaded hooks libraries
+ as some of the code in them - in particular the hooks framework and
+ the logging code - depend on global objects created within the BIND
+ 10 libraries. In the normal course of events (BIND 10 linked against
+ shared libraries), when BIND 10 is run and the operating system loads
+ a BIND 10 shared library containing a global object, address space
+ is assigned for it. When the hooks framework loads a user-library
+ linked against the same BIND 10 shared library, the operating system
+ recognises that the library is already loaded (and initialized) and
+ uses its definition of the global object. Thus both the code in the
+ BIND 10 image and the code in the user-written shared library
+ reference the same object.
+
+ If BIND 10 is statically linked, the linker allocates address space
+ in the BIND 10 image for the global object and does not include any
+ reference to the shared library containing it. When BIND 10 now loads
+ the user-written shared library - and so loads the BIND 10 library code
+ containing the global object - the operating system does not know that
+ the object already exists. Instead, it allocates new address space.
+ The version of BIND 10 in memory therefore has two copies of the object:
+ one referenced by code in the BIND 10 image, and one referenced by code
+ in the user-written hooks library. This causes problems - information
+ put in one copy is not available to the other.
+
+ Particular problems were encountered with global objects the hooks library
+ and in the logging library, so some code to alleviate the problem has been
+ included.
+
+ The issue in the hooks library is the singleton @ref
+ isc::hooks::ServerHooks object, used by the user-written hooks library
+ if it attempts to register or deregister callouts. The contents of the
+ singleton - the names of the hook points and their index - are set by
+ the relevant BIND 10 server; this information is not available in the
+ singleton created in the user's hooks library.
+
+ Within the code users by the user's hooks library, the ServerHooks
+ object is used by @ref isc::hooks::CalloutHandle and @ref
+ isc::hooks::CalloutManager objects. Both these objects are passed to the
+ hooks library code when a callout is called: the former directly through
+ the callout argument list, the latter indirectly as a pointer to it is
+ stored in the CalloutHandle. This allows a solution to the problem:
+ instead of accessing the singleton via ServerHooks::getServerHooks(),
+ the constructors of these objects store a reference to the singleton
+ ServerHooks when they are created and use that reference to access
+ ServerHooks data. Since both CalloutHandle and CalloutManager are
+ created in the statically-linked BIND 10 server, use of the reference
+ means that it is the singleton within the server - and not the one
+ within the user's hooks library - that is referenced.
+
+ The solution of the logging problem is not so straightforward. Within
+ BIND 10, there are two logging components, the BIND 10 logging framework
+ and the log4cplus libraries. Owing to static linking, there are two
+ instances of the former; but as static linking uses shared libraries of
+ third-party products, there is one instance of the latter. What further
+ complicates matters is that initialization of the logging framework is
+ in two parts: static initialization and run-time initialization.
+
+ The logging initialization comprises the following:
+
+ -# Static initialization of the log4cplus global variables.
+ -# Static initialization of messages in the various BIND 10 libraries.
+ -# Static initialization of logging framework.
+ -# Run-time initialization of the logging framework.
+ -# Run-time initialization of log4cplus
+
+ As both the BIND 10 server and the user-written hooks libraries use the
+ log4cplus shared library, item 1 - the static initialization of the log4cplus
+ global variables is performed once.
+
+ The next two tasks - static initialization of the messages in the BIND
+ 10 libraries and the static initialization of the logging framework -
+ are performed twice, once in the context of the BIND 10 server and
+ once in the context of the hooks library. For this reason, run-time
+ initialization of the logging framework needs to be performed twice,
+ once in the context of the BIND 10 server and once in the context of the
+ user-written hooks library. However, the standard logging framework
+ initialization code also performs the last task, initialization of
+ log4cplus, something that causes problems if executed more than once.
+
+ To get round this, the function isc::hooks::hooksStaticLinkInit()
+ has been written. It executes the only part of the logging framework
+ run-time initialization that actually pertains to the logging framework
+ and not log4cplus, namely loading the message dictionary with the
+ statically-initialized messages in the BIND 10 libraries.
+ This should be executed by any hooks library linking against a statically
+ initialized BIND 10. (In fact, running it against a dynamically-linked
+ BIND 10 should have no effect, as the load operation discards any duplicate
+ message entries.) The hooks library tests do this, the code being
+ copnditionally compiled within a test of the USE_STATIC_LINK macro, set
+ by the configure script.
+
+ @note Not everything is completely rosy with logging and static linking.
+ In particular, there appears to be an issue with the scenario where a
+ user-written hooks library is run by a statically-linked BIND 10 and then
+ unloaded. As far as can be determined, on unload the system attempts to
+ delete the same logger twice. This is alleviated by explictly clearing
+ the loggerptr_ variable in the isc::log::Logger destructor, but there
+ is a suspicion that some memory might be lost in these circumstances.
+ This is still under investigation.
+*/
diff --git a/src/lib/hooks/hooks_manager.cc b/src/lib/hooks/hooks_manager.cc
new file mode 100644
index 0000000..117db61
--- /dev/null
+++ b/src/lib/hooks/hooks_manager.cc
@@ -0,0 +1,200 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/library_manager_collection.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/server_hooks.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <string>
+#include <vector>
+
+using namespace std;
+
+namespace isc {
+namespace hooks {
+
+// Constructor
+
+HooksManager::HooksManager() {
+}
+
+// Return reference to singleton hooks manager.
+
+HooksManager&
+HooksManager::getHooksManager() {
+ static HooksManager manager;
+ return (manager);
+}
+
+// Are callouts present?
+
+bool
+HooksManager::calloutsPresentInternal(int index) {
+ conditionallyInitialize();
+ return (callout_manager_->calloutsPresent(index));
+}
+
+bool
+HooksManager::calloutsPresent(int index) {
+ return (getHooksManager().calloutsPresentInternal(index));
+}
+
+// Call the callouts
+
+void
+HooksManager::callCalloutsInternal(int index, CalloutHandle& handle) {
+ conditionallyInitialize();
+ return (callout_manager_->callCallouts(index, handle));
+}
+
+void
+HooksManager::callCallouts(int index, CalloutHandle& handle) {
+ return (getHooksManager().callCalloutsInternal(index, handle));
+}
+
+// Load the libraries. This will delete the previously-loaded libraries
+// (if present) and load new ones.
+
+bool
+HooksManager::loadLibrariesInternal(const std::vector<std::string>& libraries) {
+ // Unload current set of libraries (if any are loaded).
+ unloadLibrariesInternal();
+
+ // Create the library manager and load the libraries.
+ lm_collection_.reset(new LibraryManagerCollection(libraries));
+ bool status = lm_collection_->loadLibraries();
+
+ if (status) {
+ // ... and obtain the callout manager for them if successful.
+ callout_manager_ = lm_collection_->getCalloutManager();
+ } else {
+ // Unable to load libraries, reset to state before this function was
+ // called.
+ lm_collection_.reset();
+ callout_manager_.reset();
+ }
+
+ return (status);
+}
+
+bool
+HooksManager::loadLibraries(const std::vector<std::string>& libraries) {
+ return (getHooksManager().loadLibrariesInternal(libraries));
+}
+
+// Unload the libraries. This just deletes all internal objects which will
+// cause the libraries to be unloaded.
+
+void
+HooksManager::unloadLibrariesInternal() {
+ // The order of deletion does not matter here, as each library manager
+ // holds its own pointer to the callout manager. However, we may as
+ // well delete the library managers first: if there are no other references
+ // to the callout manager, the second statement will delete it, which may
+ // ease debugging.
+ lm_collection_.reset();
+ callout_manager_.reset();
+}
+
+void HooksManager::unloadLibraries() {
+ getHooksManager().unloadLibrariesInternal();
+}
+
+// Create a callout handle
+
+boost::shared_ptr<CalloutHandle>
+HooksManager::createCalloutHandleInternal() {
+ conditionallyInitialize();
+ return (boost::shared_ptr<CalloutHandle>(
+ new CalloutHandle(callout_manager_, lm_collection_)));
+}
+
+boost::shared_ptr<CalloutHandle>
+HooksManager::createCalloutHandle() {
+ return (getHooksManager().createCalloutHandleInternal());
+}
+
+// Get the list of the names of loaded libraries.
+
+std::vector<std::string>
+HooksManager::getLibraryNamesInternal() const {
+ return (lm_collection_ ? lm_collection_->getLibraryNames()
+ : std::vector<std::string>());
+}
+
+std::vector<std::string>
+HooksManager::getLibraryNames() {
+ return (getHooksManager().getLibraryNamesInternal());
+}
+
+// Perform conditional initialization if nothing is loaded.
+
+void
+HooksManager::performConditionalInitialization() {
+
+ // Nothing present, so create the collection with any empty set of
+ // libraries, and get the CalloutManager.
+ vector<string> libraries;
+ lm_collection_.reset(new LibraryManagerCollection(libraries));
+ lm_collection_->loadLibraries();
+
+ callout_manager_ = lm_collection_->getCalloutManager();
+}
+
+// Shell around ServerHooks::registerHook()
+
+int
+HooksManager::registerHook(const std::string& name) {
+ return (ServerHooks::getServerHooks().registerHook(name));
+}
+
+// Return pre- and post- library handles.
+
+isc::hooks::LibraryHandle&
+HooksManager::preCalloutsLibraryHandleInternal() {
+ conditionallyInitialize();
+ return (callout_manager_->getPreLibraryHandle());
+}
+
+isc::hooks::LibraryHandle&
+HooksManager::preCalloutsLibraryHandle() {
+ return (getHooksManager().preCalloutsLibraryHandleInternal());
+}
+
+isc::hooks::LibraryHandle&
+HooksManager::postCalloutsLibraryHandleInternal() {
+ conditionallyInitialize();
+ return (callout_manager_->getPostLibraryHandle());
+}
+
+isc::hooks::LibraryHandle&
+HooksManager::postCalloutsLibraryHandle() {
+ return (getHooksManager().postCalloutsLibraryHandleInternal());
+}
+
+// Validate libraries
+
+std::vector<std::string>
+HooksManager::validateLibraries(const std::vector<std::string>& libraries) {
+ return (LibraryManagerCollection::validateLibraries(libraries));
+}
+
+} // namespace util
+} // namespace isc
diff --git a/src/lib/hooks/hooks_manager.h b/src/lib/hooks/hooks_manager.h
new file mode 100644
index 0000000..53a2525
--- /dev/null
+++ b/src/lib/hooks/hooks_manager.h
@@ -0,0 +1,316 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef HOOKS_MANAGER_H
+#define HOOKS_MANAGER_H
+
+#include <hooks/server_hooks.h>
+
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace hooks {
+
+// Forward declarations
+class CalloutHandle;
+class CalloutManager;
+class LibraryHandle;
+class LibraryManagerCollection;
+
+/// @brief Hooks Manager
+///
+/// This is the overall manager of the hooks framework and is the main class
+/// used by a BIND 10 module when handling hooks. It is responsible for the
+/// loading and unloading of user libraries, and for calling the callouts on
+/// each hook point.
+///
+/// The class is a singleton, the single instance of the object being accessed
+/// through the static getHooksManager() method.
+
+class HooksManager : boost::noncopyable {
+public:
+ /// @brief Get singleton hooks manager
+ ///
+ /// @return Reference to the singleton hooks manager.
+ static HooksManager& getHooksManager();
+
+ /// @brief Load and reload libraries
+ ///
+ /// Loads the list of libraries into the server address space. For each
+ /// library, the "standard" functions (ones with the same names as the
+ /// hook points) are configured and the libraries' "load" function
+ /// called.
+ ///
+ /// If libraries are already loaded, they are unloaded and the new
+ /// libraries loaded.
+ ///
+ /// If any library fails to load, an error message will be logged. The
+ /// remaining libraries will be loaded if possible.
+ ///
+ /// @param libraries List of libraries to be loaded. The order is
+ /// important, as it determines the order that callouts on the same
+ /// hook will be called.
+ ///
+ /// @return true if all libraries loaded without a problem, false if one or
+ /// more libraries failed to load. In the latter case, message will
+ /// be logged that give the reason.
+ static bool loadLibraries(const std::vector<std::string>& libraries);
+
+ /// @brief Unload libraries
+ ///
+ /// Unloads the loaded libraries and leaves the hooks subsystem in the
+ /// state it was after construction but before loadLibraries() is called.
+ ///
+ /// @note: This method should be used with caution - see the notes for
+ /// the class LibraryManager for pitfalls. In general, a server
+ /// should not call this method: library unloading will automatically
+ /// take place when new libraries are loaded, and when appropriate
+ /// objects are destroyed.
+ ///
+ /// @return true if all libraries unloaded successfully, false on an error.
+ /// In the latter case, an error message will have been output.
+ static void unloadLibraries();
+
+ /// @brief Are callouts present?
+ ///
+ /// Checks loaded libraries and returns true if at lease one callout
+ /// has been registered by them for the given hook.
+ ///
+ /// @param index Hooks index for which callouts are checked.
+ ///
+ /// @return true if callouts are present, false if not.
+ /// @throw NoSuchHook Given index does not correspond to a valid hook.
+ static bool calloutsPresent(int index);
+
+ /// @brief Calls the callouts for a given hook
+ ///
+ /// Iterates through the libray handles and calls the callouts associated
+ /// with the given hook index.
+ ///
+ /// @note This method invalidates the current library index set with
+ /// setLibraryIndex().
+ ///
+ /// @param index Index of the hook to call.
+ /// @param handle Reference to the CalloutHandle object for the current
+ /// object being processed.
+ static void callCallouts(int index, CalloutHandle& handle);
+
+ /// @brief Return pre-callouts library handle
+ ///
+ /// Returns a library handle that can be used by the server to register
+ /// callouts on a hook that are called _before_ any callouts belonging
+ /// to a library.
+ ///
+ /// @note Both the reference returned and the callouts registered with
+ /// this handle only remain valid until the next loadLibraries() or
+ /// unloadLibraries() call. If the callouts are to remain registered
+ /// after this time, a new handle will need to be obtained and the
+ /// callouts re-registered.
+ ///
+ /// @return Reference to library handle associated with pre-library callout
+ /// registration.
+ static LibraryHandle& preCalloutsLibraryHandle();
+
+ /// @brief Return post-callouts library handle
+ ///
+ /// Returns a library handle that can be used by the server to register
+ /// callouts on a hook that are called _after any callouts belonging
+ /// to a library.
+ ///
+ /// @note Both the reference returned and the callouts registered with
+ /// this handle only remain valid until the next loadLibraries() or
+ /// unloadLibraries() call. If the callouts are to remain registered
+ /// after this time, a new handle will need to be obtained and the
+ /// callouts re-registered.
+ ///
+ /// @return Reference to library handle associated with post-library callout
+ /// registration.
+ static LibraryHandle& postCalloutsLibraryHandle();
+
+ /// @brief Return callout handle
+ ///
+ /// Returns a callout handle to be associated with a request passed round
+ /// the system.
+ ///
+ /// @note This handle is valid only after a loadLibraries() call and then
+ /// only up to the next loadLibraries() call.
+ ///
+ /// @return Shared pointer to a CalloutHandle object.
+ static boost::shared_ptr<CalloutHandle> createCalloutHandle();
+
+ /// @brief Register Hook
+ ///
+ /// This is just a convenience shell around the ServerHooks::registerHook()
+ /// method. It - along with the definitions of the two hook indexes for
+ /// the context_create and context_destroy methods - means that server
+ /// authors only need to deal with HooksManager and CalloutHandle, and not
+ /// include any other hooks framework classes.
+ ///
+ /// @param name Name of the hook
+ ///
+ /// @return Index of the hook, to be used in subsequent hook-related calls.
+ /// This will be greater than or equal to zero (so allowing a
+ /// negative value to indicate an invalid index).
+ ///
+ /// @throws DuplicateHook A hook with the same name has already been
+ /// registered.
+ static int registerHook(const std::string& name);
+
+ /// @brief Return list of loaded libraries
+ ///
+ /// Returns the names of the loaded libraries.
+ ///
+ /// @return List of loaded library names.
+ static std::vector<std::string> getLibraryNames();
+
+ /// @brief Validate library list
+ ///
+ /// For each library passed to it, checks that the library can be opened
+ /// and that the "version" function is present and gives the right answer.
+ /// Each library is closed afterwards.
+ ///
+ /// This is used during the configuration parsing - when the list of hooks
+ /// libraries is changed, each of the new libraries is checked before the
+ /// change is committed.
+ ///
+ /// @param List of libraries to be validated.
+ ///
+ /// @return An empty vector if all libraries validated. Otherwise it
+ /// holds the names of the libraries that failed validation.
+ static std::vector<std::string> validateLibraries(
+ const std::vector<std::string>& libraries);
+
+ /// Index numbers for pre-defined hooks.
+ static const int CONTEXT_CREATE = ServerHooks::CONTEXT_CREATE;
+ static const int CONTEXT_DESTROY = ServerHooks::CONTEXT_DESTROY;
+
+private:
+
+ /// @brief Constructor
+ ///
+ /// This is private as the object is a singleton and can only be addessed
+ /// through the getHooksManager() static method.
+ HooksManager();
+
+ //@{
+ /// The following methods correspond to similarly-named static methods,
+ /// but actually do the work on the singleton instance of the HooksManager.
+ /// See the descriptions of the static methods for more details.
+
+ /// @brief Validate library list
+ ///
+ /// @param List of libraries to be validated.
+ ///
+ /// @return An empty string if all libraries validated. Otherwise it is
+ /// the name of the first library that failed validation. The
+ /// configuration code can return this to bindctl as an indication
+ /// of the problem.
+ std::string validateLibrariesInternal(
+ const std::vector<std::string>& libraries) const;
+
+ /// @brief Load and reload libraries
+ ///
+ /// @param libraries List of libraries to be loaded. The order is
+ /// important, as it determines the order that callouts on the same
+ /// hook will be called.
+ ///
+ /// @return true if all libraries loaded without a problem, false if one or
+ /// more libraries failed to load. In the latter case, message will
+ /// be logged that give the reason.
+ bool loadLibrariesInternal(const std::vector<std::string>& libraries);
+
+ /// @brief Unload libraries
+ void unloadLibrariesInternal();
+
+ /// @brief Are callouts present?
+ ///
+ /// @param index Hooks index for which callouts are checked.
+ ///
+ /// @return true if callouts are present, false if not.
+ /// @throw NoSuchHook Given index does not correspond to a valid hook.
+ bool calloutsPresentInternal(int index);
+
+ /// @brief Calls the callouts for a given hook
+ ///
+ /// @param index Index of the hook to call.
+ /// @param handle Reference to the CalloutHandle object for the current
+ /// object being processed.
+ void callCalloutsInternal(int index, CalloutHandle& handle);
+
+ /// @brief Return callout handle
+ ///
+ /// @return Shared pointer to a CalloutHandle object.
+ boost::shared_ptr<CalloutHandle> createCalloutHandleInternal();
+
+ /// @brief Return pre-callouts library handle
+ ///
+ /// @return Reference to library handle associated with pre-library callout
+ /// registration.
+ LibraryHandle& preCalloutsLibraryHandleInternal();
+
+ /// @brief Return post-callouts library handle
+ ///
+ /// @return Reference to library handle associated with post-library callout
+ /// registration.
+ LibraryHandle& postCalloutsLibraryHandleInternal();
+
+ /// @brief Return list of loaded libraries
+ ///
+ /// @return List of loaded library names.
+ std::vector<std::string> getLibraryNamesInternal() const;
+
+ //@}
+
+ /// @brief Initialization to No Libraries
+ ///
+ /// Initializes the hooks manager with an "empty set" of libraries. This
+ /// method is called if conditionallyInitialize() determines that such
+ /// initialization is needed.
+ void performConditionalInitialization();
+
+ /// @brief Conditional initialization of the hooks manager
+ ///
+ /// loadLibraries() performs the initialization of the HooksManager,
+ /// setting up the internal structures and loading libraries. However,
+ /// in some cases, server authors may not do that. This method is called
+ /// whenever any hooks execution function is invoked (checking callouts,
+ /// calling callouts or returning a callout handle). If the HooksManager
+ /// is unitialised, it will initialize it with an "empty set" of libraries.
+ ///
+ /// For speed, the test of whether initialization is required is done
+ /// in-line here. The actual initialization is performed in
+ /// performConditionalInitialization().
+ void conditionallyInitialize() {
+ if (!lm_collection_) {
+ performConditionalInitialization();
+ }
+ }
+
+ // Members
+
+ /// Set of library managers.
+ boost::shared_ptr<LibraryManagerCollection> lm_collection_;
+
+ /// Callout manager for the set of library managers.
+ boost::shared_ptr<CalloutManager> callout_manager_;
+};
+
+} // namespace util
+} // namespace hooks
+
+#endif // HOOKS_MANAGER_H
diff --git a/src/lib/hooks/hooks_messages.mes b/src/lib/hooks/hooks_messages.mes
new file mode 100644
index 0000000..53090ab
--- /dev/null
+++ b/src/lib/hooks/hooks_messages.mes
@@ -0,0 +1,177 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+$NAMESPACE isc::hooks
+
+% HOOKS_ALL_CALLOUTS_DEREGISTERED hook library at index %1 removed all callouts on hook %2
+A debug message issued when all callouts on the specified hook registered
+by the library with the given index were removed. This is similar to
+the HOOKS_CALLOUTS_REMOVED message (and the two are likely to be seen
+together), but is issued at a lower-level in the hook framework.
+
+% HOOKS_CALLOUTS_REMOVED callouts removed from hook %1 for library %2
+This is a debug message issued during library unloading. It notes that
+one of more callouts registered by that library have been removed from
+the specified hook. This is similar to the HOOKS_DEREGISTER_ALL_CALLOUTS
+message (and the two are likely to be seen together), but is issued at a
+higher-level in the hook framework.
+
+% HOOKS_CALLOUT_CALLED hooks library with index %1 has called a callout on hook %2 that has address %3
+Only output at a high debugging level, this message indicates that
+a callout on the named hook registered by the library with the given
+index (in the list of loaded libraries) has been called and returned a
+success state. The address of the callout is given in the message
+
+% HOOKS_CALLOUT_DEREGISTERED hook library at index %1 deregistered a callout on hook %2
+A debug message issued when all instances of a particular callouts on
+the hook identified in the message that were registered by the library
+with the given index have been removed.
+
+% HOOKS_CALLOUT_ERROR error returned by callout on hook %1 registered by library with index %2 (callout address %3)
+If a callout returns an error status when called, this error message
+is issued. It identifies the hook to which the callout is attached, the
+index of the library (in the list of loaded libraries) that registered
+it and the address of the callout. The error is otherwise ignored.
+
+% HOOKS_CALLOUT_EXCEPTION exception thrown by callout on hook %1 registered by library with index %2 (callout address %3): %4
+If a callout throws an exception when called, this error message is
+issued. It identifies the hook to which the callout is attached, the
+index of the library (in the list of loaded libraries) that registered
+it and the address of the callout. The error is otherwise ignored.
+
+% HOOKS_CALLOUT_REGISTRATION hooks library with index %1 registering callout for hook '%2'
+This is a debug message, output when a library (whose index in the list
+of libraries (being) loaded is given) registers a callout.
+
+% HOOKS_CLOSE_ERROR failed to close hook library %1: %2
+BIND 10 has failed to close the named hook library for the stated reason.
+Although this is an error, this should not affect the running system
+other than as a loss of resources. If this error persists, you should
+restart BIND 10.
+
+% HOOKS_HOOK_LIST_RESET the list of hooks has been reset
+This is a message indicating that the list of hooks has been reset.
+While this is usual when running the BIND 10 test suite, it should not be
+seen when running BIND 10 in a producion environment. If this appears,
+please report a bug through the usual channels.
+
+% HOOKS_INCORRECT_VERSION hook library %1 is at version %2, require version %3
+BIND 10 has detected that the named hook library has been built against
+a version of BIND 10 that is incompatible with the version of BIND 10
+running on your system. It has not loaded the library.
+
+This is most likely due to the installation of a new version of BIND 10
+without rebuilding the hook library. A rebuild and re-install of the
+library should fix the problem in most cases.
+
+% HOOKS_LIBRARY_LOADED hooks library %1 successfully loaded
+This information message is issued when a user-supplied hooks library
+has been successfully loaded.
+
+% HOOKS_LIBRARY_LOADING loading hooks library %1
+This is a debug message output just before the specified library is loaded.
+If the action is successfully, it will be followed by the
+HOOKS_LIBRARY_LOADED informational message.
+
+% HOOKS_LIBRARY_UNLOADED hooks library %1 successfully unloaded
+This information message is issued when a user-supplied hooks library
+has been successfully unloaded.
+
+% HOOKS_LIBRARY_UNLOADING unloading library %1
+This is a debug message called when the specified library is
+being unloaded. If all is successful, it will be followed by the
+HOOKS_LIBRARY_UNLOADED informational message.
+
+% HOOKS_LIBRARY_VERSION hooks library %1 reports its version as %2
+A debug message issued when the version check on the hooks library
+has succeeded.
+
+% HOOKS_LOAD_ERROR 'load' function in hook library %1 returned error %2
+A "load" function was found in the library named in the message and
+was called. The function returned a non-zero status (also given in
+the message) which was interpreted as an error. The library has been
+unloaded and no callouts from it will be installed.
+
+% HOOKS_LOAD_EXCEPTION 'load' function in hook library %1 threw an exception
+A "load" function was found in the library named in the message and
+was called. The function threw an exception (an error indication)
+during execution, which is an error condition. The library has been
+unloaded and no callouts from it will be installed.
+
+% HOOKS_LOAD_FRAMEWORK_EXCEPTION 'load' function in hook library %1 threw an exception: reason %2
+A "load" function was found in the library named in the message and
+was called. Either the hooks framework or the function threw an
+exception (an error indication) during execution, which is an error
+condition; the cause of the exception is recorded in the message.
+The library has been unloaded and no callouts from it will be
+installed.
+
+% HOOKS_LOAD_SUCCESS 'load' function in hook library %1 returned success
+This is a debug message issued when the "load" function has been found
+in a hook library and has been successfully called.
+
+% HOOKS_NO_LOAD no 'load' function found in hook library %1
+This is a debug message saying that the specified library was loaded
+but no function called "load" was found in it. Providing the library
+contained some "standard" functions (i.e. functions with the names of
+the hooks for the given server), this is not an issue.
+
+% HOOKS_NO_UNLOAD no 'unload' function found in hook library %1
+This is a debug message issued when the library is being unloaded.
+It merely states that the library did not contain an "unload" function.
+
+% HOOKS_NO_VERSION no 'version' function found in hook library %1
+The shared library named in the message was found and successfully loaded,
+but BIND 10 did not find a function named "version" in it. This function
+is required and should return the version of BIND 10 against which the
+library was built. The value is used to check that the library was built
+against a compatible version of BIND 10. The library has not been loaded.
+
+% HOOKS_OPEN_ERROR failed to open hook library %1: %2
+BIND 10 failed to open the specified hook library for the stated
+reason. The library has not been loaded. BIND 10 will continue to
+function, but without the services offered by the library.
+
+% HOOKS_STD_CALLOUT_REGISTERED hooks library %1 registered standard callout for hook %2 at address %3
+This is a debug message, output when the library loading function has
+located a standard callout (a callout with the same name as a hook point)
+and registered it. The address of the callout is indicated.
+
+% HOOKS_UNLOAD_ERROR 'unload' function in hook library %1 returned error %2
+During the unloading of a library, an "unload" function was found.
+It was called, but returned an error (non-zero) status, resulting in
+the issuing of this message. The unload process continued after this
+message and the library has been unloaded.
+
+% HOOKS_UNLOAD_EXCEPTION 'unload' function in hook library %1 threw an exception
+During the unloading of a library, an "unload" function was found. It was
+called, but in the process generated an exception (an error indication).
+The unload process continued after this message and the library has
+been unloaded.
+
+% HOOKS_UNLOAD_FRAMEWORK_EXCEPTION 'unload' function in hook library %1 threw an exception, reason %2
+During the unloading of a library, an "unload" function was found.
+It was called, but in the process either it or the hooks framework
+generated an exception (an error indication); the cause of the error
+is recorded in the message. The unload process continued after
+this message and the library has been unloaded.
+
+% HOOKS_UNLOAD_SUCCESS 'unload' function in hook library %1 returned success
+This is a debug message issued when an "unload" function has been found
+in a hook library during the unload process, called, and returned success.
+
+% HOOKS_VERSION_EXCEPTION 'version' function in hook library %1 threw an exception
+This error message is issued if the version() function in the specified
+hooks library was called and generated an exception. The library is
+considered unusable and will not be loaded.
diff --git a/src/lib/hooks/hooks_user.dox b/src/lib/hooks/hooks_user.dox
new file mode 100644
index 0000000..9c56861
--- /dev/null
+++ b/src/lib/hooks/hooks_user.dox
@@ -0,0 +1,1075 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Note: the prefix "hooksdg" to all labels is an abbreviation for "Hooks
+// Developer's Guide" and is used to prevent a clash with symbols in any
+// other Doxygen file.
+
+/**
+ @page hooksdgDevelopersGuide Hooks Developer's Guide
+
+ @section hooksdgIntroduction Introduction
+
+Although the BIND 10 framework and its associated DNS and DHCP programs
+provide comprehensive functionality, there will be times when it does
+not quite do what you require: the processing has to be extended in some
+way to solve your problem.
+
+Since the BIND 10 source code is freely available (BIND 10 being an
+open-source project), one option is to modify it to do what
+you want. Whilst perfectly feasible, there are drawbacks:
+
+- Although well-documented, BIND 10 is a large program. Just
+understanding how it works will take a significant amount of time. In
+addition, despite the fact that its object-oriented design keeps the
+coupling between modules to a minimum, an inappropriate change to one
+part of the program during the extension could cause another to
+behave oddly or to stop working altogether.
+
+- The change may need to be re-applied or re-written with every new
+version of BIND 10. As new functionality is added or bugs are fixed,
+the code or algorithms in the core software may change - and may change
+significantly.
+
+To overcome these problems, BIND 10 provides the "Hooks" interface -
+a defined interface for third-party or user-written code. (For ease of
+reference in the rest of this document, all such code will be referred
+to as "user code".) At specific points in its processing
+("hook points") BIND 10 will make a call to this code. The call passes
+data that the user code can examine and, if required, modify.
+BIND 10 uses the modified data in the remainder of its processing.
+
+In order to minimise the interaction between BIND 10 and the user
+code, the latter is built independently of BIND 10 in the form of
+a shared library (or libraries). These are made known to BIND 10
+through its configuration mechanism, and BIND 10 loads the library at
+run time. Libraries can be unloaded and reloaded as needed while BIND
+10 is running.
+
+Use of a defined API and the BIND 10 configuration mechanism means that
+as new versions of BIND 10 are released, there is no need to modify
+the user code. Unless there is a major change in an interface
+(which will be clearly documented), all that will be required is a rebuild
+of the libraries.
+
+ at note Although the defined interface should not change, the internals
+of some of the classes and structures referenced by the user code may
+change between versions of BIND 10. These changes have to be reflected
+in the compiled version of the software, hence the need for a rebuild.
+
+ at subsection hooksdgLanguages Languages
+
+The core of BIND 10 is written in C++. While it is the intention to
+provide interfaces into user code written in other languages, the initial
+versions of the Hooks system requires that user code be written in C++.
+All examples in this guide are in that language.
+
+ at subsection hooksdgTerminology Terminology
+
+In the remainder of this guide, the following terminology is used:
+
+- Hook/Hook Point - used interchageably, this is a point in the code at
+which a call to user functions is made. Each hook has a name and
+each hook can have any number (including 0) of user functions
+attached to it.
+
+- Callout - a user function called by the server at a hook
+point. This is so-named because the server "calls out" to the library
+to execute a user function.
+
+- Framework function - the functions that a user library needs to
+supply in order for the hooks framework to load and unload the library.
+
+- User code/user library - non-BIND 10 code that is compiled into a
+shared library and loaded by BIND 10 into its address space.
+
+
+ at section hooksdgTutorial Tutorial
+
+To illustrate how to write code that integrates with BIND 10, we will
+use the following (rather contrived) example:
+
+The BIND 10 DHCPv4 server is used to allocate IPv4 addresses to clients
+(as well as to pass them other information such as the address of DNS
+servers). We will suppose that we need to classify clients requesting
+IPv4 addresses according to their hardware address, and want to log both
+the hardware address and allocated IP address for the clients of interest.
+
+The following sections describe how to implement these requirements.
+The code presented here is not efficient and there are better ways of
+doing the task. The aim however, is to illustrate the main features of
+user hook code not to provide an optimal solution.
+
+
+ at subsection hooksdgFrameworkFunctions Framework Functions
+
+Loading and initializing a library holding user code makes use
+of three (user-supplied) functions:
+
+- version - defines the version of BIND 10 code with which the user-library
+is built
+- load - called when the library is loaded by the server.
+- unload - called when the library is unloaded by the server.
+
+Of these, only "version" is mandatory, although in our example, all three
+are used.
+
+ at subsubsection hooksdgVersionFunction The "version" Function
+
+"version" is used by the hooks framework to check that the libraries
+it is loading are compatible with the version of BIND 10 being run.
+Although the hooks system allows BIND 10 and user code to interface
+through a defined API, the relationship is somewhat tight in that the
+user code will depend on the internal structures of BIND 10. If these
+change - as they can between BIND 10 releases - and BIND 10 is run with
+a version of user code built against an earlier version of BIND
+10, a program crash could result.
+
+To guard against this, the "version" function must be provided in every
+library. It returns a constant defined in header files of the version
+of BIND 10 against which it was built. The hooks framework checks this
+for compatibility with the running version of BIND 10 before loading
+the library.
+
+In this tutorial, we'll put "version" in its own file, version.cc. The
+contents are:
+
+ at code
+// version.cc
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+int version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+}
+ at endcode
+
+The file "hooks/hooks.h" is specified relative to the BIND 10 libraries
+source directory - this is covered later in the section @ref hooksdgBuild.
+It defines the symbol BIND10_HOOKS_VERSION, which has a value that changes
+on every release of BIND 10: this is the value that needs to be returned
+to the hooks framework.
+
+A final point to note is that the definition of "version" is enclosed
+within 'extern "C"' braces. All functions accessed by the hooks
+framework use C linkage, mainly to avoid the name mangling that
+accompanies use of the C++ compiler, but also to avoid issues related
+to namespaces.
+
+ at subsubsection hooksdgLoadUnloadFunctions The "load" and "unload" Functions
+
+As the names suggest, "load" is called when a library is loaded and
+"unload" called when it is unloaded. (It is always guaranteed that
+"load" is called: "unload" may not be called in some circumstances,
+e.g. if the system shuts down abnormally.) These functions are the
+places where any library-wide resources are allocated and deallocated.
+"load" is also the place where any callouts with non-standard names
+(names that are not hook point names) can be registered:
+this is covered further in the section @ref hooksdgCalloutRegistration.
+
+The example does not make any use callouts with non-standard names. However,
+as our design requires that the log file be open while BIND 10 is active
+and the library loaded, we'll open the file in the "load" function and close
+it in "unload".
+
+We create two files, one for the file handle declaration:
+
+ at code
+// library_common.h
+
+#ifndef LIBRARY_COMMON_H
+#define LIBRARY_COMMON_H
+
+#include <fstream>
+
+// "Interesting clients" log file handle declaration.
+extern std::fstream interesting;
+
+#endif // LIBRARY_COMMON_H
+ at endcode
+
+... and one to hold the "load" and "unload" functions:
+
+ at code
+// load_unload.cc
+
+#include <hooks/hooks.h>
+#include "library_common.h"
+
+using namespace isc::hooks;
+
+// "Interesting clients" log file handle definition.
+std::fstream interesting;
+
+extern "C" {
+
+int load(LibraryHandle&) {
+ interesting.open("/data/clients/interesting.log",
+ std::fstream::out | std::fstream::app);
+ return (interesting ? 0 : 1);
+}
+
+int unload() {
+ if (interesting) {
+ interesting.close();
+ }
+ return (0);
+}
+
+}
+ at endcode
+
+Notes:
+- The file handle ("interesting") is declared in a header file and defined
+outside of any function. This means it can be accessed by any function
+within the user library. For convenience, the definition is in the
+load_unload.cc file.
+- "load" is called with a LibraryHandle argument, this being used in
+the registration of functions. As no functions are being registered
+in this example, the argument specification omits the variable name
+(whilst retaining the type) to avoid an "unused variable" compiler
+warning. (The LibraryHandle and its use is discussed in the section
+ at ref hooksdgLibraryHandle.)
+- In the current version of the hooks framework, it is not possible to pass
+any configuration information to the "load" function. The name of the log
+file must therefore be hard-coded as an absolute path name or communicated
+to the user code by some other means.
+- "load" must 0 on success and non-zero on error. The hooks framework
+will abandon the loading of the library if "load" returns an error status.
+(In this example, "interesting" can be tested as a boolean value,
+returning "true" if the file opened successfully.)
+- "unload" closes the log file if it is open and is a no-op otherwise. As
+with "load", a zero value must be returned on success and a non-zero value
+on an error. The hooks framework will record a non-zero status return
+as an error in the current BIND 10 log but otherwise ignore it.
+- As before, the function definitions are enclosed in 'extern "C"' braces.
+
+ at subsection hooksdgCallouts Callouts
+
+Having sorted out the framework, we now come to the functions that
+actually do something. These functions are known as "callouts" because
+the BIND 10 code "calls out" to them. Each BIND 10 server has a number of
+hooks to which callouts can be attached: server-specific documentation
+describes in detail the points in the server at which the hooks are
+present together with the data passed to callouts attached to them.
+
+Before we continue with the example, we'll discuss how arguments are
+passed to callouts and information is returned to the server. We will
+also discuss how information can be moved between callouts.
+
+ at subsubsection hooksdgCalloutSignature The Callout Signature
+
+All callouts are declared with the signature:
+ at code
+extern "C" {
+int callout(CalloutHandle& handle);
+}
+ at endcode
+
+(As before, the callout is declared with "C" linkage.) Information is passed
+between BIND 10 and the callout through name/value pairs in the CalloutHandle
+object. The object is also used to pass information between callouts on a
+per-request basis. (Both of these concepts are explained below.)
+
+A callout returns an "int" as a status return. A value of 0 indicates
+success, anything else signifies an error. The status return has no
+effect on server processing; the only difference between a success
+and error code is that if the latter is returned, the server will
+log an error, specifying both the library and hook that generated it.
+Effectively the return status provides a quick way for a callout to log
+error information to the BIND 10 logging system.
+
+ at subsubsection hooksdgArguments Callout Arguments
+
+The CalloutHandle object provides two methods to get and set the
+arguments passed to the callout. These methods are called (naturally
+enough) getArgument and SetArgument. Their usage is illustrated by the
+following code snippets.
+
+ at code
+ // Server-side code snippet to show the setting of arguments
+
+ int count = 10;
+ boost::shared_ptr<Pkt4> pktptr = ... // Set to appropriate value
+
+ // Assume that "handle" has been created
+ handle.setArgument("data_count", count);
+ handle.setArgument("inpacket", pktptr);
+
+ // Call the callouts attached to the hook
+ ...
+
+ // Retrieve the modified values
+ handle.getArgument("data_count", count);
+ handle.getArgument("inpacket", pktptr);
+ at endcode
+
+In the callout
+
+ at code
+ int number;
+ boost::shared_ptr<Pkt4> packet;
+
+ // Retrieve data set by the server.
+ handle.getArgument("data_count", number);
+ handle.getArgument("inpacket", packet);
+
+ // Modify "number"
+ number = ...;
+
+ // Update the arguments to send the value back to the server.
+ handle.setArgument("data_count", number);
+ at endcode
+
+As can be seen "getArgument" is used to retrieve data from the
+CalloutHandle, and setArgument used to put data into it. If a callout
+wishes to alter data and pass it back to the server, it should retrieve
+the data with getArgument, modify it, and call setArgument to send
+it back.
+
+There are several points to be aware of:
+
+- the data type of the variable in the call to getArgument must match
+the data type of the variable passed to the corresponding setArgument
+<B>exactly</B>: using what would normally be considered to be a
+"compatible" type is not enough. For example, if the server passed
+an argument as an "int" and the callout attempted to retrieve it as a
+"long", an exception would be thrown even though any value that can
+be stored in an "int" will fit into a "long". This restriction also
+applies the "const" attribute but only as applied to data pointed to by
+pointers, e.g. if an argument is defined as a "char*", an exception will
+be thrown if an attempt is made to retrieve it into a variable of type
+"const char*". (However, if an argument is set as a "const int", it can
+be retrieved into an "int".) The documentation of each hook point will
+detail the data type of each argument.
+- Although all arguments can be modified, some altered values may not
+be read by the server. (These would be ones that the server considers
+"read-only".) Consult the documentation of each hook to see whether an
+argument can be used to transfer data back to the server.
+- If a pointer to an object is passed to a callout (either a "raw"
+pointer, or a boost smart pointer (as in the example above), and the
+underlying object is altered through that pointer, the change will be
+reflected in the server even if no call is made to setArgument.
+
+In all cases, consult the documentation for the particular hook to see whether
+parameters can be modified. As a general rule:
+
+- Do not alter arguments unless you mean the change to be reflected in
+the server.
+- If you alter an argument, call CalloutHandle::setArgument to update the
+value in the CalloutHandle object.
+
+ at subsubsection hooksdgSkipFlag The "Skip" Flag
+
+When a to callouts attached to a hook returns, the server will usually continue
+its processing. However, a callout might have done something that means that
+the server should follow another path. Possible actions a server could take
+include:
+
+- Skip the next stage of processing because the callout has already
+done it. For example, a hook is located just before the DHCP server
+allocates an address to the client. A callout may decide to allocate
+special addresses for certain clients, in which case it needs to tell
+the server not to allocate an address in this case.
+- Drop the packet and continue with the next request. A possible scenario
+is a DNS server where a callout inspects the source address of an incoming
+packet and compares it against a black list; if the address is on it,
+the callout notifies the server to drop the packet.
+
+To handle these common cases, the CalloutHandle has a "skip" flag.
+This is set by a callout when it wishes the server to skip normal
+processing. It is set false by the hooks framework before callouts on a
+hook are called. If the flag is set on return, the server will take the
+"skip" action relevant for the hook.
+
+The methods to get and set the "skip" flag are getSkip and setSkip. Their
+usage is intuitive:
+
+ at code
+ // Get the current setting of the skip flag.
+ bool skip = handle.getSkip();
+
+ // Do some processing...
+ :
+ if (lease_allocated) {
+ // Flag the server to skip the next step of the processing as we
+ // already have an address.
+ handle.setSkip(true);
+ }
+ return;
+
+ at endcode
+
+Like arguments, the "skip" flag is passed to all callouts on a hook. Callouts
+later in the list are able to examine (and modify) the settings of earlier ones.
+
+ at subsubsection hooksdgCalloutContext Per-Request Context
+
+Although many of the BIND 10 modules can be characterised as handling
+singles packet - e.g. the DHCPv4 server receives a DISCOVER packet,
+processes it and responds with an OFFER, this is not true in all cases.
+The principal exception is the recursive DNS resolver: this receives a
+packet from a client but that packet may itself generate multiple packets
+being sent to upstream servers. To avoid possible confusion the rest of
+this section uses the term "request" to indicate a request by a client
+for some information or action.
+
+As well as argument information, the CalloutHandle object can be used by
+callouts to attach information to a request being handled by the server.
+This information (known as "context") is not used by the server: its purpose
+is to allow callouts to pass information between one another on a
+per-request basis.
+
+Context only exists only for the duration of the request: when a request
+is completed, the context is destroyed. A new request starts with no
+context information. Context is particularly useful in servers that may
+be processing multiple requests simultaneously: callouts can effectively
+attach data to a request that follows the request around the system.
+
+Context information is held as name/value pairs in the same way
+as arguments, being accessed by the pair of methods setContext and
+getContext. They have the same restrictions as the setArgument and
+getArgument methods - the type of data retrieved from context must
+<B>exactly</B> match the type of the data set.
+
+The example in the next section illustrates their use.
+
+ at subsection hooksdgExampleCallouts Example Callouts
+
+Continuing with the tutorial, the requirements need us to retrieve the
+hardware address of the incoming packet, classify it, and write it,
+together with the assigned IP address, to a log file. Although we could
+do this in one callout, for this example we'll use two:
+
+- pkt4_receive - a callout on this hook is invoked when a packet has been
+received and has been parsed. It is passed a single argument, "query4"
+which is an isc::dhcp::Pkt4 object (representing a DHCP v4 packet).
+We will do the classification here.
+
+- pkt4_send - called when a response is just about to be sent back to
+the client. It is passed a single argument "response4". This is the
+point at which the example code will write the hardware and IP addresses
+to the log file.
+
+The standard for naming callouts is to give them the same name as
+the hook. If this is done, the callouts will be automatically found
+by the Hooks system (this is discussed further in section @ref
+hooksdgCalloutRegistration). For our example, we will assume this is the
+case, so the code for the first callout (used to classify the client's
+hardware address) is:
+
+ at code
+// pkt_receive4.cc
+
+#include <hooks/hooks.h>
+#include <dhcp/pkt4.h>
+#include "library_common.h"
+
+#include <string>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace std;
+
+extern "C" {
+
+// This callout is called at the "pkt4_receive" hook.
+int pkt4_receive(CalloutHandle& handle) {
+
+ // A pointer to the packet is passed to the callout via a "boost" smart
+ // pointer. The include file "pkt4.h" typedefs a pointer to the Pkt4
+ // object as Pkt4Ptr. Retrieve a pointer to the object.
+ Pkt4Ptr query4_ptr;
+ handle.getArgument("query4", query4_ptr);
+
+ // Point to the hardware address.
+ HWAddrPtr hwaddr_ptr = query4_ptr->getHWAddr();
+
+ // The hardware address is held in a public member variable. We'll classify
+ // it as interesting if the sum of all the bytes in it is divisible by 4.
+ // (This is a contrived example after all!)
+ long sum = 0;
+ for (int i = 0; i < hwaddr_ptr->hwaddr_.size(); ++i) {
+ sum += hwaddr_ptr->hwaddr_[i];
+ }
+
+ // Classify it.
+ if (sum % 4 == 0) {
+ // Store the text form of the hardware address in the context to pass
+ // to the next callout.
+ string hwaddr = hwaddr_ptr->toText();
+ handle.setContext("hwaddr", hwaddr);
+ }
+
+ return (0);
+};
+
+}
+ at endcode
+
+The pkt4_receive callout placed the hardware address of an interesting client in
+the "hwaddr" context for the packet. Turning now to the callout that will
+write this information to the log file:
+
+ at code
+// pkt4_send.cc
+
+#include <hooks/hooks.h>
+#include <dhcp/pkt4.h>
+#include "library_common.h"
+
+#include <string>
+
+using namespace isc::dhcp;
+using namespace isc::hooks;
+using namespace std;
+
+extern "C" {
+
+// This callout is called at the "pkt4_send" hook.
+int pkt4_send(CalloutHandle& handle) {
+
+ // Obtain the hardware address of the "interesting" client. We have to
+ // use a try...catch block here because if the client was not interesting,
+ // no information would be set and getArgument would thrown an exception.
+ string hwaddr;
+ try {
+ handle.getContext("hwaddr", hwaddr);
+
+ // getContext didn't throw so the client is interesting. Get a pointer
+ // to the reply.
+ Pkt4Ptr response4_ptr;
+ handle.getArgument("response4", response4_ptr);
+
+ // Get the string form of the IP address.
+ string ipaddr = response4_ptr->getYiaddr().toText();
+
+ // Write the information to the log file.
+ interesting << hwaddr << " " << ipaddr << "\n";
+
+ // ... and to guard against a crash, we'll flush the output stream.
+ flush(interesting);
+
+ } catch (const NoSuchCalloutContext&) {
+ // No such element in the per-request context with the name "hwaddr".
+ // This means that the request was not an interesting, so do nothing
+ // and dismiss the exception.
+ }
+
+ return (0);
+}
+
+}
+ at endcode
+
+ at subsection hooksdgBuild Building the Library
+
+Building the code requires building a shareable library. This requires
+the the code be compiled as positition-independent code (using the
+compiler's "-fpic" switch) and linked as a shared library (with the
+linker's "-shared" switch). The build command also needs to point to
+the BIND 10 include directory and link in the appropriate libraries.
+
+Assuming that BIND 10 has been installed in the default location, the
+command line needed to create the library using the Gnu C++ compiler on a
+Linux system is:
+
+ at code
+g++ -I /usr/include/bind10 -L /usr/lib/bind10/lib -fpic -shared -o example.so \
+ load_unload.cc pkt4_receive.cc pkt4_send.cc version.cc \
+ -lb10-dhcpsrv -lb10-dhcp++ -lb10-hooks -lb10-log -lb10-util -lb10-exceptions
+ at endcode
+
+Notes:
+- The compilation command and switches required may vary depending on
+your operating system and compiler - consult the relevant documentation
+for details.
+- The values for the "-I" and "-L" switches depend on where you have
+installed BIND 10.
+- The list of libraries that need to be included in the command line
+depends on the functionality used by the hook code and the module to
+which they are attached (e.g. hook code for DNS will need to link against
+the libb10-dns++ library). Depending on operating system, you may also need
+to explicitly list libraries on which the BIND 10 libraries depend.
+
+ at subsection hooksdgConfiguration Configuring the Hook Library
+
+The final step is to make the library known to BIND 10. All BIND 10 modules to
+which hooks can be added contain the "hook_library" element, and user
+libraries are added to this. (The BIND 10 hooks system can handle multiple libraries - this is discussed below.).
+
+To add the example library (assumed to be in /usr/local/lib) to the DHCPv4
+module, the following bindctl commands must be executed:
+
+ at code
+> config add Dhcp4/hook_libraries
+> config set Dhcp4/hook_libraries[0] "/usr/local/lib/example.so"
+> config commit
+ at endcode
+
+The DHCPv4 server will load the library and execute the callouts each time a
+request is received.
+
+ at note The above assumes that the hooks library will be used with a version of
+BIND 10 that is dynamically-linked. For information regarding running
+hooks libraries against a statically-linked BIND 10, see
+ at ref hooksdgStaticallyLinkedBind10.
+
+ at section hooksdgAdvancedTopics Advanced Topics
+
+ at subsection hooksdgContextCreateDestroy Context Creation and Destruction
+
+As well as the hooks defined by the server, the hooks framework defines
+two hooks of its own, "context_create" and "context_destroy". The first
+is called when a request is created in the server, before any of the
+server-specific hooks gets called. It's purpose it to allow a library
+to initialize per-request context. The second is called after all
+server-defined hooks have been processed, and is to allow a library to
+tidy up.
+
+As an example, the pkt4_send example above required that the code
+check for an exception being thrown when accessing the "hwaddr" context
+item in case it was not set. An alternative strategy would have been to
+provide a callout for the "context_create" hook and set the context item
+"hwaddr" to an empty string. Instead of needing to handle an exception,
+pkt4_send would be guaranteed to get something when looking for
+the hwaddr item and so could write or not write the output depending on
+the value.
+
+In most cases, "context_destroy" is not needed as the Hooks system
+automatically deletes context. An example where it could be required
+is where memory has been allocated by a callout during the processing
+of a request and a raw pointer to it stored in the context object. On
+destruction of the context, that memory will not be automatically
+released. Freeing in the memory in the "context_destroy callout will solve
+that problem.
+
+Actually, when the context is destroyed, the destructor
+associated with any objects stored in it are run. Rather than point to
+allocated memory with a raw pointer, a better idea would be to point to
+it with a boost "smart" pointer and store that pointer in the context.
+When the context is destroyed, the smart pointer's destructor is run,
+which will automatically delete the pointed-to object.
+
+These approaches are illustrated in the following examples.
+Here it is assumed that the hooks library is performing some form of
+security checking on the packet and needs to maintain information in
+a user-specified "SecurityInformation" object. (The details of this
+fictitious object are of no concern here.) The object is created in
+the context_create callout and used in both the pkt4_receive and the
+pkt4_send callouts.
+
+ at code
+// Storing information in a "raw" pointer. Assume that the
+
+#include <hooks/hooks.h>
+ :
+
+extern "C" {
+
+// context_create callout - called when the request is created.
+int context_create(CalloutHandle& handle) {
+ // Create the security information and store it in the context
+ // for this packet.
+ SecurityInformation* si = new SecurityInformation();
+ handle.setContext("security_information", si);
+}
+
+// Callouts that use the context
+int pkt4_receive(CalloutHandle& handle) {
+ // Retrieve the pointer to the SecurityInformation object
+ SecurityInformation si;
+ handle.getContext("security_information", si);
+ :
+ :
+ // Set the security information
+ si->setSomething(...);
+
+ // The pointed-to information has been updated but the pointer has not been
+ // altered, so there is no need to call setContext() again.
+}
+
+int pkt4_send(CalloutHandle& handle) {
+ // Retrieve the pointer to the SecurityInformation object
+ SecurityInformation si;
+ handle.getContext("security_information", si);
+ :
+ :
+ // Retrieve security information
+ bool active = si->getSomething(...);
+ :
+}
+
+// Context destruction. We need to delete the pointed-to SecurityInformation
+// object because we will lose the pointer to it when the CalloutHandle is
+// destroyed.
+int context_destroy(CalloutHandle& handle) {
+ // Retrieve the pointer to the SecurityInformation object
+ SecurityInformation si;
+ handle.getContext("security_information", si);
+
+ // Delete the pointed-to memory.
+ delete si;
+}
+ at endcode
+
+The requirement for the context_destroy callout can be eliminated if
+a Boost shared ptr is used to point to the allocated memory:
+
+ at code
+// Storing information in a "raw" pointer. Assume that the
+
+#include <hooks/hooks.h>
+#include <boost/shared_ptr.hpp>
+ :
+
+extern "C" {
+
+// context_create callout - called when the request is created.
+
+int context_create(CalloutHandle& handle) {
+ // Create the security information and store it in the context for this
+ // packet.
+ boost::shared_ptr<SecurityInformation> si(new SecurityInformation());
+ handle.setContext("security_information", si);
+}
+
+// Other than the data type, a shared pointer has similar semantics to a "raw"
+// pointer. Only the code from pkt4_receive is shown here.
+
+int pkt4_receive(CalloutHandle& handle) {
+ // Retrieve the pointer to the SecurityInformation object
+ boost::shared_ptr<SecurityInformation> si;
+ handle.setContext("security_information", si);
+ :
+ :
+ // Modify the security information
+ si->setSomething(...);
+
+ // The pointed-to information has been updated but the pointer has not
+ // altered, so theree is no need to reset the context.
+}
+
+// No context_destroy callout is needed to delete the allocated
+// SecurityInformation object. When the CalloutHandle is destroyed, the shared
+// pointer object will be destroyed. If that is the last shared pointer to the
+// allocated memory, then it too will be deleted.
+ at endcode
+
+(Note that a Boost shared pointer - rather than any other Boost smart pointer -
+should be used, as the pointer objects are copied within the hooks framework and
+only shared pointers have the correct behavior for the copy operation.)
+
+
+ at subsection hooksdgCalloutRegistration Registering Callouts
+
+As briefly mentioned in @ref hooksdgExampleCallouts, the standard is for
+callouts in the user library to have the same name as the name of the
+hook to which they are being attached. This convention was followed
+in the tutorial, e.g. the callout that needed to be attached to the
+"pkt4_receive" hook was named pkt4_receive.
+
+The reason for this convention is that when the library is loaded, the
+hook framework automatically searches the library for functions with
+the same names as the server hooks. When it finds one, it attaches it
+to the appropriate hook point. This simplifies the loading process and
+bookkeeping required to create a library of callouts.
+
+However, the hooks system is flexible in this area: callouts can have
+non-standard names, and multiple callouts can be registered on a hook.
+
+ at subsubsection hooksdgLibraryHandle The LibraryHandle Object
+
+The way into the part of the hooks framework that allows callout
+registration is through the LibraryHandle object. This was briefly
+introduced in the discussion of the framework functions, in that
+an object of this type is pass to the "load" function. A LibraryHandle
+can also be obtained from within a callout by calling the CalloutHandle's
+getLibraryHandle() method.
+
+The LibraryHandle provides three methods to manipulate callouts:
+
+- registerCallout - register a callout on a hook.
+- deregisterCallout - deregister a callout from a hook.
+- deregisterAllCallouts - deregister all callouts on a hook.
+
+The following sections cover some of the ways in which these can be used.
+
+ at subsubsection hooksdgNonstandardCalloutNames Non-Standard Callout Names
+
+The example in the tutorial used standard names for the callouts. As noted
+above, it is possible to use non-standard names. Suppose, instead of the
+callout names "pkt4_receive" and "pkt4_send", we had named our callouts
+"classify" and "write_data". The hooks framework would not have registered
+these callouts, so we would have needed to do it ourself. The place to
+do this is the "load" framework function, and its code would have had to
+been modified to:
+
+ at code
+int load(LibraryHandle& libhandle) {
+ // Register the callouts on the hooks. We assume that a header file
+ // declares the "classify" and "write_data" functions.
+ libhandle.registerCallout("pkt4_receive", classify);
+ libhandle.registerCallout("pkt4_send", write_data);
+
+ // Open the log file
+ interesting.open("/data/clients/interesting.log",
+ std::fstream::out | std::fstream::app);
+ return (interesting ? 0 : 1);
+}
+ at endcode
+
+It is possible for a library to contain callouts with both standard and
+non-standard names: ones with standard names will be registered automatically,
+ones with non-standard names need to be registered manually.
+
+ at subsubsection hooksdgMultipleCallouts Multiple Callouts on a Hook
+
+The BIND 10 hooks framework allows multiple callouts to be attached to
+a hook point. Although it is likely to be rare for user code to need to
+do this, there may be instances where it make sense.
+
+To register multiple callouts on a hook, just call
+LibraryHandle::registerCallout multiple times on the same hook, e.g.
+
+ at code
+ libhandle.registerCallout("pkt4_receive", classify);
+ libhandle.registerCallout("pkt4_receive", write_data);
+ at endcode
+
+The hooks framework will call the callouts in the order they are
+registered. The same CalloutHandle is passed between them, so any
+change made to the CalloutHandle's arguments, "skip" flag, or per-request
+context by the first is visible to the second.
+
+ at subsubsection hooksdgDynamicRegistration Dynamic Registration and Reregistration of Callouts
+
+The previous sections have dealt with callouts being registered during
+the call to "load". The hooks framework is more flexible than that
+in that callouts can be registered and deregistered within a callout.
+In fact, a callout is able to register or deregister itself, and a callout
+is able to be registered on a hook multiple times.
+
+Using our contrived example again, the DHCPv4 server processes one request
+to completion before it starts processing the next. With this knowledge,
+we could alter the logic of the code so that the callout attached to the
+"pkt4_receive" hook registers the callout doing the logging when it detects
+an interesting packet, and the callout doing the logging deregisters
+itself in its execution. The relevant modifications to the code in
+the tutorial are shown below:
+
+ at code
+// pkt4_receive.cc
+// :
+
+int pkt4_receive(CalloutHandle& handle) {
+
+ :
+ :
+
+ // Classify it.
+ if (sum % 4 == 0) {
+ // Store the text form of the hardware address in the context to pass
+ // to the next callout.
+ handle.setContext("hwaddr", hwaddr_ptr->hwaddr_.toText());
+
+ // Register the callback to log the data.
+ handle.getLibraryHandle().registerCallout("pkt4_send", write_data);
+ }
+
+ return (0);
+};
+ at endcode
+
+ at code
+// pkt4_send.cc
+ :
+
+int write_data(CalloutHandle& handle) {
+
+ // Obtain the hardware address of the "interesting" client. As the
+ // callback is only registered when interesting data is present, we
+ // know that the context contains the hardware address so an exception
+ // will not be thrown when we call getArgument().
+ string hwaddr;
+ handle.getArgument("hwaddr", hwaddr);
+
+ // The pointer to the reply.
+ ConstPkt4Ptr reply;
+ handle.getArgument("reply", reply);
+
+ // Get the string form of the IP address.
+ string ipaddr = reply->getYiaddr().toText():
+
+ // Write the information to the log file and flush.
+ interesting << hwaddr << " " << ipaddr << "\n";
+ flush(interesting);
+
+ // We've logged the data, so deregister ourself. This callout will not
+ // be called again until it is registered by pkt4_receive.
+
+ handle.getLibraryHandle().deregisterCallout("pkt4_send", write_data);
+
+ return (0);
+}
+ at endcode
+
+Note that the above example used a non-standard name for the callout
+that wrote the data. Had the name been a standard one, it would have been
+registered when the library was loaded and called for the first request,
+regardless of whether that was defined as "interesting". (Although as
+callouts with standard names are always registered before "load" gets called,
+we could have got round that problem by deregistering that particular
+callout in the "load" function.)
+
+
+ at note Deregistration of a callout on the hook that is currently
+being called only takes effect when the server next calls the hook.
+To illustrate this, suppose the callouts attached to a hook are A, B and C
+(in that order), and during execution, A deregisters B and C and adds D.
+When callout A returns, B and C will still run. The next time the server
+calls the hook's callouts, A and D will run (in that order).
+
+ at subsection hooksdgMultipleLibraries Multiple User Libraries
+
+As alluded to in the section @ref hooksdgConfiguration, BIND 10 can load
+multiple libraries. The libraries are loaded in the order specified in
+the configuration, and the callouts attached to the hooks in the order
+presented by the libraries.
+
+The following picture illustrates this, and also illustrates the scope of
+data passed around the system.
+
+ at image html DataScopeArgument.png "Scope of Arguments"
+
+In this illustration, a server has three hook points, alpha, beta
+and gamma. Two libraries are configured, library 1 and library 2.
+Library 1 registers the callout "authorize" for hook alpha, "check" for
+hook beta and "add_option" for hook gamma. Library 2 registers "logpkt",
+"validate" and "putopt"
+
+The horizontal red lines represent arguments to callouts. When the server
+calls hook alpha, it creates an argument list and calls the
+first callout for the hook, "authorize". When that callout returns, the
+same (but possibly modified) argument list is passed to the next callout
+in the chain, "logpkt". Another, separate argument list is created for
+hook beta and passed to the callouts "check" and "validate" in
+that order. A similar sequence occurs for hook gamma.
+
+The next picture shows the scope of the context associated with a
+request.
+
+ at image html DataScopeContext.png "Illustration of per-library context"
+
+The vertical blue lines represent callout context. Context is
+per-packet but also per-library. When the server calls "authorize",
+the CalloutHandle's getContext and setContext methods access a context
+created purely for library 1. The next callout on the hook will access
+context created for library 2. These contexts are passed to the callouts
+associated with the next hook. So when "check" is called, it gets the
+context data that was set by "authorize", when "validate" is called,
+it gets the context data set by "logpkt".
+
+It is stressed that the context for callouts associated with different
+libraries is entirely separate. For example, suppose "authorize" sets
+the CalloutHandle's context item "foo" to 2 and "logpkt" sets an item of
+the same name to the string "bar". When "check" accesses the context
+item "foo", it gets a value of 2; when "validate" accesses an item of
+the same name, it gets the value "bar".
+
+It is also stressed that all this context exists only for the life of the
+request being processed. When that request is complete, all the
+context associated with that request - for all libraries - is destroyed,
+and new context created for the next request.
+
+This structure means that library authors can use per-request context
+without worrying about the presence of other libraries. Other libraries
+may be present, but will not affect the context values set by a library's
+callouts.
+
+ at subsection hooksdgInterLibraryData Passing Data Between Libraries
+
+In rare cases, it is possible that one library may want to pass
+data to another. This can be done in a limited way by means of the
+CalloutHandle's setArgument and getArgument calls. For example, in the
+above diagram, the callout "add_option" can pass a value to "putopt"
+by setting a name.value pair in the hook's argument list. "putopt"
+would be able to read this, but would not be able to return information
+back to "add_option".
+
+All argument names used by BIND 10 will be a combination of letters
+(both upper- and lower-case), digits, hyphens and underscores: no
+other characters will be used. As argument names are simple strings,
+it is suggested that if such a mechanism be used, the names of the data
+values passed between the libraries include a special character such as
+the dollar symbol or percent sign. In this way there is no danger that
+a name will conflict with any existing or future BIND 10 argument names.
+
+
+ at subsection hooksdgRegisterMultipleLibraries Dynamic Callout Registration and Multiple Libraries
+
+On a particular hook, callouts are called in the order the libraries appear
+in the configuration and, within a library, in the order the callouts
+are registered.
+
+This order applies to dynamically-registered callouts as well. As an
+example, consider the diagram above where for hook "beta", callout "check"
+is followed by callout "validate". Suppose that when "authorize" is run,
+it registers a new callout ("double_check") on hook "beta". That
+callout will be inserted at the end of the callouts registered by
+library 1 and before any registered by library 2. It would therefore
+appear between "check" and "validate". On the other hand, if it were
+"logpkt" that registered the new callout, "double_check" would appear
+after "validate".
+
+ at subsection hooksdgStaticallyLinkedBind10 Running Against a Statically-Linked BIND 10
+
+If BIND 10 is built with the --enable-static-link switch (set when
+running the "configure" script), no shared BIND 10 libraries are built;
+instead, archive libraries are created and BIND 10 is linked to them.
+If you create a hooks library also linked against these archive libraries,
+when the library is loaded you end up with two copies of the library code,
+one in BIND 10 and one in your library.
+
+To run successfully, your library needs to perform run-time initialization
+of the BIND 10 code in your library (something performed by BIND 10
+in the case of shared libraries). To do this, call the function
+isc::hooks::hooksStaticLinkInit() as the first statement of the load()
+function. (If your library does not include a load() function, you need
+to add one.) For example:
+
+ at code
+#include <hooks/hooks.h>
+
+extern "C" {
+
+int version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int load() {
+ isc::hooks::hooksStaticLinkInit();
+ :
+}
+
+// Other callout functions
+ :
+
+}
+ at endcode
+*/
diff --git a/src/lib/hooks/images/DataScopeArgument.dia b/src/lib/hooks/images/DataScopeArgument.dia
new file mode 100644
index 0000000..02a4f17
Binary files /dev/null and b/src/lib/hooks/images/DataScopeArgument.dia differ
diff --git a/src/lib/hooks/images/DataScopeArgument.png b/src/lib/hooks/images/DataScopeArgument.png
new file mode 100644
index 0000000..34a5bd1
Binary files /dev/null and b/src/lib/hooks/images/DataScopeArgument.png differ
diff --git a/src/lib/hooks/images/DataScopeContext.dia b/src/lib/hooks/images/DataScopeContext.dia
new file mode 100644
index 0000000..1e39f5b
Binary files /dev/null and b/src/lib/hooks/images/DataScopeContext.dia differ
diff --git a/src/lib/hooks/images/DataScopeContext.png b/src/lib/hooks/images/DataScopeContext.png
new file mode 100644
index 0000000..ba18875
Binary files /dev/null and b/src/lib/hooks/images/DataScopeContext.png differ
diff --git a/src/lib/hooks/images/HooksUml.dia b/src/lib/hooks/images/HooksUml.dia
new file mode 100644
index 0000000..0972fca
Binary files /dev/null and b/src/lib/hooks/images/HooksUml.dia differ
diff --git a/src/lib/hooks/images/HooksUml.png b/src/lib/hooks/images/HooksUml.png
new file mode 100644
index 0000000..3859e6a
Binary files /dev/null and b/src/lib/hooks/images/HooksUml.png differ
diff --git a/src/lib/hooks/library_handle.cc b/src/lib/hooks/library_handle.cc
new file mode 100644
index 0000000..7f43116
--- /dev/null
+++ b/src/lib/hooks/library_handle.cc
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+
+namespace isc {
+namespace hooks {
+
+// Callout manipulation - all deferred to the CalloutManager.
+
+void
+LibraryHandle::registerCallout(const std::string& name, CalloutPtr callout) {
+ // Reset library index if required, saving the current value.
+ int saved_index = callout_manager_->getLibraryIndex();
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(index_);
+ }
+
+ // Register the callout.
+ callout_manager_->registerCallout(name, callout);
+
+ // Restore the library index if required. We know that the saved index
+ // is valid for the number of libraries (or is -1, which is an internal
+ // state indicating there is no current library index) as we obtained it
+ // from the callout manager.
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(saved_index);
+ }
+}
+
+bool
+LibraryHandle::deregisterCallout(const std::string& name, CalloutPtr callout) {
+ int saved_index = callout_manager_->getLibraryIndex();
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(index_);
+ }
+
+ bool status = callout_manager_->deregisterCallout(name, callout);
+
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(saved_index);
+ }
+
+ return (status);
+}
+
+bool
+LibraryHandle::deregisterAllCallouts(const std::string& name) {
+ int saved_index = callout_manager_->getLibraryIndex();
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(index_);
+ }
+
+ bool status = callout_manager_->deregisterAllCallouts(name);
+
+ if (index_ >= 0) {
+ callout_manager_->setLibraryIndex(saved_index);
+ }
+
+ return (status);
+}
+
+} // namespace util
+} // namespace isc
diff --git a/src/lib/hooks/library_handle.h b/src/lib/hooks/library_handle.h
new file mode 100644
index 0000000..4fe47cd
--- /dev/null
+++ b/src/lib/hooks/library_handle.h
@@ -0,0 +1,149 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LIBRARY_HANDLE_H
+#define LIBRARY_HANDLE_H
+
+#include <string>
+
+namespace isc {
+namespace hooks {
+
+// Forward declarations
+class CalloutHandle;
+class CalloutManager;
+
+/// Typedef for a callout pointer. (Callouts must have "C" linkage.)
+extern "C" {
+ typedef int (*CalloutPtr)(CalloutHandle&);
+};
+
+/// @brief Library handle
+///
+/// This class is accessed by the user library when registering callouts,
+/// either by the library's load() function, or by one of the callouts
+/// themselves.
+///
+/// It is really little more than a shell around the CalloutManager. By
+/// presenting this object to the user-library callouts, callouts can manage
+/// the callout list for their own library, but cannot affect the callouts
+/// registered by other libraries.
+///
+/// (This restriction is achieved by the CalloutManager maintaining the concept
+/// of the "current library". When a callout is registered - either by the
+/// library's load() function, or by a callout in the library - the registration
+/// information includes the library active at the time. When that callout is
+/// called, the CalloutManager uses that information to set the "current
+/// library": the registration functions only operator on data whose
+/// associated library is equal to the "current library".)
+
+class LibraryHandle {
+public:
+
+ /// @brief Constructor
+ ///
+ /// @param manager Back pointer to the containing CalloutManager.
+ /// This pointer is used to access appropriate methods in that
+ /// object. Note that the "raw" pointer is safe - the only
+ /// instance of the LibraryHandle in the system is as a member of
+ /// the CalloutManager to which it points.
+ ///
+ /// @param index Index of the library to which the LibraryHandle applies.
+ /// If negative, the library index as set in the CalloutManager is
+ /// used. Otherwise the current library index is saved, this value
+ /// is set as the current index, the registration call is made, and
+ /// the CalloutManager's library index is reset. Note: although -1
+ /// is a valid argument value for
+ /// isc::hooks::CalloutManager::setLibraryIndex(), in this class is
+ /// is used as a sentinel to indicate that the library index in
+ /// isc::util::CalloutManager should not be set or reset.
+ LibraryHandle(CalloutManager* manager, int index = -1)
+ : callout_manager_(manager), index_(index)
+ {}
+
+ /// @brief Register a callout on a hook
+ ///
+ /// Registers a callout function with a given hook. The callout is added
+ /// to the end of the callouts for the current library that are associated
+ /// with that hook.
+ ///
+ /// @param name Name of the hook to which the callout is added.
+ /// @param callout Pointer to the callout function to be registered.
+ ///
+ /// @throw NoSuchHook The hook name is unrecognised.
+ /// @throw Unexpected The hook name is valid but an internal data structure
+ /// is of the wrong size.
+ void registerCallout(const std::string& name, CalloutPtr callout);
+
+ /// @brief De-Register a callout on a hook
+ ///
+ /// Searches through the functions registered by the current library with
+ /// the named hook and removes all entries matching the callout. It does
+ /// not affect callouts registered by other libraries.
+ ///
+ /// @param name Name of the hook from which the callout is removed.
+ /// @param callout Pointer to the callout function to be removed.
+ ///
+ /// @return true if a one or more callouts were deregistered.
+ ///
+ /// @throw NoSuchHook The hook name is unrecognised.
+ /// @throw Unexpected The hook name is valid but an internal data structure
+ /// is of the wrong size.
+ bool deregisterCallout(const std::string& name, CalloutPtr callout);
+
+ /// @brief Removes all callouts on a hook
+ ///
+ /// Removes all callouts associated with a given hook that were registered.
+ /// by the current library. It does not affect callouts that were
+ /// registered by other libraries.
+ ///
+ /// @param name Name of the hook from which the callouts are removed.
+ ///
+ /// @return true if one or more callouts were deregistered.
+ ///
+ /// @throw NoSuchHook Thrown if the hook name is unrecognised.
+ bool deregisterAllCallouts(const std::string& name);
+
+private:
+ /// @brief Copy constructor
+ ///
+ /// Private (with no implementation) as it makes no sense to copy an object
+ /// of this type. All code receives a reference to an existing handle which
+ /// is tied to a particular CalloutManager. Creating a copy of that handle
+ /// runs the risk of a "dangling pointer" to the original handle's callout
+ /// manager.
+ ///
+ /// @param Unused - should be the object to copy.
+ LibraryHandle(const LibraryHandle&);
+
+ /// @brief Assignment operator
+ ///
+ /// Declared private like the copy constructor for the same reasons. It too
+ /// has no implementation.
+ ///
+ /// @param Unused - should be the object to copy.
+ LibraryHandle& operator=(const LibraryHandle&);
+
+ /// Back pointer to the collection object for the library
+ CalloutManager* callout_manager_;
+
+ /// Library index to which this handle applies. -1 indicates that it
+ /// applies to whatever index is current in the CalloutManager.
+ int index_;
+};
+
+} // namespace util
+} // namespace isc
+
+#endif // LIBRARY_HANDLE_H
diff --git a/src/lib/hooks/library_manager.cc b/src/lib/hooks/library_manager.cc
new file mode 100644
index 0000000..4b04005
--- /dev/null
+++ b/src/lib/hooks/library_manager.cc
@@ -0,0 +1,362 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <hooks/hooks.h>
+#include <hooks/hooks_log.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/library_manager.h>
+#include <hooks/pointer_converter.h>
+#include <hooks/server_hooks.h>
+
+#include <string>
+#include <vector>
+
+#include <dlfcn.h>
+
+using namespace std;
+
+namespace isc {
+namespace hooks {
+
+
+// Constructor (used by external agency)
+LibraryManager::LibraryManager(const std::string& name, int index,
+ const boost::shared_ptr<CalloutManager>& manager)
+ : dl_handle_(NULL), index_(index), manager_(manager),
+ library_name_(name)
+{
+ if (!manager) {
+ isc_throw(NoCalloutManager, "must specify a CalloutManager when "
+ "instantiating a LibraryManager object");
+ }
+}
+
+// Constructor (used by "validate" for library validation). Note that this
+// sets "manager_" to not point to anything, which means that methods such as
+// registerStandardCallout() will fail, probably with a segmentation fault.
+// There are no checks for this condition in those methods: this constructor
+// is declared "private", so can only be executed by a method in this class.
+// The only method to do so is "validateLibrary", which takes care not to call
+// methods requiring a non-NULL manager.
+LibraryManager::LibraryManager(const std::string& name)
+ : dl_handle_(NULL), index_(-1), manager_(), library_name_(name)
+{}
+
+// Destructor.
+LibraryManager::~LibraryManager() {
+ if (manager_) {
+ // LibraryManager instantiated to load a library, so ensure that
+ // it is unloaded before exiting.
+ static_cast<void>(unloadLibrary());
+ } else {
+ // LibraryManager instantiated to validate a library, so just ensure
+ // that it is closed before exiting.
+ static_cast<void>(closeLibrary());
+ }
+}
+
+// Open the library
+
+bool
+LibraryManager::openLibrary() {
+
+ // Open the library. We'll resolve names now, so that if there are any
+ // issues we don't bugcheck in the middle of apparently unrelated code.
+ dl_handle_ = dlopen(library_name_.c_str(), RTLD_NOW | RTLD_LOCAL);
+ if (dl_handle_ == NULL) {
+ LOG_ERROR(hooks_logger, HOOKS_OPEN_ERROR).arg(library_name_)
+ .arg(dlerror());
+ }
+
+ return (dl_handle_ != NULL);
+}
+
+// Close the library if not already open
+
+bool
+LibraryManager::closeLibrary() {
+
+ // Close the library if it is open. (If not, this is a no-op.)
+ int status = 0;
+ if (dl_handle_ != NULL) {
+ status = dlclose(dl_handle_);
+ dl_handle_ = NULL;
+ if (status != 0) {
+ LOG_ERROR(hooks_logger, HOOKS_CLOSE_ERROR).arg(library_name_)
+ .arg(dlerror());
+ }
+ }
+
+ return (status == 0);
+}
+
+// Check the version of the library
+
+bool
+LibraryManager::checkVersion() const {
+
+ // Get the pointer to the "version" function.
+ PointerConverter pc(dlsym(dl_handle_, VERSION_FUNCTION_NAME));
+ if (pc.versionPtr() != NULL) {
+ int version = BIND10_HOOKS_VERSION - 1; // This is an invalid value
+ try {
+ version = (*pc.versionPtr())();
+ } catch (...) {
+ LOG_ERROR(hooks_logger, HOOKS_VERSION_EXCEPTION).arg(library_name_);
+ return (false);
+ }
+
+ if (version == BIND10_HOOKS_VERSION) {
+ // All OK, version checks out
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_LIBRARY_VERSION)
+ .arg(library_name_).arg(version);
+ return (true);
+
+ } else {
+ LOG_ERROR(hooks_logger, HOOKS_INCORRECT_VERSION).arg(library_name_)
+ .arg(version).arg(BIND10_HOOKS_VERSION);
+ }
+ } else {
+ LOG_ERROR(hooks_logger, HOOKS_NO_VERSION).arg(library_name_);
+ }
+
+ return (false);
+}
+
+// Register the standard callouts
+
+void
+LibraryManager::registerStandardCallouts() {
+ // Set the library index for doing the registration. This is picked up
+ // when the library handle is created.
+ manager_->setLibraryIndex(index_);
+
+ // Iterate through the list of known hooks
+ vector<string> hook_names = ServerHooks::getServerHooks().getHookNames();
+ for (int i = 0; i < hook_names.size(); ++i) {
+
+ // Look up the symbol
+ void* dlsym_ptr = dlsym(dl_handle_, hook_names[i].c_str());
+ PointerConverter pc(dlsym_ptr);
+ if (pc.calloutPtr() != NULL) {
+ // Found a symbol, so register it.
+ manager_->getLibraryHandle().registerCallout(hook_names[i],
+ pc.calloutPtr());
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS,
+ HOOKS_STD_CALLOUT_REGISTERED).arg(library_name_)
+ .arg(hook_names[i]).arg(dlsym_ptr);
+
+ }
+ }
+}
+
+// Run the "load" function if present.
+
+bool
+LibraryManager::runLoad() {
+
+ // Get the pointer to the "load" function.
+ PointerConverter pc(dlsym(dl_handle_, LOAD_FUNCTION_NAME));
+ if (pc.loadPtr() != NULL) {
+
+ // Call the load() function with the library handle. We need to set
+ // the CalloutManager's index appropriately. We'll invalidate it
+ // afterwards.
+
+ int status = -1;
+ try {
+ manager_->setLibraryIndex(index_);
+ status = (*pc.loadPtr())(manager_->getLibraryHandle());
+ } catch (const isc::Exception& ex) {
+ LOG_ERROR(hooks_logger, HOOKS_LOAD_FRAMEWORK_EXCEPTION)
+ .arg(library_name_).arg(ex.what());
+ return (false);
+ } catch (...) {
+ LOG_ERROR(hooks_logger, HOOKS_LOAD_EXCEPTION).arg(library_name_);
+ return (false);
+ }
+
+ if (status != 0) {
+ LOG_ERROR(hooks_logger, HOOKS_LOAD_ERROR).arg(library_name_)
+ .arg(status);
+ return (false);
+ } else {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_LOAD_SUCCESS)
+ .arg(library_name_);
+ }
+
+ } else {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_NO_LOAD)
+ .arg(library_name_);
+ }
+
+ return (true);
+}
+
+
+// Run the "unload" function if present.
+
+bool
+LibraryManager::runUnload() {
+
+ // Get the pointer to the "load" function.
+ PointerConverter pc(dlsym(dl_handle_, UNLOAD_FUNCTION_NAME));
+ if (pc.unloadPtr() != NULL) {
+
+ // Call the load() function with the library handle. We need to set
+ // the CalloutManager's index appropriately. We'll invalidate it
+ // afterwards.
+ int status = -1;
+ try {
+ status = (*pc.unloadPtr())();
+ } catch (const isc::Exception& ex) {
+ LOG_ERROR(hooks_logger, HOOKS_UNLOAD_FRAMEWORK_EXCEPTION)
+ .arg(library_name_).arg(ex.what());
+ return (false);
+ } catch (...) {
+ // Exception generated. Note a warning as the unload will occur
+ // anyway.
+ LOG_WARN(hooks_logger, HOOKS_UNLOAD_EXCEPTION).arg(library_name_);
+ return (false);
+ }
+
+ if (status != 0) {
+ LOG_ERROR(hooks_logger, HOOKS_UNLOAD_ERROR).arg(library_name_)
+ .arg(status);
+ return (false);
+ } else {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_UNLOAD_SUCCESS)
+ .arg(library_name_);
+ }
+ } else {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_NO_UNLOAD)
+ .arg(library_name_);
+ }
+
+ return (true);
+}
+
+// The main library loading function.
+
+bool
+LibraryManager::loadLibrary() {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_LIBRARY_LOADING)
+ .arg(library_name_);
+
+ // In the following, if a method such as openLibrary() fails, it will
+ // have issued an error message so there is no need to issue another one
+ // here.
+
+ // Open the library (which is a check that it exists and is accessible).
+ if (openLibrary()) {
+
+ // Library opened OK, see if a version function is present and if so,
+ // check what value it returns.
+ if (checkVersion()) {
+
+ // Version OK, so now register the standard callouts and call the
+ // library's load() function if present.
+ registerStandardCallouts();
+ if (runLoad()) {
+
+ // Success - the library has been successfully loaded.
+ LOG_INFO(hooks_logger, HOOKS_LIBRARY_LOADED).arg(library_name_);
+ return (true);
+
+ } else {
+
+ // The load function failed, so back out. We can't just close
+ // the library as (a) we need to call the library's "unload"
+ // function (if present) in case "load" allocated resources that
+ // need to be freed and (b) we need to remove any callouts that
+ // have been installed.
+ static_cast<void>(unloadLibrary());
+ }
+ }
+
+ // Either the version check or call to load() failed, so close the
+ // library and free up resources. Ignore the status return here - we
+ // already know there's an error and will have output a message.
+ static_cast<void>(closeLibrary());
+ }
+
+ return (false);
+}
+
+// The library unloading function. Call the unload() function (if present),
+// remove callouts from the callout manager, then close the library. This is
+// only run if the library is still loaded and is a no-op if the library is
+// not open.
+
+bool
+LibraryManager::unloadLibrary() {
+ bool result = true;
+ if (dl_handle_ != NULL) {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_TRACE, HOOKS_LIBRARY_UNLOADING)
+ .arg(library_name_);
+
+ // Call the unload() function if present. Note that this is done first
+ // - operations take place in the reverse order to which they were done
+ // when the library was loaded.
+ result = runUnload();
+
+ // Regardless of status, remove all callouts associated with this
+ // library on all hooks.
+ vector<string> hooks = ServerHooks::getServerHooks().getHookNames();
+ manager_->setLibraryIndex(index_);
+ for (int i = 0; i < hooks.size(); ++i) {
+ bool removed = manager_->deregisterAllCallouts(hooks[i]);
+ if (removed) {
+ LOG_DEBUG(hooks_logger, HOOKS_DBG_CALLS, HOOKS_CALLOUTS_REMOVED)
+ .arg(hooks[i]).arg(library_name_);
+ }
+ }
+
+ // ... and close the library.
+ result = closeLibrary() && result;
+ if (result) {
+
+ // Issue the informational message only if the library was unloaded
+ // with no problems. If there was an issue, an error message would
+ // have been issued.
+ LOG_INFO(hooks_logger, HOOKS_LIBRARY_UNLOADED).arg(library_name_);
+ }
+ }
+ return (result);
+}
+
+// Validate the library. We must be able to open it, and the version function
+// must both exist and return the right number. Note that this is a static
+// method.
+
+bool
+LibraryManager::validateLibrary(const std::string& name) {
+ // Instantiate a library manager for the validation. We use the private
+ // constructor as we don't supply a CalloutManager.
+ LibraryManager manager(name);
+
+ // Try to open it and, if we succeed, check the version.
+ bool validated = manager.openLibrary() && manager.checkVersion();
+
+ // Regardless of whether the version checked out, close the library. (This
+ // is a no-op if the library failed to open.)
+ static_cast<void>(manager.closeLibrary());
+
+ return (validated);
+}
+
+} // namespace hooks
+} // namespace isc
diff --git a/src/lib/hooks/library_manager.h b/src/lib/hooks/library_manager.h
new file mode 100644
index 0000000..dc7c5eb
--- /dev/null
+++ b/src/lib/hooks/library_manager.h
@@ -0,0 +1,231 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LIBRARY_MANAGER_H
+#define LIBRARY_MANAGER_H
+
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <string>
+
+namespace isc {
+namespace hooks {
+
+/// @brief No Callout Manager
+///
+/// Thrown if a library manager is instantiated by an external agency without
+/// specifying a CalloutManager object.
+class NoCalloutManager : public Exception {
+public:
+ NoCalloutManager(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+class CalloutManager;
+class LibraryHandle;
+class LibraryManager;
+
+/// @brief Library manager
+///
+/// This class handles the loading and unloading of a specific library. It also
+/// provides a static method for checking that a library is valid (this is used
+/// in configuration parsing).
+///
+/// On loading, it opens the library using dlopen and checks the version (set
+/// with the "version" method. If all is OK, it iterates through the list of
+/// known hooks and locates their symbols, registering each callout as it does
+/// so. Finally it locates the "load" function (if present) and calls it.
+///
+/// On unload, it calls the "unload" method if present, clears the callouts on
+/// all hooks, and closes the library.
+///
+/// @note Caution needs to be exercised when using the unload method. During
+/// normal use, data will pass between the server and the library. In
+/// this process, the library may allocate memory and pass it back to the
+/// server. This could happen by the server setting arguments or context
+/// in the CalloutHandle object, or by the library modifying the content
+/// of pointed-to data. If the library is unloaded, this memory may lie
+/// in the virtual address space deleted in that process. (The word "may"
+/// is used, as this could be operating-system specific.) Should this
+/// happen, any reference to the memory will cause a segmentation fault.
+/// This can occur in a quite obscure place, for example in the middle of
+/// a destructor of an STL class when it is deleting memory allocated
+/// when the data structure was extended by a function in the library.
+///
+/// @note The only safe way to run the "unload" function is to ensure that all
+/// possible references to it are removed first. This means that all
+/// CalloutHandles must be destroyed, as must any data items that were
+/// passed to the callouts. In practice, it could mean that a server
+/// suspends processing of new requests until all existing ones have
+/// been serviced and all packet/context structures destroyed before
+/// reloading the libraries.
+///
+/// When validating a library, only the fact that the library can be opened and
+/// version() exists and returns the correct number is checked. The library
+/// is closed after the validation.
+
+class LibraryManager {
+public:
+ /// @brief Constructor
+ ///
+ /// This constructor is used by external agencies (i.e. the
+ /// LibraryManagerCollection) when instantiating a LibraryManager. It
+ /// stores the library name - the actual actual loading is done in
+ /// loadLibrary().
+ ///
+ /// @param name Name of the library to load. This should be an absolute
+ /// path name.
+ /// @param index Index of this library
+ /// @param manager CalloutManager object
+ ///
+ /// @throw NoCalloutManager Thrown if the manager argument is NULL.
+ LibraryManager(const std::string& name, int index,
+ const boost::shared_ptr<CalloutManager>& manager);
+
+ /// @brief Destructor
+ ///
+ /// If the library is open, closes it. This is principally a safety
+ /// feature to ensure closure in the case of an exception destroying this
+ /// object. However, see the caveat in the class header about when it is
+ /// safe to unload libraries.
+ ~LibraryManager();
+
+ /// @brief Validate library
+ ///
+ /// A static method that is used to validate a library. Validation checks
+ /// that the library can be opened, that "version" exists, and that it
+ /// returns the right number.
+ ///
+ /// @param name Name of the library to validate
+ ///
+ /// @return true if the library validated, false if not. If the library
+ /// fails to validate, the reason for the failure is logged.
+ static bool validateLibrary(const std::string& name);
+
+ /// @brief Loads a library
+ ///
+ /// Open the library and check the version. If all is OK, load all standard
+ /// symbols then call "load" if present.
+ ///
+ /// @return true if the library loaded successfully, false otherwise. In the
+ /// latter case, the library will be unloaded if possible.
+ bool loadLibrary();
+
+ /// @brief Unloads a library
+ ///
+ /// Calls the libraries "unload" function if present, the closes the
+ /// library.
+ ///
+ /// However, see the caveat in the class header about when it is safe to
+ /// unload libraries.
+ ///
+ /// @return true if the library unloaded successfully, false if an error
+ /// occurred in the process (most likely the unload() function
+ /// (if present) returned an error). Even if an error did occur,
+ /// the library is closed if possible.
+ bool unloadLibrary();
+
+ /// @brief Return library name
+ ///
+ /// @return Name of this library
+ std::string getName() const {
+ return (library_name_);
+ }
+
+protected:
+ // The following methods are protected as they are accessed in testing.
+
+ /// @brief Open library
+ ///
+ /// Opens the library associated with this LibraryManager. A message is
+ /// logged on an error.
+ ///
+ /// @return true if the library opened successfully, false otherwise.
+ bool openLibrary();
+
+ /// @brief Close library
+ ///
+ /// Closes the library associated with this LibraryManager. A message is
+ /// logged on an error.
+ ///
+ /// @return true if the library closed successfully, false otherwise. "true"
+ /// is also returned if the library were already closed when this
+ /// method was called.
+ bool closeLibrary();
+
+ /// @brief Check library version
+ ///
+ /// With the library open, accesses the "version()" function and, if
+ /// present, checks the returned value against the hooks version symbol
+ /// for the currently running BIND 10. The "version()" function is
+ /// mandatory and must be present (and return the correct value) for the
+ /// library to load.
+ ///
+ /// If there is no version() function, or if there is a mismatch in
+ /// version number, a message logged.
+ ///
+ /// @return bool true if the check succeeded
+ bool checkVersion() const;
+
+ /// @brief Register standard callouts
+ ///
+ /// Loops through the list of hook names and searches the library for
+ /// functions with those names. Any that are found are registered as
+ /// callouts for that hook.
+ void registerStandardCallouts();
+
+ /// @brief Run the load function if present
+ ///
+ /// Searches for the "load" framework function and, if present, runs it.
+ ///
+ /// @return bool true if not found or found and run successfully,
+ /// false on an error. In this case, an error message will
+ /// have been output.
+ bool runLoad();
+
+ /// @brief Run the unload function if present
+ ///
+ /// Searches for the "unload" framework function and, if present, runs it.
+ ///
+ /// @return bool true if not found or found and run successfully,
+ /// false on an error. In this case, an error message will
+ /// have been output.
+ bool runUnload();
+
+private:
+ /// @brief Validating constructor
+ ///
+ /// Constructor used when the LibraryManager is instantiated to validate
+ /// a library (i.e. by the "validateLibrary" static method).
+ ///
+ /// @param name Name of the library to load. This should be an absolute
+ /// path name.
+ LibraryManager(const std::string& name);
+
+ // Member variables
+
+ void* dl_handle_; ///< Handle returned by dlopen
+ int index_; ///< Index associated with this library
+ boost::shared_ptr<CalloutManager> manager_;
+ ///< Callout manager for registration
+ std::string library_name_; ///< Name of the library
+
+};
+
+} // namespace hooks
+} // namespace isc
+
+#endif // LIBRARY_MANAGER_H
diff --git a/src/lib/hooks/library_manager_collection.cc b/src/lib/hooks/library_manager_collection.cc
new file mode 100644
index 0000000..b9122e2
--- /dev/null
+++ b/src/lib/hooks/library_manager_collection.cc
@@ -0,0 +1,129 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_manager.h>
+#include <hooks/library_manager.h>
+#include <hooks/library_manager_collection.h>
+
+namespace isc {
+namespace hooks {
+
+// Return callout manager for the loaded libraries. This call is only valid
+// after one has been created for the loaded libraries (which includes the
+// case of no loaded libraries).
+//
+// Note that there is no real connection between the callout manager and the
+// libraries, other than it knows the number of libraries so can do sanity
+// checks on values passed to it. However, this may change in the future,
+// so the hooks framework is written such that a callout manager is used only
+// with the LibraryManagerCollection that created it. It is also the reason
+// why each LibraryManager contains a pointer to this CalloutManager.
+
+boost::shared_ptr<CalloutManager>
+LibraryManagerCollection::getCalloutManager() const {
+
+ // Only return a pointer if we have a CalloutManager created.
+ if (! callout_manager_) {
+ isc_throw(LoadLibrariesNotCalled, "must load hooks libraries before "
+ "attempting to retrieve a CalloutManager for them");
+ }
+
+ return (callout_manager_);
+}
+
+// Load a set of libraries
+
+bool
+LibraryManagerCollection::loadLibraries() {
+
+ // Unload libraries if any are loaded.
+ static_cast<void>(unloadLibraries());
+
+ // Create the callout manager. A pointer to this is maintained by
+ // each library. Note that the callout manager does not hold any memory
+ // allocated by a library: although a library registers a callout (and so
+ // causes the creation of an entry in the CalloutManager's callout list),
+ // that creation is done by the CalloutManager itself. The CalloutManager
+ // is created within the server.
+ //
+ // The upshot of this is that it is therefore safe for the CalloutManager
+ // to be deleted after all associated libraries are deleted, hence this
+ // link (LibraryManager -> CalloutManager) is safe.
+ callout_manager_.reset(new CalloutManager(library_names_.size()));
+
+ // Now iterate through the libraries are load them one by one. We'll
+ for (int i = 0; i < library_names_.size(); ++i) {
+ // Create a pointer to the new library manager. The index of this
+ // library is determined by the number of library managers currently
+ // loaded: note that the library indexes run from 1 to (number of loaded
+ // libraries).
+ boost::shared_ptr<LibraryManager> manager(
+ new LibraryManager(library_names_[i], lib_managers_.size() + 1,
+ callout_manager_));
+
+ // Load the library. On success, add it to the list of loaded
+ // libraries. On failure, unload all currently loaded libraries,
+ // leaving the object in the state it was in before loadLibraries was
+ // called.
+ if (manager->loadLibrary()) {
+ lib_managers_.push_back(manager);
+ } else {
+ static_cast<void>(unloadLibraries());
+ return (false);
+ }
+ }
+
+ return (true);
+}
+
+// Unload the libraries.
+
+void
+LibraryManagerCollection::unloadLibraries() {
+
+ // Delete the library managers in the reverse order to which they were
+ // created, then clear the library manager vector.
+ for (int i = lib_managers_.size() - 1; i >= 0; --i) {
+ lib_managers_[i].reset();
+ }
+ lib_managers_.clear();
+
+ // Get rid of the callout manager. (The other member, the list of library
+ // names, was cleared when the libraries were loaded.)
+ callout_manager_.reset();
+}
+
+// Return number of loaded libraries.
+int
+LibraryManagerCollection::getLoadedLibraryCount() const {
+ return (lib_managers_.size());
+}
+
+// Validate the libraries.
+std::vector<std::string>
+LibraryManagerCollection::validateLibraries(
+ const std::vector<std::string>& libraries) {
+
+ std::vector<std::string> failures;
+ for (int i = 0; i < libraries.size(); ++i) {
+ if (!LibraryManager::validateLibrary(libraries[i])) {
+ failures.push_back(libraries[i]);
+ }
+ }
+
+ return (failures);
+}
+
+} // namespace hooks
+} // namespace isc
diff --git a/src/lib/hooks/library_manager_collection.h b/src/lib/hooks/library_manager_collection.h
new file mode 100644
index 0000000..0a255ba
--- /dev/null
+++ b/src/lib/hooks/library_manager_collection.h
@@ -0,0 +1,170 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef LIBRARY_MANAGER_COLLECTION_H
+#define LIBRARY_MANAGER_COLLECTION_H
+
+#include <exceptions/exceptions.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <vector>
+
+namespace isc {
+namespace hooks {
+
+/// @brief LoadLibraries not called
+///
+/// Thrown if an attempt is made get a CalloutManager before the libraries
+/// have been loaded.
+class LoadLibrariesNotCalled : public Exception {
+public:
+ LoadLibrariesNotCalled(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+
+// Forward declarations
+class CalloutManager;
+class LibraryManager;
+
+/// @brief Library manager collection
+///
+/// The LibraryManagerCollection class, as the name implies, is responsible for
+/// managing the collection of LibraryManager objects that describe the loaded
+/// libraries. As such, it converts a single operation (e.g load libraries)
+/// into multiple operations, one per library. However, the class does more
+/// than that - it provides a single object with which to manage lifetimes.
+///
+/// As described in the LibraryManager documentation, a CalloutHandle may end
+/// up with pointers to memory within the address space of a loaded library.
+/// If the library is unloaded before this address space is deleted, the
+/// deletion of the CalloutHandle may attempt to free memory into the newly-
+/// unmapped address space and cause a segmentation fault.
+///
+/// To prevent this, each CalloutHandle maintains a shared pointer to the
+/// LibraryManagerCollection current when it was created. In addition, the
+/// containing HooksManager object also maintains a shared pointer to it. A
+/// a LibraryManagerCollection is never explicitly deleted: when a new set
+/// of libraries is loaded, the HooksManager clears its pointer to the
+/// collection. The LibraryManagerCollection is only destroyed when all
+/// CallHandle objects referencing it are destroyed.
+///
+/// Note that this does not completely solve the problem - a hook function may
+/// have modified a packet being processed by the server and that packet may
+/// hold a pointer to memory in the library's virtual address space. To avoid
+/// a segmentation fault, that packet needs to free the memory before the
+/// LibraryManagerCollection is destroyed and this places demands on the server
+/// code. However, the link with the CalloutHandle does at least mean that
+/// authors of server code do not need to be so careful about when they destroy
+/// CalloutHandles.
+///
+/// The collection object also provides a utility function to validate a set
+/// of libraries. The function checks that each library exists, can be opened,
+/// that the "version" function exists and return the right number.
+
+class LibraryManagerCollection {
+public:
+ /// @brief Constructor
+ ///
+ /// @param libraries List of libraries that this collection will manage.
+ /// The order of the libraries is important.
+ LibraryManagerCollection(const std::vector<std::string>& libraries)
+ : library_names_(libraries)
+ {}
+
+ /// @brief Destructor
+ ///
+ /// Unloads all loaded libraries.
+ ~LibraryManagerCollection() {
+ static_cast<void>(unloadLibraries());
+ }
+
+ /// @brief Load libraries
+ ///
+ /// Loads the libraries. This creates the LibraryManager associated with
+ /// each library and calls its loadLibrary() method. If a library fails
+ /// to load, the loading is abandoned and all libraries loaded so far
+ /// are unloaded.
+ ///
+ /// @return true if all libraries loaded, false if one or more failed t
+ //// load.
+ bool loadLibraries();
+
+ /// @brief Get callout manager
+ ///
+ /// Returns a callout manager that can be used with this set of loaded
+ /// libraries (even if the number of loaded libraries is zero). This
+ /// method may only be caslled after loadLibraries() has been called.
+ ///
+ /// @return Pointer to a callout manager for this set of libraries.
+ ///
+ /// @throw LoadLibrariesNotCalled Thrown if this method is called between
+ /// construction and the time loadLibraries() is called.
+ boost::shared_ptr<CalloutManager> getCalloutManager() const;
+
+ /// @brief Get library names
+ ///
+ /// Returns the list of library names. If called before loadLibraries(),
+ /// the list is the list of names to be loaded; if called afterwards, it
+ /// is the list of libraries that have been loaded.
+ std::vector<std::string> getLibraryNames() const {
+ return (library_names_);
+ }
+
+ /// @brief Get number of loaded libraries
+ ///
+ /// Mainly for testing, this returns the number of libraries that are
+ /// loaded.
+ ///
+ /// @return Number of libraries that are loaded.
+ int getLoadedLibraryCount() const;
+
+ /// @brief Validate libraries
+ ///
+ /// Utility function to validate libraries. It checks that the libraries
+ /// exist, can be opened, that a "version" function is present in them, and
+ /// that it returns the right number. All errors are logged.
+ ///
+ /// @param libraries List of libraries to validate
+ ///
+ /// @return Vector of libraries that faled to validate, or an empty vector
+ /// if all validated.
+ static std::vector<std::string>
+ validateLibraries(const std::vector<std::string>& libraries);
+
+protected:
+ /// @brief Unload libraries
+ ///
+ /// Unloads and closes all loaded libraries. They are unloaded in the
+ /// reverse order to the order in which they were loaded.
+ void unloadLibraries();
+
+private:
+
+ /// Vector of library names
+ std::vector<std::string> library_names_;
+
+ /// Vector of library managers
+ std::vector<boost::shared_ptr<LibraryManager> > lib_managers_;
+
+ /// Callout manager to be associated with the libraries
+ boost::shared_ptr<CalloutManager> callout_manager_;
+};
+
+} // namespace hooks
+} // namespace isc
+
+
+#endif // LIBRARY_MANAGER_COLLECTION_H
diff --git a/src/lib/hooks/pointer_converter.h b/src/lib/hooks/pointer_converter.h
new file mode 100644
index 0000000..1fe15ac
--- /dev/null
+++ b/src/lib/hooks/pointer_converter.h
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef POINTER_CONVERTER_H
+#define POINTER_CONVERTER_H
+
+#include <hooks/hooks.h>
+
+namespace isc {
+namespace hooks {
+
+/// @brief Local class for conversion of void pointers to function pointers
+///
+/// Converting between void* and function pointers in C++ is fraught with
+/// difficulty and pitfalls, e.g. see
+/// https://groups.google.com/forum/?hl=en&fromgroups#!topic/comp.lang.c++/37o0l8rtEE0
+///
+/// The method given in that article - convert using a union is used here. A
+/// union is declared (and zeroed) and the appropriate member extracted when
+/// needed.
+
+class PointerConverter {
+public:
+ /// @brief Constructor
+ ///
+ /// Zeroes the union and stores the void* pointer we wish to convert (the
+ /// one returned by dlsym).
+ ///
+ /// @param dlsym_ptr void* pointer returned by call to dlsym()
+ PointerConverter(void* dlsym_ptr) {
+ memset(&pointers_, 0, sizeof(pointers_));
+ pointers_.dlsym_ptr = dlsym_ptr;
+ }
+
+ /// @brief Constructor
+ ///
+ /// Zeroes the union and stores the CalloutPtr pointer we wish to convert.
+ /// This constructor is used in debug messages; output of a pointer to
+ /// an object (including to a function) is, on some compilers, printed as
+ /// "1".
+ ///
+ /// @param callout_ptr Pointer to callout function
+ PointerConverter(CalloutPtr callout_ptr) {
+ memset(&pointers_, 0, sizeof(pointers_));
+ pointers_.callout_ptr = callout_ptr;
+ }
+
+ /// @name Pointer accessor functions
+ ///
+ /// It is up to the caller to ensure that the correct member is called so
+ /// that the correct type of pointer is returned.
+ ///
+ ///@{
+
+ /// @brief Return pointer returned by dlsym call
+ ///
+ /// @return void* pointer returned by the call to dlsym(). This can be
+ /// used in statements that print the hexadecimal value of the
+ /// symbol.
+ void* dlsymPtr() const {
+ return (pointers_.dlsym_ptr);
+ }
+
+ /// @brief Return pointer to callout function
+ ///
+ /// @return Pointer to the callout function
+ CalloutPtr calloutPtr() const {
+ return (pointers_.callout_ptr);
+ }
+
+ /// @brief Return pointer to load function
+ ///
+ /// @return Pointer to the load function
+ load_function_ptr loadPtr() const {
+ return (pointers_.load_ptr);
+ }
+
+ /// @brief Return pointer to unload function
+ ///
+ /// @return Pointer to the unload function
+ unload_function_ptr unloadPtr() const {
+ return (pointers_.unload_ptr);
+ }
+
+ /// @brief Return pointer to version function
+ ///
+ /// @return Pointer to the version function
+ version_function_ptr versionPtr() const {
+ return (pointers_.version_ptr);
+ }
+
+ ///@}
+
+private:
+
+ /// @brief Union linking void* and pointers to functions.
+ union {
+ void* dlsym_ptr; // void* returned by dlsym
+ CalloutPtr callout_ptr; // Pointer to callout
+ load_function_ptr load_ptr; // Pointer to load function
+ unload_function_ptr unload_ptr; // Pointer to unload function
+ version_function_ptr version_ptr; // Pointer to version function
+ } pointers_;
+};
+
+} // namespace hooks
+} // namespace isc
+
+
+#endif // POINTER_CONVERTER_H
diff --git a/src/lib/hooks/server_hooks.cc b/src/lib/hooks/server_hooks.cc
new file mode 100644
index 0000000..3057d25
--- /dev/null
+++ b/src/lib/hooks/server_hooks.cc
@@ -0,0 +1,159 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <hooks/hooks_log.h>
+#include <hooks/server_hooks.h>
+
+#include <utility>
+#include <vector>
+
+using namespace std;
+using namespace isc;
+
+namespace isc {
+namespace hooks {
+
+// Constructor - register the pre-defined hooks and check that the indexes
+// assigned to them are as expected.
+//
+// Note that there are no logging messages here or in registerHooks(). The
+// recommended way to initialize hook names is to use static initialization.
+// Here, a static object is declared in a file outside of any function or
+// method. As a result, it is instantiated and its constructor run before the
+// program starts. By putting calls to ServerHooks::registerHook() in there,
+// hooks names are already registered when the program runs. However, at that
+// point, the logging system is not initialized, so messages are unable to
+// be output.
+
+ServerHooks::ServerHooks() {
+ initialize();
+}
+
+// Register a hook. The index assigned to the hook is the current number
+// of entries in the collection, so ensuring that hook indexes are unique
+// and non-negative.
+
+int
+ServerHooks::registerHook(const string& name) {
+
+ // Determine index for the new element and insert.
+ int index = hooks_.size();
+ pair<HookCollection::iterator, bool> result =
+ hooks_.insert(make_pair(name, index));
+
+ if (!result.second) {
+ // New element was not inserted because an element with the same name
+ // already existed.
+ isc_throw(DuplicateHook, "hook with name " << name <<
+ " is already registered");
+ }
+
+ // Element was inserted, so add to the inverse hooks collection.
+ inverse_hooks_[index] = name;
+
+ // ... and return numeric index.
+ return (index);
+}
+
+// Set ServerHooks object to initial state.
+
+void
+ServerHooks::initialize() {
+
+ // Clear out the name->index and index->name maps.
+ hooks_.clear();
+ inverse_hooks_.clear();
+
+ // Register the pre-defined hooks.
+ int create = registerHook("context_create");
+ int destroy = registerHook("context_destroy");
+
+ // Check registration went as expected.
+ if ((create != CONTEXT_CREATE) || (destroy != CONTEXT_DESTROY)) {
+ isc_throw(Unexpected, "pre-defined hook indexes are not as expected. "
+ "context_create: expected = " << CONTEXT_CREATE <<
+ ", actual = " << create <<
+ ". context_destroy: expected = " << CONTEXT_DESTROY <<
+ ", actual = " << destroy);
+ }
+}
+
+// Reset ServerHooks object to initial state.
+
+void
+ServerHooks::reset() {
+
+ // Clear all hooks then initialize the pre-defined ones.
+ initialize();
+
+ // Log a warning - although this is done during testing, it should never be
+ // seen in a production system.
+ LOG_WARN(hooks_logger, HOOKS_HOOK_LIST_RESET);
+}
+
+// Find the name associated with a hook index.
+
+std::string
+ServerHooks::getName(int index) const {
+
+ // Get iterator to matching element.
+ InverseHookCollection::const_iterator i = inverse_hooks_.find(index);
+ if (i == inverse_hooks_.end()) {
+ isc_throw(NoSuchHook, "hook index " << index << " is not recognised");
+ }
+
+ return (i->second);
+}
+
+// Find the index associated with a hook name.
+
+int
+ServerHooks::getIndex(const string& name) const {
+
+ // Get iterator to matching element.
+ HookCollection::const_iterator i = hooks_.find(name);
+ if (i == hooks_.end()) {
+ isc_throw(NoSuchHook, "hook name " << name << " is not recognised");
+ }
+
+ return (i->second);
+}
+
+// Return vector of hook names. The names are not sorted - it is up to the
+// caller to perform sorting if required.
+
+vector<string>
+ServerHooks::getHookNames() const {
+
+ vector<string> names;
+ HookCollection::const_iterator i;
+ for (i = hooks_.begin(); i != hooks_.end(); ++i) {
+ names.push_back(i->first);
+ }
+
+ return (names);
+}
+
+// Return global ServerHooks object
+
+ServerHooks&
+ServerHooks::getServerHooks() {
+ static ServerHooks hooks;
+ return (hooks);
+}
+
+
+} // namespace util
+} // namespace isc
diff --git a/src/lib/hooks/server_hooks.h b/src/lib/hooks/server_hooks.h
new file mode 100644
index 0000000..c4a7ae8
--- /dev/null
+++ b/src/lib/hooks/server_hooks.h
@@ -0,0 +1,183 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef SERVER_HOOKS_H
+#define SERVER_HOOKS_H
+
+#include <exceptions/exceptions.h>
+
+#include <boost/noncopyable.hpp>
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace isc {
+namespace hooks {
+
+/// @brief Duplicate hook
+///
+/// Thrown if an attempt is made to register a hook with the same name as a
+/// previously-registered hook.
+class DuplicateHook : public Exception {
+public:
+ DuplicateHook(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// @brief Invalid hook
+///
+/// Thrown if an attempt is made to get the index for an invalid hook.
+class NoSuchHook : public Exception {
+public:
+ NoSuchHook(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+
+/// @brief Server hook collection
+///
+/// This class is used by the server-side code to register hooks - points in the
+/// server processing at which libraries can register functions (callouts) that
+/// the server will call. These functions can modify data and so affect the
+/// processing of the server.
+///
+/// The ServerHooks class is little more than a wrapper around the std::map
+/// class. It stores a hook, assigning to it a unique index number. This
+/// number is then used by the server code to identify the hook being called.
+/// (Although it would be feasible to use a name as an index, using an integer
+/// will speed up the time taken to locate the callouts, which may make a
+/// difference in a frequently-executed piece of code.)
+///
+/// ServerHooks is a singleton object and is only accessible by the static
+/// method getServerHooks().
+
+class ServerHooks : public boost::noncopyable {
+public:
+
+ /// Index numbers for pre-defined hooks.
+ static const int CONTEXT_CREATE = 0;
+ static const int CONTEXT_DESTROY = 1;
+
+ /// @brief Reset to Initial State
+ ///
+ /// Resets the collection of hooks to the initial state, with just the
+ /// context_create and context_destroy hooks set. This used during
+ /// testing to reset the global ServerHooks object; it should never be
+ /// used in production.
+ ///
+ /// @throws isc::Unexpected if the registration of the pre-defined hooks
+ /// fails in some way.
+ void reset();
+
+ /// @brief Register a hook
+ ///
+ /// Registers a hook and returns the hook index.
+ ///
+ /// @param name Name of the hook
+ ///
+ /// @return Index of the hook, to be used in subsequent hook-related calls.
+ /// This will be greater than or equal to zero (so allowing a
+ /// negative value to indicate an invalid index).
+ ///
+ /// @throws DuplicateHook A hook with the same name has already been
+ /// registered.
+ int registerHook(const std::string& name);
+
+ /// @brief Get hook name
+ ///
+ /// Returns the name of a hook given the index. This is most likely to be
+ /// used in log messages.
+ ///
+ /// @param index Index of the hoold
+ ///
+ /// @return Name of the hook.
+ ///
+ /// @throw NoSuchHook if the hook index is invalid.
+ std::string getName(int index) const;
+
+ /// @brief Get hook index
+ ///
+ /// Returns the index of a hook.
+ ///
+ /// @param name Name of the hook
+ ///
+ /// @return Index of the hook, to be used in subsequent calls.
+ ///
+ /// @throw NoSuchHook if the hook name is unknown to the caller.
+ int getIndex(const std::string& name) const;
+
+ /// @brief Return number of hooks
+ ///
+ /// Returns the total number of hooks registered.
+ ///
+ /// @return Number of hooks registered.
+ int getCount() const {
+ return (hooks_.size());
+ }
+
+ /// @brief Get hook names
+ ///
+ /// Return list of hooks registered in the object.
+ ///
+ /// @return Vector of strings holding hook names.
+ std::vector<std::string> getHookNames() const;
+
+ /// @brief Return ServerHooks object
+ ///
+ /// Returns the global ServerHooks object.
+ ///
+ /// @return Reference to the global ServerHooks object.
+ static ServerHooks& getServerHooks();
+
+private:
+ /// @brief Constructor
+ ///
+ /// This pre-registers two hooks, context_create and context_destroy, which
+ /// are called by the server before processing a packet and after processing
+ /// for the packet has completed. They allow the server code to allocate
+ /// and destroy per-packet context.
+ ///
+ /// The constructor is declared private to enforce the singleton nature of
+ /// the object. A reference to the singleton is obtainable through the
+ /// getServerHooks() static method.
+ ///
+ /// @throws isc::Unexpected if the registration of the pre-defined hooks
+ /// fails in some way.
+ ServerHooks();
+
+ /// @brief Initialize hooks
+ ///
+ /// Sets the collection of hooks to the initial state, with just the
+ /// context_create and context_destroy hooks set. This is used during
+ /// construction.
+ ///
+ /// @throws isc::Unexpected if the registration of the pre-defined hooks
+ /// fails in some way.
+ void initialize();
+
+ /// Useful typedefs.
+ typedef std::map<std::string, int> HookCollection;
+ typedef std::map<int, std::string> InverseHookCollection;
+
+ /// Two maps, one for name->index, the other for index->name. (This is
+ /// simpler than using a multi-indexed container.)
+ HookCollection hooks_; ///< Hook name/index collection
+ InverseHookCollection inverse_hooks_; ///< Hook index/name collection
+};
+
+} // namespace util
+} // namespace isc
+
+#endif // SERVER_HOOKS_H
diff --git a/src/lib/hooks/tests/.gitignore b/src/lib/hooks/tests/.gitignore
new file mode 100644
index 0000000..6fa0ec3
--- /dev/null
+++ b/src/lib/hooks/tests/.gitignore
@@ -0,0 +1,4 @@
+/marker_file.h
+/test_libraries.h
+
+/run_unittests
diff --git a/src/lib/hooks/tests/Makefile.am b/src/lib/hooks/tests/Makefile.am
new file mode 100644
index 0000000..36b6287
--- /dev/null
+++ b/src/lib/hooks/tests/Makefile.am
@@ -0,0 +1,146 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES) $(MULTITHREADING_FLAG)
+
+# Some versions of GCC warn about some versions of Boost regarding
+# missing initializer for members in its posix_time.
+# https://svn.boost.org/trac/boost/ticket/3477
+# But older GCC compilers don't have the flag.
+AM_CXXFLAGS = $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
+
+# BIND 10 libraries against which the test user libraries are linked.
+HOOKS_LIB = $(top_builddir)/src/lib/hooks/libb10-hooks.la
+LOG_LIB = $(top_builddir)/src/lib/log/libb10-log.la
+EXCEPTIONS_LIB = $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+UTIL_LIB = $(top_builddir)/src/lib/util/libb10-util.la
+THREADS_LIB = $(top_builddir)/src/lib/util/threads/libb10-threads.la
+
+ALL_LIBS = $(HOOKS_LIB) $(LOG_LIB) $(EXCEPTIONS_LIB) $(UTIL_LIB) $(THREADS_LIB)
+
+if USE_CLANGPP
+# see ../Makefile.am
+AM_CXXFLAGS += -Wno-unused-parameter
+endif
+
+# Files to clean include the file created by testing.
+CLEANFILES = *.gcno *.gcda $(builddir)/marker_file.dat
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+# Build shared libraries for testing. The libtool way to create a shared library
+# is to specify "-avoid-version -export-dynamic -module" in the library LDFLAGS
+# (see http://www.gnu.org/software/libtool/manual/html_node/Link-mode.html).
+# Use of these switches will guarantee that the .so files are created in the
+# .libs folder and they can be dlopened.
+# Note that the shared libraries with callouts should not be used together with
+# the --enable-static-link option. With this option, the bind10 libraries are
+# statically linked with the program and if the callout invokes the methods
+# which belong to these libraries, the library with the callout will get its
+# own copy of the static objects (e.g. logger, ServerHooks) and that will lead
+# to unexpected errors. For this reason, the --enable-static-link option is
+# ignored for unit tests built here.
+
+lib_LTLIBRARIES = libnvl.la libivl.la libfxl.la libbcl.la liblcl.la liblecl.la \
+ libucl.la libfcl.la
+
+# No version function
+libnvl_la_SOURCES = no_version_library.cc
+libnvl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libnvl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libnvl_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+# Incorrect version function
+libivl_la_SOURCES = incorrect_version_library.cc
+libivl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libivl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libivl_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+# All framework functions throw an exception
+libfxl_la_SOURCES = framework_exception_library.cc
+libfxl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libfxl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libfxl_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+# The basic callout library - contains standard callouts
+libbcl_la_SOURCES = basic_callout_library.cc
+libbcl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libbcl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libbcl_la_LDFLAGS = -avoid-version -export-dynamic -module
+libbcl_la_LIBADD = $(ALL_LIBS)
+
+# The load callout library - contains a load function
+liblcl_la_SOURCES = load_callout_library.cc
+liblcl_la_CXXFLAGS = $(AM_CXXFLAGS)
+liblcl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+liblcl_la_LDFLAGS = -avoid-version -export-dynamic -module
+liblcl_la_LIBADD = $(ALL_LIBS)
+
+# The load error callout library - contains a load function that returns
+# an error.
+liblecl_la_SOURCES = load_error_callout_library.cc
+liblecl_la_CXXFLAGS = $(AM_CXXFLAGS)
+liblecl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+liblecl_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+# The unload callout library - contains an unload function that
+# creates a marker file.
+libucl_la_SOURCES = unload_callout_library.cc
+libucl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libucl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libucl_la_LDFLAGS = -avoid-version -export-dynamic -module
+
+# The full callout library - contains all three framework functions.
+libfcl_la_SOURCES = full_callout_library.cc
+libfcl_la_CXXFLAGS = $(AM_CXXFLAGS)
+libfcl_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
+libfcl_la_LDFLAGS = -avoid-version -export-dynamic -module
+libfcl_la_LIBADD = $(ALL_LIBS)
+
+TESTS += run_unittests
+run_unittests_SOURCES = run_unittests.cc
+run_unittests_SOURCES += callout_handle_unittest.cc
+run_unittests_SOURCES += callout_manager_unittest.cc
+run_unittests_SOURCES += common_test_class.h
+run_unittests_SOURCES += handles_unittest.cc
+run_unittests_SOURCES += hooks_manager_unittest.cc
+run_unittests_SOURCES += library_manager_collection_unittest.cc
+run_unittests_SOURCES += library_manager_unittest.cc
+run_unittests_SOURCES += server_hooks_unittest.cc
+
+nodist_run_unittests_SOURCES = marker_file.h
+nodist_run_unittests_SOURCES += test_libraries.h
+
+run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+if USE_STATIC_LINK
+run_unittests_LDFLAGS += -static
+endif
+
+run_unittests_LDADD = $(AM_LDADD) $(GTEST_LDADD)
+run_unittests_LDADD += $(ALL_LIBS)
+run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+
+# As noted in configure.ac, libtool doesn't work perfectly with Darwin: it embeds the
+# final install path in dynamic libraries and loadable modules refer to that path even
+# if its loaded within the source tree, so preventing tests from working - but only
+# when linking statically. The solution used in other Makefiles (setting the path
+# to the dynamic libraries via an environment variable) don't seem to work. What does
+# work is to run the unit test using libtool and specifying paths via -dlopen switches.
+# So... If running in an environment where we have to set the library path AND if
+# linking statically, override the "check" target and run the unit tests ourselves.
+if USE_STATIC_LINK
+if SET_ENV_LIBRARY_PATH
+check-TESTS:
+ $(LIBTOOL) --mode=execute -dlopen $(HOOKS_LIB) -dlopen $(LOG_LIB) -dlopen $(EXCEPTIONS_LIB) -dlopen $(UTIL_LIB) -dlopen $(THREADS_LIB) ./run_unittests
+endif
+endif
+
+endif
+
+noinst_PROGRAMS = $(TESTS)
+
+EXTRA_DIST = marker_file.h.in test_libraries.h.in
diff --git a/src/lib/hooks/tests/basic_callout_library.cc b/src/lib/hooks/tests/basic_callout_library.cc
new file mode 100644
index 0000000..0a81f23
--- /dev/null
+++ b/src/lib/hooks/tests/basic_callout_library.cc
@@ -0,0 +1,124 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Basic callout library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - Only the "version" framework function is supplied.
+///
+/// - A context_create callout is supplied.
+///
+/// - Three "standard" callouts are supplied corresponding to the hooks
+/// "hookpt_one", "hookpt_two", "hookpt_three". All do some trivial
+/// calculations on the arguments supplied to it and the context variables,
+/// returning intermediate results through the "result" argument. The result
+/// of executing all four callouts in order is:
+///
+/// @f[ (10 + data_1) * data_2 - data_3 @f]
+///
+/// ...where data_1, data_2 and data_3 are the values passed in arguments of
+/// the same name to the three callouts (data_1 passed to hookpt_one, data_2
+/// to hookpt_two etc.) and the result is returned in the argument "result".
+
+#include <hooks/hooks.h>
+#include <fstream>
+
+using namespace isc::hooks;
+using namespace std;
+
+extern "C" {
+
+// Callouts. All return their result through the "result" argument.
+
+int
+context_create(CalloutHandle& handle) {
+ handle.setContext("result", static_cast<int>(10));
+ handle.setArgument("result", static_cast<int>(10));
+ return (0);
+}
+
+// First callout adds the passed "data_1" argument to the initialized context
+// value of 10. (Note that the value set by context_create is accessed through
+// context and not the argument, so checking that context is correctly passed
+// between callouts in the same library.)
+
+int
+hookpt_one(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_1", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result += data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Second callout multiplies the current context value by the "data_2"
+// argument.
+
+int
+hookpt_two(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_2", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result *= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Final callout subtracts the result in "data_3".
+
+int
+hookpt_three(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_3", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result -= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Framework functions.
+
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+// load() initializes the user library if the main image was statically linked.
+int
+load(isc::hooks::LibraryHandle&) {
+#ifdef USE_STATIC_LINK
+ hooksStaticLinkInit();
+#endif
+ return (0);
+}
+
+}
+
diff --git a/src/lib/hooks/tests/callout_handle_unittest.cc b/src/lib/hooks/tests/callout_handle_unittest.cc
new file mode 100644
index 0000000..b24a4cf
--- /dev/null
+++ b/src/lib/hooks/tests/callout_handle_unittest.cc
@@ -0,0 +1,329 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/server_hooks.h>
+
+#include <boost/shared_ptr.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+/// @file
+/// @brief Holds the CalloutHandle argument tests
+///
+/// Additional testing of the CalloutHandle - together with the interaction
+/// of the LibraryHandle - is done in the handles_unittests set of tests.
+
+class CalloutHandleTest : public ::testing::Test {
+public:
+
+ /// @brief Constructor
+ ///
+ /// Sets up a callout manager to be referenced by the CalloutHandle in
+ /// these tests. (The "4" for the number of libraries in the
+ /// CalloutManager is arbitrary - it is not used in these tests.)
+ CalloutHandleTest() : manager_(new CalloutManager(4))
+ {}
+
+ /// Obtain hook manager
+ boost::shared_ptr<CalloutManager>& getCalloutManager() {
+ return (manager_);
+ }
+
+private:
+ /// Callout manager accessed by this CalloutHandle.
+ boost::shared_ptr<CalloutManager> manager_;
+};
+
+// *** Argument Tests ***
+//
+// The first set of tests check that the CalloutHandle can store and retrieve
+// arguments. These are very similar to the LibraryHandle context tests.
+
+// Test that we can store multiple values of the same type and that they
+// are distinct.
+
+TEST_F(CalloutHandleTest, ArgumentDistinctSimpleType) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Store and retrieve an int (random value).
+ int a = 42;
+ handle.setArgument("integer1", a);
+ EXPECT_EQ(42, a);
+
+ int b = 0;
+ handle.getArgument("integer1", b);
+ EXPECT_EQ(42, b);
+
+ // Add another integer (another random value).
+ int c = 142;
+ handle.setArgument("integer2", c);
+ EXPECT_EQ(142, c);
+
+ int d = 0;
+ handle.getArgument("integer2", d);
+ EXPECT_EQ(142, d);
+
+ // Add a short (random value).
+ short e = -81;
+ handle.setArgument("short", e);
+ EXPECT_EQ(-81, e);
+
+ short f = 0;
+ handle.getArgument("short", f);
+ EXPECT_EQ(-81, f);
+}
+
+// Test that trying to get an unknown argument throws an exception.
+
+TEST_F(CalloutHandleTest, ArgumentUnknownName) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Set an integer
+ int a = 42;
+ handle.setArgument("integer1", a);
+ EXPECT_EQ(42, a);
+
+ // Check we can retrieve it
+ int b = 0;
+ handle.getArgument("integer1", b);
+ EXPECT_EQ(42, b);
+
+ // Check that getting an unknown name throws an exception.
+ int c = 0;
+ EXPECT_THROW(handle.getArgument("unknown", c), NoSuchArgument);
+}
+
+// Test that trying to get an argument with an incorrect type throws an
+// exception.
+
+TEST_F(CalloutHandleTest, ArgumentIncorrectType) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Set an integer
+ int a = 42;
+ handle.setArgument("integer1", a);
+ EXPECT_EQ(42, a);
+
+ // Check we can retrieve it
+ long b = 0;
+ EXPECT_THROW(handle.getArgument("integer1", b), boost::bad_any_cast);
+}
+
+// Now try with some very complex types. The types cannot be defined within
+// the function and they should contain a copy constructor. For this reason,
+// a simple "struct" is used.
+
+struct Alpha {
+ int a;
+ int b;
+ Alpha(int first = 0, int second = 0) : a(first), b(second) {}
+};
+
+struct Beta {
+ int c;
+ int d;
+ Beta(int first = 0, int second = 0) : c(first), d(second) {}
+};
+
+TEST_F(CalloutHandleTest, ComplexTypes) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Declare two variables of different (complex) types. (Note as to the
+ // variable names: aleph and beth are the first two letters of the Hebrew
+ // alphabet.)
+ Alpha aleph(1, 2);
+ EXPECT_EQ(1, aleph.a);
+ EXPECT_EQ(2, aleph.b);
+ handle.setArgument("aleph", aleph);
+
+ Beta beth(11, 22);
+ EXPECT_EQ(11, beth.c);
+ EXPECT_EQ(22, beth.d);
+ handle.setArgument("beth", beth);
+
+ // Ensure we can extract the data correctly.
+ Alpha aleph2;
+ EXPECT_EQ(0, aleph2.a);
+ EXPECT_EQ(0, aleph2.b);
+ handle.getArgument("aleph", aleph2);
+ EXPECT_EQ(1, aleph2.a);
+ EXPECT_EQ(2, aleph2.b);
+
+ Beta beth2;
+ EXPECT_EQ(0, beth2.c);
+ EXPECT_EQ(0, beth2.d);
+ handle.getArgument("beth", beth2);
+ EXPECT_EQ(11, beth2.c);
+ EXPECT_EQ(22, beth2.d);
+
+ // Ensure that complex types also thrown an exception if we attempt to
+ // get a context element of the wrong type.
+ EXPECT_THROW(handle.getArgument("aleph", beth), boost::bad_any_cast);
+}
+
+// Check that the context can store pointers. And also check that it respects
+// that a "pointer to X" is not the same as a "pointer to const X".
+
+TEST_F(CalloutHandleTest, PointerTypes) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Declare a couple of variables, const and non-const.
+ Alpha aleph(5, 10);
+ const Beta beth(15, 20);
+
+ Alpha* pa = ℵ
+ const Beta* pcb = ℶ
+
+ // Check pointers can be set and retrieved OK.
+ handle.setArgument("non_const_pointer", pa);
+ handle.setArgument("const_pointer", pcb);
+
+ Alpha* pa2 = 0;
+ handle.getArgument("non_const_pointer", pa2);
+ EXPECT_TRUE(pa == pa2);
+
+ const Beta* pcb2 = 0;
+ handle.getArgument("const_pointer", pcb2);
+ EXPECT_TRUE(pcb == pcb2);
+
+ // Check that the "const" is protected in the context.
+ const Alpha* pca3;
+ EXPECT_THROW(handle.getArgument("non_const_pointer", pca3),
+ boost::bad_any_cast);
+
+ Beta* pb3;
+ EXPECT_THROW(handle.getArgument("const_pointer", pb3),
+ boost::bad_any_cast);
+}
+
+// Check that we can get the names of the arguments.
+
+TEST_F(CalloutHandleTest, ContextItemNames) {
+ CalloutHandle handle(getCalloutManager());
+
+ vector<string> expected_names;
+
+ expected_names.push_back("faith");
+ handle.setArgument("faith", 42);
+ expected_names.push_back("hope");
+ handle.setArgument("hope", 43);
+ expected_names.push_back("charity");
+ handle.setArgument("charity", 44);
+
+ // Get the names and check against the expected names. We'll sort
+ // both arrays to simplify the checking.
+ vector<string> actual_names = handle.getArgumentNames();
+
+ sort(actual_names.begin(), actual_names.end());
+ sort(expected_names.begin(), expected_names.end());
+ EXPECT_TRUE(expected_names == actual_names);
+}
+
+// Test that we can delete an argument.
+
+TEST_F(CalloutHandleTest, DeleteArgument) {
+ CalloutHandle handle(getCalloutManager());
+
+ int one = 1;
+ int two = 2;
+ int three = 3;
+ int four = 4;
+ int value; // Return value
+
+ handle.setArgument("one", one);
+ handle.setArgument("two", two);
+ handle.setArgument("three", three);
+ handle.setArgument("four", four);
+
+ // Delete "one".
+ handle.getArgument("one", value);
+ EXPECT_EQ(1, value);
+ handle.deleteArgument("one");
+
+ EXPECT_THROW(handle.getArgument("one", value), NoSuchArgument);
+ handle.getArgument("two", value);
+ EXPECT_EQ(2, value);
+ handle.getArgument("three", value);
+ EXPECT_EQ(3, value);
+ handle.getArgument("four", value);
+ EXPECT_EQ(4, value);
+
+ // Delete "three".
+ handle.getArgument("three", value);
+ EXPECT_EQ(3, value);
+ handle.deleteArgument("three");
+
+ EXPECT_THROW(handle.getArgument("one", value), NoSuchArgument);
+ handle.getArgument("two", value);
+ EXPECT_EQ(2, value);
+ EXPECT_THROW(handle.getArgument("three", value), NoSuchArgument);
+ handle.getArgument("four", value);
+ EXPECT_EQ(4, value);
+}
+
+// Test that we can delete all arguments.
+
+TEST_F(CalloutHandleTest, DeleteAllArguments) {
+ CalloutHandle handle(getCalloutManager());
+
+ int one = 1;
+ int two = 2;
+ int three = 3;
+ int four = 4;
+ int value; // Return value
+
+ // Set the arguments. The previous test verifies that this works.
+ handle.setArgument("one", one);
+ handle.setArgument("two", two);
+ handle.setArgument("three", three);
+ handle.setArgument("four", four);
+
+ // Delete all arguments...
+ handle.deleteAllArguments();
+
+ // ... and check that none are left.
+ EXPECT_THROW(handle.getArgument("one", value), NoSuchArgument);
+ EXPECT_THROW(handle.getArgument("two", value), NoSuchArgument);
+ EXPECT_THROW(handle.getArgument("three", value), NoSuchArgument);
+ EXPECT_THROW(handle.getArgument("four", value), NoSuchArgument);
+}
+
+// Test the "skip" flag.
+
+TEST_F(CalloutHandleTest, SkipFlag) {
+ CalloutHandle handle(getCalloutManager());
+
+ // Should be false on construction.
+ EXPECT_FALSE(handle.getSkip());
+
+ handle.setSkip(true);
+ EXPECT_TRUE(handle.getSkip());
+
+ handle.setSkip(false);
+ EXPECT_FALSE(handle.getSkip());
+}
+
+// Further tests of the "skip" flag and tests of getting the name of the
+// hook to which the current callout is attached is in the "handles_unittest"
+// module.
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/callout_manager_unittest.cc b/src/lib/hooks/tests/callout_manager_unittest.cc
new file mode 100644
index 0000000..c3f3b1d
--- /dev/null
+++ b/src/lib/hooks/tests/callout_manager_unittest.cc
@@ -0,0 +1,882 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/server_hooks.h>
+
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <climits>
+#include <string>
+#include <vector>
+
+/// @file
+/// @brief CalloutManager and LibraryHandle tests
+///
+/// These set of tests check the CalloutManager and LibraryHandle. They are
+/// together in the same file because the LibraryHandle is little more than a
+/// restricted interface to the CalloutManager, and a lot of the support
+/// structure for the tests is common.
+
+using namespace isc;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+class CalloutManagerTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ///
+ /// Sets up a collection of three LibraryHandle objects to use in the test.
+ CalloutManagerTest() {
+
+ // Set up the server hooks. There is sone singleton for all tests,
+ // so reset it and explicitly set up the hooks for the test.
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+ alpha_index_ = hooks.registerHook("alpha");
+ beta_index_ = hooks.registerHook("beta");
+ gamma_index_ = hooks.registerHook("gamma");
+ delta_index_ = hooks.registerHook("delta");
+
+ // Set up the callout manager with these hooks. Assume a maximum of
+ // four libraries.
+ callout_manager_.reset(new CalloutManager(10));
+
+ // Set up the callout handle.
+ callout_handle_.reset(new CalloutHandle(callout_manager_));
+
+ // Initialize the static variable.
+ callout_value_ = 0;
+ }
+
+ /// @brief Return the callout handle
+ CalloutHandle& getCalloutHandle() {
+ return (*callout_handle_);
+ }
+
+ /// @brief Return the callout manager
+ boost::shared_ptr<CalloutManager> getCalloutManager() {
+ return (callout_manager_);
+ }
+
+ /// Static variable used for accumulating information
+ static int callout_value_;
+
+ /// Hook indexes. These are somewhat ubiquitous, so are made public for
+ /// ease of reference instead of being accessible by a function.
+ int alpha_index_;
+ int beta_index_;
+ int gamma_index_;
+ int delta_index_;
+
+private:
+ /// Callout handle used in calls
+ boost::shared_ptr<CalloutHandle> callout_handle_;
+
+ /// Callout manager used for the test
+ boost::shared_ptr<CalloutManager> callout_manager_;
+};
+
+// Definition of the static variable.
+int CalloutManagerTest::callout_value_ = 0;
+
+// Callout definitions
+//
+// The callouts defined here are structured in such a way that it is possible
+// to determine the order in which they are called and whether they are called
+// at all. The method used is simple - after a sequence of callouts, the digits
+// in the value, reading left to right, determines the order of the callouts
+// called. For example, callout one followed by two followed by three followed
+// by two followed by one results in a value of 12321.
+//
+// Functions return a zero to indicate success.
+
+extern "C" {
+int callout_general(int number) {
+ CalloutManagerTest::callout_value_ =
+ 10 * CalloutManagerTest::callout_value_ + number;
+ return (0);
+}
+
+int callout_one(CalloutHandle&) {
+ return (callout_general(1));
+}
+
+int callout_two(CalloutHandle&) {
+ return (callout_general(2));
+}
+
+int callout_three(CalloutHandle&) {
+ return (callout_general(3));
+}
+
+int callout_four(CalloutHandle&) {
+ return (callout_general(4));
+}
+
+int callout_five(CalloutHandle&) {
+ return (callout_general(5));
+}
+
+int callout_six(CalloutHandle&) {
+ return (callout_general(6));
+}
+
+int callout_seven(CalloutHandle&) {
+ return (callout_general(7));
+}
+
+// The next functions are duplicates of some of the above, but return an error.
+
+int callout_one_error(CalloutHandle& handle) {
+ (void) callout_one(handle);
+ return (1);
+}
+
+int callout_two_error(CalloutHandle& handle) {
+ (void) callout_two(handle);
+ return (1);
+}
+
+int callout_three_error(CalloutHandle& handle) {
+ (void) callout_three(handle);
+ return (1);
+}
+
+int callout_four_error(CalloutHandle& handle) {
+ (void) callout_four(handle);
+ return (1);
+}
+
+}; // extern "C"
+
+// *** Callout Tests ***
+//
+// The next set of tests check that callouts can be called.
+
+// Constructor - check that we trap bad parameters.
+
+TEST_F(CalloutManagerTest, BadConstructorParameters) {
+ boost::scoped_ptr<CalloutManager> cm;
+
+ // Invalid number of libraries
+ EXPECT_THROW(cm.reset(new CalloutManager(-1)), BadValue);
+}
+
+// Check the number of libraries is reported successfully.
+
+TEST_F(CalloutManagerTest, NumberOfLibraries) {
+ boost::scoped_ptr<CalloutManager> cm;
+
+ // Check two valid values of number of libraries to ensure that the
+ // GetNumLibraries() returns the value set.
+ EXPECT_NO_THROW(cm.reset(new CalloutManager()));
+ EXPECT_EQ(0, cm->getNumLibraries());
+
+ EXPECT_NO_THROW(cm.reset(new CalloutManager(0)));
+ EXPECT_EQ(0, cm->getNumLibraries());
+
+ EXPECT_NO_THROW(cm.reset(new CalloutManager(4)));
+ EXPECT_EQ(4, cm->getNumLibraries());
+
+ EXPECT_NO_THROW(cm.reset(new CalloutManager(42)));
+ EXPECT_EQ(42, cm->getNumLibraries());
+}
+
+// Check that we can only set the current library index to the correct values.
+
+TEST_F(CalloutManagerTest, CheckLibraryIndex) {
+ // Check valid indexes. As the callout manager is sized for 10 libraries,
+ // we expect:
+ //
+ // -1 to be valid as it is the standard "invalid" value.
+ // 0 to be valid for the pre-user library callouts
+ // 1-10 to be valid for the user-library callouts
+ // INT_MAX to be valid for the post-user library callouts
+ //
+ // All other values to be invalid.
+ for (int i = -1; i < 11; ++i) {
+ EXPECT_NO_THROW(getCalloutManager()->setLibraryIndex(i));
+ EXPECT_EQ(i, getCalloutManager()->getLibraryIndex());
+ }
+ EXPECT_NO_THROW(getCalloutManager()->setLibraryIndex(INT_MAX));
+ EXPECT_EQ(INT_MAX, getCalloutManager()->getLibraryIndex());
+
+ // Check invalid ones
+ EXPECT_THROW(getCalloutManager()->setLibraryIndex(-2), NoSuchLibrary);
+ EXPECT_THROW(getCalloutManager()->setLibraryIndex(11), NoSuchLibrary);
+}
+
+// Check that we can only register callouts on valid hook names.
+
+TEST_F(CalloutManagerTest, ValidHookNames) {
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_NO_THROW(getCalloutManager()->registerCallout("alpha", callout_one));
+ EXPECT_THROW(getCalloutManager()->registerCallout("unknown", callout_one),
+ NoSuchHook);
+}
+
+
+// Check we can register callouts appropriately.
+
+TEST_F(CalloutManagerTest, RegisterCallout) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+
+ // Set up so that hooks "alpha" and "beta" have callouts attached from a
+ // different libraries.
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("beta", callout_two);
+
+ // Check all is as expected.
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Check that calling the callouts returns as expected. (This is also a
+ // test of the callCallouts method.)
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1, callout_value_);
+
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(2, callout_value_);
+
+ // Register some more callouts from different libraries on hook "alpha".
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("alpha", callout_five);
+
+ // Check it is as expected.
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1345, callout_value_);
+
+ // ... and check the additional callouts were not registered on the "beta"
+ // hook.
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(2, callout_value_);
+
+ // Add another callout to hook "alpha" from library index 2 - this should
+ // appear at the end of the callout list for that library.
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_six);
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(13465, callout_value_);
+
+ // Add a callout from library index 1 - this should appear between the
+ // callouts from library index 0 and linrary index 2.
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_seven);
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(173465, callout_value_);
+}
+
+// Check the "calloutsPresent()" method.
+
+TEST_F(CalloutManagerTest, CalloutsPresent) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Set up so that hooks "alpha", "beta" and "delta" have callouts attached
+ // to them, and callout "gamma" does not. (In the statements below, the
+ // exact callouts attached to a hook are not relevant - only the fact
+ // that some callouts are). Chose the libraries for which the callouts
+ // are registered randomly.
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->registerCallout("beta", callout_two);
+
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("delta", callout_four);
+
+ // Check all is as expected.
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Check we fail on an invalid hook index.
+ EXPECT_THROW(getCalloutManager()->calloutsPresent(42), NoSuchHook);
+ EXPECT_THROW(getCalloutManager()->calloutsPresent(-1), NoSuchHook);
+}
+
+// Test that calling a hook with no callouts on it returns success.
+
+TEST_F(CalloutManagerTest, CallNoCallouts) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Call the callouts on an arbitrary hook and ensure that nothing happens.
+ callout_value_ = 475;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(475, callout_value_); // Unchanged
+}
+
+// Test that the callouts are called in the correct order (i.e. the callouts
+// from the first library in the order they were registered, then the callouts
+// from the second library in the order they were registered etc.)
+
+TEST_F(CalloutManagerTest, CallCalloutsSuccess) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Each library contributes one callout on hook "alpha".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ // Do a random selection of callouts on hook "beta".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("beta", callout_one);
+ getCalloutManager()->registerCallout("beta", callout_three);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("beta", callout_two);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("beta", callout_four);
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(1324, callout_value_);
+
+ // Ensure that calling the callouts on a hook with no callouts works.
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(gamma_index_, getCalloutHandle());
+ EXPECT_EQ(0, callout_value_);
+}
+
+// Test that the callouts are called in order, but that callouts occurring
+// after a callout that returns an error are not called.
+//
+// (Note: in this test, the callouts that return an error set the value of
+// callout_value_ before they return the error code.)
+
+TEST_F(CalloutManagerTest, CallCalloutsError) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Each library contributing one callout on hook "alpha". The first callout
+ // returns an error (after adding its value to the result).
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one_error);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ // Each library contributing multiple callouts on hook "beta". The last
+ // callout on the first library returns an error.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("beta", callout_one);
+ getCalloutManager()->registerCallout("beta", callout_one_error);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("beta", callout_two);
+ getCalloutManager()->registerCallout("beta", callout_two);
+ getCalloutManager()->registerCallout("beta", callout_three);
+ getCalloutManager()->registerCallout("beta", callout_three);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("beta", callout_four);
+ getCalloutManager()->registerCallout("beta", callout_four);
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(11223344, callout_value_);
+
+ // A callout in a random position in the callout list returns an error.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("gamma", callout_one);
+ getCalloutManager()->registerCallout("gamma", callout_one);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("gamma", callout_two);
+ getCalloutManager()->registerCallout("gamma", callout_two);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("gamma", callout_four_error);
+ getCalloutManager()->registerCallout("gamma", callout_four);
+ getCalloutManager()->callCallouts(gamma_index_, getCalloutHandle());
+ EXPECT_EQ(112244, callout_value_);
+
+ // The last callout on a hook returns an error.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("delta", callout_one);
+ getCalloutManager()->registerCallout("delta", callout_one);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("delta", callout_two);
+ getCalloutManager()->registerCallout("delta", callout_two);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("delta", callout_three);
+ getCalloutManager()->registerCallout("delta", callout_three);
+ getCalloutManager()->setLibraryIndex(3);
+ getCalloutManager()->registerCallout("delta", callout_four);
+ getCalloutManager()->registerCallout("delta", callout_four_error);
+ getCalloutManager()->callCallouts(delta_index_, getCalloutHandle());
+ EXPECT_EQ(11223344, callout_value_);
+}
+
+// Now test that we can deregister a single callout on a hook.
+
+TEST_F(CalloutManagerTest, DeregisterSingleCallout) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Add a callout to hook "alpha" and check it is added correctly.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(2, callout_value_);
+
+ // Remove it and check that the no callouts are present. We have to reset
+ // the current library index here as it was invalidated by the call
+ // to callCallouts().
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+}
+
+// Now test that we can deregister a single callout on a hook that has multiple
+// callouts from the same library.
+
+TEST_F(CalloutManagerTest, DeregisterSingleCalloutSameLibrary) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Add multiple callouts to hook "alpha".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ // Remove the callout_two callout. We have to reset the current library
+ // index here as it was invalidated by the call to callCallouts().
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(134, callout_value_);
+
+ // Try removing it again.
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_FALSE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(134, callout_value_);
+
+}
+
+// Check we can deregister multiple callouts from the same library.
+
+TEST_F(CalloutManagerTest, DeregisterMultipleCalloutsSameLibrary) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Each library contributes one callout on hook "alpha".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(12123434, callout_value_);
+
+ // Remove the callout_two callouts. We have to reset the current library
+ // index here as it was invalidated by the call to callCallouts().
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(113434, callout_value_);
+
+ // Try removing multiple callouts that includes one at the end of the
+ // list of callouts.
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_four));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1133, callout_value_);
+
+ // ... and from the start.
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_one));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(33, callout_value_);
+
+ // ... and the remaining callouts.
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_three));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(0, callout_value_);
+
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+}
+
+// Check we can deregister multiple callouts from multiple libraries.
+
+TEST_F(CalloutManagerTest, DeregisterMultipleCalloutsMultipleLibraries) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Each library contributes two callouts to hook "alpha".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_five);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(123452, callout_value_);
+
+ // Remove the callout_two callout from library 0. It should not affect
+ // the second callout_two callout registered by library 2.
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(13452, callout_value_);
+}
+
+// Check we can deregister all callouts from a single library.
+
+TEST_F(CalloutManagerTest, DeregisterAllCallouts) {
+ // Ensure that no callouts are attached to hook one.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+
+ // Each library contributes two callouts to hook "alpha".
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_five);
+ getCalloutManager()->registerCallout("alpha", callout_six);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(123456, callout_value_);
+
+ // Remove all callouts from library index 1.
+ getCalloutManager()->setLibraryIndex(1);
+ EXPECT_TRUE(getCalloutManager()->deregisterAllCallouts("alpha"));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1256, callout_value_);
+
+ // Remove all callouts from library index 2.
+ getCalloutManager()->setLibraryIndex(2);
+ EXPECT_TRUE(getCalloutManager()->deregisterAllCallouts("alpha"));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(12, callout_value_);
+}
+
+// Check that we can register/deregister callouts on different libraries
+// and different hooks, and that the callout instances are regarded as
+// unique and do not affect one another.
+
+TEST_F(CalloutManagerTest, MultipleCalloutsLibrariesHooks) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Register callouts on the alpha hook.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_one);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout_three);
+ getCalloutManager()->registerCallout("alpha", callout_four);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout_five);
+ getCalloutManager()->registerCallout("alpha", callout_two);
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(123452, callout_value_);
+
+ // Register the same callouts on the beta hook, and check that those
+ // on the alpha hook are not affected.
+ callout_value_ = 0;
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("beta", callout_five);
+ getCalloutManager()->registerCallout("beta", callout_one);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("beta", callout_four);
+ getCalloutManager()->registerCallout("beta", callout_three);
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(5143, callout_value_);
+
+ // Check that the order of callouts on the alpha hook has not been affected.
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(123452, callout_value_);
+
+ // Remove callout four from beta and check that alpha is not affected.
+ getCalloutManager()->setLibraryIndex(2);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("beta", callout_four));
+
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(beta_index_, getCalloutHandle());
+ EXPECT_EQ(513, callout_value_);
+
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(123452, callout_value_);
+}
+
+// Library handle tests. As by inspection the LibraryHandle can be seen to be
+// little more than shell around CalloutManager, only a basic set of tests
+// is done concerning registration and deregistration of functions.
+//
+// More extensive tests (i.e. checking that when a callout is called it can
+// only register and deregister callouts within its library) require that
+// the CalloutHandle object pass the appropriate LibraryHandle to the
+// callout. These tests are done in the handles_unittest tests.
+
+TEST_F(CalloutManagerTest, LibraryHandleRegistration) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+
+ // Set up so that hooks "alpha" and "beta" have callouts attached from a
+ // different libraries.
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->getLibraryHandle().registerCallout("alpha",
+ callout_one);
+ getCalloutManager()->getLibraryHandle().registerCallout("alpha",
+ callout_two);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->getLibraryHandle().registerCallout("alpha",
+ callout_three);
+ getCalloutManager()->getLibraryHandle().registerCallout("alpha",
+ callout_four);
+
+ // Check all is as expected.
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Check that calling the callouts returns as expected. (This is also a
+ // test of the callCallouts method.)
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ // Deregister a callout on library index 0 (after we check we can't
+ // deregister it through library index 1).
+ getCalloutManager()->setLibraryIndex(1);
+ EXPECT_FALSE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ getCalloutManager()->setLibraryIndex(0);
+ EXPECT_TRUE(getCalloutManager()->deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(134, callout_value_);
+
+ // Deregister all callouts on library index 1.
+ getCalloutManager()->setLibraryIndex(1);
+ EXPECT_TRUE(getCalloutManager()->deregisterAllCallouts("alpha"));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1, callout_value_);
+}
+
+// A repeat of the test above, but using the alternate constructor for the
+// LibraryHandle.
+TEST_F(CalloutManagerTest, LibraryHandleAlternateConstructor) {
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(alpha_index_));
+
+ // Set up so that hooks "alpha" and "beta" have callouts attached from a
+ // different libraries.
+ LibraryHandle lh0(getCalloutManager().get(), 0);
+ lh0.registerCallout("alpha", callout_one);
+ lh0.registerCallout("alpha", callout_two);
+
+ LibraryHandle lh1(getCalloutManager().get(), 1);
+ lh1.registerCallout("alpha", callout_three);
+ lh1.registerCallout("alpha", callout_four);
+
+ // Check all is as expected.
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Check that calling the callouts returns as expected. (This is also a
+ // test of the callCallouts method.)
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ // Deregister a callout on library index 0 (after we check we can't
+ // deregister it through library index 1).
+ EXPECT_FALSE(lh1.deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1234, callout_value_);
+
+ EXPECT_TRUE(lh0.deregisterCallout("alpha", callout_two));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(134, callout_value_);
+
+ // Deregister all callouts on library index 1.
+ EXPECT_TRUE(lh1.deregisterAllCallouts("alpha"));
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(1, callout_value_);
+}
+
+// Check that the pre- and post- user callout library handles work
+// appropriately with no user libraries.
+
+TEST_F(CalloutManagerTest, LibraryHandlePrePostNoLibraries) {
+ // Create a local callout manager and callout handle to reflect no libraries
+ // being loaded.
+ boost::shared_ptr<CalloutManager> manager(new CalloutManager(0));
+ CalloutHandle handle(manager);
+
+ // Ensure that no callouts are attached to any of the hooks.
+ EXPECT_FALSE(manager->calloutsPresent(alpha_index_));
+
+ // Setup the pre-and post callouts.
+ manager->getPostLibraryHandle().registerCallout("alpha", callout_four);
+ manager->getPreLibraryHandle().registerCallout("alpha", callout_one);
+ // Check all is as expected.
+ EXPECT_TRUE(manager->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(manager->calloutsPresent(beta_index_));
+ EXPECT_FALSE(manager->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(manager->calloutsPresent(delta_index_));
+
+ // Check that calling the callouts returns as expected.
+ callout_value_ = 0;
+ manager->callCallouts(alpha_index_, handle);
+ EXPECT_EQ(14, callout_value_);
+
+ // Deregister the pre- library callout.
+ EXPECT_TRUE(manager->getPreLibraryHandle().deregisterAllCallouts("alpha"));
+ callout_value_ = 0;
+ manager->callCallouts(alpha_index_, handle);
+ EXPECT_EQ(4, callout_value_);
+}
+
+// Repeat the tests with one user library.
+
+TEST_F(CalloutManagerTest, LibraryHandlePrePostUserLibrary) {
+
+ // Setup the pre-, library and post callouts.
+ getCalloutManager()->getPostLibraryHandle().registerCallout("alpha",
+ callout_four);
+ getCalloutManager()->getPreLibraryHandle().registerCallout("alpha",
+ callout_one);
+
+ // ... and set up a callout in between, on library number 2.
+ LibraryHandle lh1(getCalloutManager().get(), 2);
+ lh1.registerCallout("alpha", callout_five);
+
+ // Check all is as expected.
+ EXPECT_TRUE(getCalloutManager()->calloutsPresent(alpha_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(beta_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(gamma_index_));
+ EXPECT_FALSE(getCalloutManager()->calloutsPresent(delta_index_));
+
+ // Check that calling the callouts returns as expected.
+ callout_value_ = 0;
+ getCalloutManager()->callCallouts(alpha_index_, getCalloutHandle());
+ EXPECT_EQ(154, callout_value_);
+}
+
+// The setting of the hook index is checked in the handles_unittest
+// set of tests, as access restrictions mean it is not easily tested
+// on its own.
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/common_test_class.h b/src/lib/hooks/tests/common_test_class.h
new file mode 100644
index 0000000..803e25c
--- /dev/null
+++ b/src/lib/hooks/tests/common_test_class.h
@@ -0,0 +1,142 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef COMMON_HOOKS_TEST_CLASS_H
+#define COMMON_HOOKS_TEST_CLASS_H
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/server_hooks.h>
+#include <hooks/tests/marker_file.h>
+
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+/// @brief Common hooks test class
+///
+/// This class is a shared parent of the test fixture class in the tests of the
+/// higher-level hooks classes (LibraryManager, LibraryManagerCollection and
+/// HooksManager). It
+///
+/// - sets the the ServerHooks object with three hooks and stores their
+/// indexes.
+/// - executes the callouts (which are assumed to perform a calculation)
+/// and checks the results.
+
+class HooksCommonTestClass {
+public:
+ /// @brief Constructor
+ HooksCommonTestClass() {
+
+ // Set up the server hooks. ServerHooks is a singleton, so we reset it
+ // between each test.
+ isc::hooks::ServerHooks& hooks =
+ isc::hooks::ServerHooks::getServerHooks();
+ hooks.reset();
+ hookpt_one_index_ = hooks.registerHook("hookpt_one");
+ hookpt_two_index_ = hooks.registerHook("hookpt_two");
+ hookpt_three_index_ = hooks.registerHook("hookpt_three");
+ }
+
+ /// @brief Call callouts test
+ ///
+ /// All of the loaded libraries for which callouts are called register four
+ /// callouts: a context_create callout and three callouts that are attached
+ /// to hooks hookpt_one, hookpt_two and hookpt_three. These four callouts,
+ /// executed in sequence, perform a series of calculations. Data is passed
+ /// between callouts in the argument list, in a variable named "result".
+ ///
+ /// context_create initializes the calculation by setting a seed
+ /// value, called r0 here. This value is dependent on the library being
+ /// loaded. Prior to that, the argument "result" is initialized to -1,
+ /// the purpose being to avoid exceptions when running this test with no
+ /// libraries loaded.
+ ///
+ /// Callout hookpt_one is passed a value d1 and performs a simple arithmetic
+ /// operation on it and r0 yielding a result r1. Hence we can say that
+ /// @f[ r1 = hookpt_one(r0, d1) @f]
+ ///
+ /// Callout hookpt_two is passed a value d2 and peforms another simple
+ /// arithmetic operation on it and d2, yielding r2, i.e.
+ /// @f[ r2 = hookpt_two(d1, d2) @f]
+ ///
+ /// hookpt_three does a similar operation giving
+ /// @f[ r3 = hookpt_three(r2, d3) @f].
+ ///
+ /// The details of the operations hookpt_one, hookpt_two and hookpt_three
+ /// depend on the library, so the results obtained not only depend on
+ /// the data, but also on the library loaded. This method is passed both
+ /// data and expected results. It executes the three callouts in sequence,
+ /// checking the intermediate and final results. Only if the expected
+ /// library has been loaded correctly and the callouts in it registered
+ /// correctly will be the results be as expected.
+ ///
+ /// It is assumed that callout_manager_ has been set up appropriately.
+ ///
+ /// @note The CalloutHandle used in the calls is declared locally here.
+ /// The advantage of this (apart from scope reduction) is that on
+ /// exit, it is destroyed. This removes any references to memory
+ /// allocated by loaded libraries while they are still loaded.
+ ///
+ /// @param manager CalloutManager to use for the test
+ /// @param r0...r3, d1..d3 Data (dN) and expected results (rN) - both
+ /// intermediate and final. The arguments are ordered so that they
+ /// appear in the argument list in the order they are used.
+ void executeCallCallouts(
+ const boost::shared_ptr<isc::hooks::CalloutManager>& manager,
+ int r0, int d1, int r1, int d2, int r2, int d3, int r3) {
+ static const char* COMMON_TEXT = " callout returned the wong value";
+ static const char* RESULT = "result";
+
+ int result;
+
+ // Set up a callout handle for the calls.
+ isc::hooks::CalloutHandle handle(manager);
+
+ // Initialize the argument RESULT. This simplifies testing by
+ // eliminating the generation of an exception when we try the unload
+ // test. In that case, RESULT is unchanged.
+ handle.setArgument(RESULT, -1);
+
+ // Seed the calculation.
+ manager->callCallouts(isc::hooks::ServerHooks::CONTEXT_CREATE, handle);
+ handle.getArgument(RESULT, result);
+ EXPECT_EQ(r0, result) << "context_create" << COMMON_TEXT;
+
+ // Perform the first calculation.
+ handle.setArgument("data_1", d1);
+ manager->callCallouts(hookpt_one_index_, handle);
+ handle.getArgument(RESULT, result);
+ EXPECT_EQ(r1, result) << "hookpt_one" << COMMON_TEXT;
+
+ // ... the second ...
+ handle.setArgument("data_2", d2);
+ manager->callCallouts(hookpt_two_index_, handle);
+ handle.getArgument(RESULT, result);
+ EXPECT_EQ(r2, result) << "hookpt_two" << COMMON_TEXT;
+
+ // ... and the third.
+ handle.setArgument("data_3", d3);
+ manager->callCallouts(hookpt_three_index_, handle);
+ handle.getArgument(RESULT, result);
+ EXPECT_EQ(r3, result) << "hookpt_three" << COMMON_TEXT;
+ }
+
+ /// Hook indexes. These are are made public for ease of reference.
+ int hookpt_one_index_;
+ int hookpt_two_index_;
+ int hookpt_three_index_;
+};
+
+#endif // COMMON_HOOKS_TEST_CLASS_H
diff --git a/src/lib/hooks/tests/framework_exception_library.cc b/src/lib/hooks/tests/framework_exception_library.cc
new file mode 100644
index 0000000..e90fd36
--- /dev/null
+++ b/src/lib/hooks/tests/framework_exception_library.cc
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Framework exception library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - All three framework functions are supplied (version(), load() and
+/// unload()) and all generate an exception.
+
+#include <hooks/hooks.h>
+
+#include <exception>
+
+extern "C" {
+
+int
+version() {
+ throw std::exception();
+}
+
+int
+load(isc::hooks::LibraryHandle& handle) {
+ throw std::exception();
+}
+
+int
+unload() {
+ throw std::exception();
+}
+
+};
+
diff --git a/src/lib/hooks/tests/full_callout_library.cc b/src/lib/hooks/tests/full_callout_library.cc
new file mode 100644
index 0000000..3a87f54
--- /dev/null
+++ b/src/lib/hooks/tests/full_callout_library.cc
@@ -0,0 +1,141 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Full callout library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// The characteristics of this library are:
+///
+/// - All three framework functions are supplied (version(), load() and
+/// unload()), with unload() creating a marker file. The test code checks
+/// for the presence of this file, so verifying that unload() has been run.
+///
+/// - One standard and two non-standard callouts are supplied, with the latter
+/// being registered by the load() function.
+///
+/// All callouts do trivial calculations, the result of all being called in
+/// sequence being
+///
+/// @f[ ((7 * data_1) - data_2) * data_3 @f]
+///
+/// ...where data_1, data_2 and data_3 are the values passed in arguments of
+/// the same name to the three callouts (data_1 passed to hookpt_one, data_2
+/// to hookpt_two etc.) and the result is returned in the argument "result".
+
+#include <hooks/hooks.h>
+#include <hooks/tests/marker_file.h>
+
+#include <fstream>
+
+using namespace isc::hooks;
+
+extern "C" {
+
+// Callouts
+
+int
+context_create(CalloutHandle& handle) {
+ handle.setContext("result", static_cast<int>(7));
+ handle.setArgument("result", static_cast<int>(7));
+ return (0);
+}
+
+// First callout adds the passed "data_1" argument to the initialized context
+// value of 7. (Note that the value set by context_create is accessed through
+// context and not the argument, so checking that context is correctly passed
+// between callouts in the same library.)
+
+int
+hookpt_one(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_1", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result *= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Second callout subtracts the passed value of data_2 from the current
+// running total.
+
+static int
+hook_nonstandard_two(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_2", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result -= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Final callout multplies the current running total by data_3.
+
+static int
+hook_nonstandard_three(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_3", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result *= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Framework functions
+
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int
+load(LibraryHandle& handle) {
+ // Initialize if the main image was statically linked
+#ifdef USE_STATIC_LINK
+ hooksStaticLinkInit();
+#endif
+ // Register the non-standard functions
+ handle.registerCallout("hookpt_two", hook_nonstandard_two);
+ handle.registerCallout("hookpt_three", hook_nonstandard_three);
+
+ return (0);
+}
+
+int
+unload() {
+ // Create the marker file.
+ std::fstream marker;
+ marker.open(MARKER_FILE, std::fstream::out);
+ marker.close();
+
+ return (0);
+}
+
+};
+
diff --git a/src/lib/hooks/tests/handles_unittest.cc b/src/lib/hooks/tests/handles_unittest.cc
new file mode 100644
index 0000000..c19dff2
--- /dev/null
+++ b/src/lib/hooks/tests/handles_unittest.cc
@@ -0,0 +1,974 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_handle.h>
+#include <hooks/server_hooks.h>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/scoped_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <string>
+
+/// @file
+/// CalloutHandle/LibraryHandle interaction tests
+///
+/// This file holds unit tests checking the interaction between the
+/// CalloutHandle/LibraryHandle and CalloutManager classes. In particular,
+/// they check that:
+///
+/// - A CalloutHandle's context is shared between callouts from the same
+/// library, but there is a separate context for each library.
+///
+/// - The various methods manipulating the items in the CalloutHandle's context
+/// work correctly.
+///
+/// - An active callout can only modify the registration of callouts registered
+/// by its own library.
+
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+class HandlesTest : public ::testing::Test {
+public:
+ /// @brief Constructor
+ ///
+ /// Sets up the various elements used in each test.
+ HandlesTest() {
+ // Set up four hooks, although through gamma
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+ alpha_index_ = hooks.registerHook("alpha");
+ beta_index_ = hooks.registerHook("beta");
+ gamma_index_ = hooks.registerHook("gamma");
+ delta_index_ = hooks.registerHook("delta");
+
+ // Set up for three libraries.
+ manager_.reset(new CalloutManager(3));
+
+ // Initialize remaining variables.
+ common_string_ = "";
+ }
+
+ /// @brief Return callout manager
+ boost::shared_ptr<CalloutManager> getCalloutManager() {
+ return (manager_);
+ }
+
+ /// Hook indexes - these are frequently accessed, so are accessed directly.
+ int alpha_index_;
+ int beta_index_;
+ int gamma_index_;
+ int delta_index_;
+
+ /// String accessible by all callouts whatever the library
+ static std::string common_string_;
+
+private:
+ /// Callout manager. Declared static so that the callout functions can
+ /// access it.
+ boost::shared_ptr<CalloutManager> manager_;
+};
+
+/// Define the common string
+std::string HandlesTest::common_string_;
+
+
+// The next set of functions define the callouts used by the tests. They
+// manipulate the data in such a way that callouts called - and the order in
+// which they were called - can be determined. The functions also check that
+// the "callout context" data areas are separate.
+//
+// Three libraries are assumed, and each supplies four callouts. All callouts
+// manipulate two context elements the CalloutHandle, the elements being called
+// "string" and "int" (which describe the type of data manipulated).
+//
+// For the string item, each callout shifts data to the left and inserts its own
+// data. The data is a string of the form "nmc", where "n" is the number of
+// the library, "m" is the callout number and "y" is the indication of what
+// callout handle was passed as an argument ("1" or "2": "0" is used when no
+// identification has been set in the callout handle).
+//
+// For simplicity, and to cut down the number of functions actually written,
+// the callout indicator ("1" or "2") ) used in the in the CalloutHandle
+// functions is passed via a CalloutArgument. The argument is named "string":
+// use of a name the same as that of one of the context elements serves as a
+// check that the argument name space and argument context space are separate.
+//
+// For integer data, the value starts at zero and an increment is added on each
+// call. This increment is equal to:
+//
+// 100 * library number + 10 * callout number + callout handle
+//
+// Although this gives less information than the string value, the reasons for
+// using it are:
+//
+// - It is a separate item in the context, so checks that the context can
+// handle multiple items.
+// - It provides an item that can be deleted by the context deletion
+// methods.
+
+
+// Values set in the CalloutHandle context. There are three libraries, so
+// there are three contexts for the callout, one for each library.
+
+std::string& resultCalloutString(int index) {
+ static std::string result_callout_string[3];
+ return (result_callout_string[index]);
+}
+
+int& resultCalloutInt(int index) {
+ static int result_callout_int[3];
+ return (result_callout_int[index]);
+}
+
+// A simple function to zero the results.
+
+static void zero_results() {
+ for (int i = 0; i < 3; ++i) {
+ resultCalloutString(i) = "";
+ resultCalloutInt(i) = 0;
+ }
+}
+
+
+// Library callouts.
+
+// Common code for setting the callout context values.
+
+int
+execute(CalloutHandle& callout_handle, int library_num, int callout_num) {
+
+ // Obtain the callout handle number
+ int handle_num = 0;
+ try {
+ callout_handle.getArgument("handle_num", handle_num);
+ } catch (const NoSuchArgument&) {
+ // handle_num argument not set: this is the case in the tests where
+ // the context_create hook check is tested.
+ handle_num = 0;
+ }
+
+ // Create the basic data to be appended to the context value.
+ int idata = 100 * library_num + 10 * callout_num + handle_num;
+ string sdata = boost::lexical_cast<string>(idata);
+
+ // Get the context data. As before, this will not exist for the first
+ // callout called. (In real life, the library should create it when the
+ // "context_create" hook gets called before any packet processing takes
+ // place.)
+ int int_value = 0;
+ try {
+ callout_handle.getContext("int", int_value);
+ } catch (const NoSuchCalloutContext&) {
+ int_value = 0;
+ }
+
+ string string_value = "";
+ try {
+ callout_handle.getContext("string", string_value);
+ } catch (const NoSuchCalloutContext&) {
+ string_value = "";
+ }
+
+ // Update the values and set them back in the callout context.
+ int_value += idata;
+ callout_handle.setContext("int", int_value);
+
+ string_value += sdata;
+ callout_handle.setContext("string", string_value);
+
+ return (0);
+}
+
+// The following functions are the actual callouts - the name is of the
+// form "callout_<library number>_<callout number>"
+
+int
+callout11(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 1, 1));
+}
+
+int
+callout12(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 1, 2));
+}
+
+int
+callout13(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 1, 3));
+}
+
+int
+callout21(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 2, 1));
+}
+
+int
+callout22(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 2, 2));
+}
+
+int
+callout23(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 2, 3));
+}
+
+int
+callout31(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 3, 1));
+}
+
+int
+callout32(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 3, 2));
+}
+
+int
+callout33(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 3, 3));
+}
+
+// Common callout code for the fourth hook (which makes the data available for
+// checking). It copies the library and callout context data to the global
+// variables.
+
+int printExecute(CalloutHandle& callout_handle, int library_num) {
+ callout_handle.getContext("string", resultCalloutString(library_num - 1));
+ callout_handle.getContext("int", resultCalloutInt(library_num - 1));
+
+ return (0);
+}
+
+// These are the actual callouts.
+
+int
+print1(CalloutHandle& callout_handle) {
+ return (printExecute(callout_handle, 1));
+}
+
+int
+print2(CalloutHandle& callout_handle) {
+ return (printExecute(callout_handle, 2));
+}
+
+int
+print3(CalloutHandle& callout_handle) {
+ return (printExecute(callout_handle, 3));
+}
+
+// This test checks the many-faced nature of the context for the CalloutContext.
+
+TEST_F(HandlesTest, ContextAccessCheck) {
+ // Register callouts for the different libraries.
+ CalloutHandle handle(getCalloutManager());
+
+ // Library 0.
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("beta", callout12);
+ getCalloutManager()->registerCallout("gamma", callout13);
+ getCalloutManager()->registerCallout("delta", print1);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout21);
+ getCalloutManager()->registerCallout("beta", callout22);
+ getCalloutManager()->registerCallout("gamma", callout23);
+ getCalloutManager()->registerCallout("delta", print2);
+
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout31);
+ getCalloutManager()->registerCallout("beta", callout32);
+ getCalloutManager()->registerCallout("gamma", callout33);
+ getCalloutManager()->registerCallout("delta", print3);
+
+ // Create the callout handles and distinguish them by setting the
+ // "handle_num" argument.
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+
+ // Now call the callouts attached to the first three hooks. Each hook is
+ // called twice (once for each callout handle) before the next hook is
+ // called.
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_2);
+ getCalloutManager()->callCallouts(gamma_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(gamma_index_, callout_handle_2);
+
+ // Get the results for each callout (the callout on hook "delta" copies
+ // the context values into a location the test can access). Explicitly
+ // zero the variables before getting the results so we are certain that
+ // the values are the results of the callouts.
+
+ zero_results();
+
+ // To explain the expected callout context results.
+ //
+ // Each callout handle maintains a separate context for each library. When
+ // the first call to callCallouts() is made, "111" gets appended to
+ // the context for library 1 maintained by the first callout handle, "211"
+ // gets appended to the context maintained for library 2, and "311" to
+ // the context maintained for library 3. In each case, the first digit
+ // corresponds to the library number, the second to the callout number and
+ // the third to the "handle_num" of the callout handle. For the first call
+ // to callCallouts, handle 1 is used, so the last digit is always 1.
+ //
+ // The next call to callCallouts() calls the same callouts but for the
+ // second callout handle. It also maintains three contexts (one for
+ // each library) and they will get "112", "212", "312" appended to
+ // them. The explanation for the digits is the same as before, except that
+ // in this case, the callout handle is number 2, so the third digit is
+ // always 2. These additions don't affect the contexts maintained by
+ // callout handle 1.
+ //
+ // The process is then repeated for hooks "beta" and "gamma" which, for
+ // callout handle 1, append "121", "221" and "321" for hook "beta" and
+ // "311", "321" and "331" for hook "gamma".
+ //
+ // The expected integer values can be found by summing up the values
+ // corresponding to the elements of the strings.
+
+ // At this point, we have only called the "print" function for callout
+ // handle "1", so the following results are checking the context values
+ // maintained in that callout handle.
+
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+ EXPECT_EQ("111121131", resultCalloutString(0));
+ EXPECT_EQ("211221231", resultCalloutString(1));
+ EXPECT_EQ("311321331", resultCalloutString(2));
+
+ EXPECT_EQ((111 + 121 + 131), resultCalloutInt(0));
+ EXPECT_EQ((211 + 221 + 231), resultCalloutInt(1));
+ EXPECT_EQ((311 + 321 + 331), resultCalloutInt(2));
+
+ // Repeat the checks for callout 2.
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+
+ EXPECT_EQ((112 + 122 + 132), resultCalloutInt(0));
+ EXPECT_EQ((212 + 222 + 232), resultCalloutInt(1));
+ EXPECT_EQ((312 + 322 + 332), resultCalloutInt(2));
+
+ EXPECT_EQ("112122132", resultCalloutString(0));
+ EXPECT_EQ("212222232", resultCalloutString(1));
+ EXPECT_EQ("312322332", resultCalloutString(2));
+}
+
+// Now repeat the test, but add a deletion callout to the list. The "beta"
+// hook of library 2 will have an additional callout to delete the "int"
+// element: the same hook for library 3 will delete both elements. In
+// addition, the names of context elements for the libraries at this point
+// will be printed.
+
+// List of context item names.
+
+vector<string>&
+getItemNames(int index) {
+ static vector<string> context_items[3];
+ return (context_items[index]);
+}
+
+// Context item deletion functions.
+
+int
+deleteIntContextItem(CalloutHandle& handle) {
+ handle.deleteContext("int");
+ return (0);
+}
+
+int
+deleteAllContextItems(CalloutHandle& handle) {
+ handle.deleteAllContext();
+ return (0);
+}
+
+// Generic print function - copy names in sorted order.
+
+int
+printContextNamesExecute(CalloutHandle& handle, int library_num) {
+ const int index = library_num - 1;
+ getItemNames(index) = handle.getContextNames();
+ sort(getItemNames(index).begin(), getItemNames(index).end());
+ return (0);
+}
+
+int
+printContextNames1(CalloutHandle& handle) {
+ return (printContextNamesExecute(handle, 1));
+}
+
+int
+printContextNames2(CalloutHandle& handle) {
+ return (printContextNamesExecute(handle, 2));
+}
+
+int
+printContextNames3(CalloutHandle& handle) {
+ return (printContextNamesExecute(handle, 3));
+}
+
+// Perform the test including deletion of context items.
+
+TEST_F(HandlesTest, ContextDeletionCheck) {
+
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("beta", callout12);
+ getCalloutManager()->registerCallout("beta", printContextNames1);
+ getCalloutManager()->registerCallout("gamma", callout13);
+ getCalloutManager()->registerCallout("delta", print1);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout21);
+ getCalloutManager()->registerCallout("beta", callout22);
+ getCalloutManager()->registerCallout("beta", deleteIntContextItem);
+ getCalloutManager()->registerCallout("beta", printContextNames2);
+ getCalloutManager()->registerCallout("gamma", callout23);
+ getCalloutManager()->registerCallout("delta", print2);
+
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout31);
+ getCalloutManager()->registerCallout("beta", callout32);
+ getCalloutManager()->registerCallout("beta", deleteAllContextItems);
+ getCalloutManager()->registerCallout("beta", printContextNames3);
+ getCalloutManager()->registerCallout("gamma", callout33);
+ getCalloutManager()->registerCallout("delta", print3);
+
+ // Create the callout handles and distinguish them by setting the "long"
+ // argument.
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+
+ // Now call the callouts attached to the first three hooks. Each hook is
+ // called twice (once for each callout handle) before the next hook is
+ // called.
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_2);
+ getCalloutManager()->callCallouts(gamma_index_, callout_handle_1);
+ getCalloutManager()->callCallouts(gamma_index_, callout_handle_2);
+
+ // Get the results for each callout. Explicitly zero the variables before
+ // getting the results so we are certain that the values are the results
+ // of the callouts.
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+
+ // The logic by which the expected results are arrived at is described
+ // in the ContextAccessCheck test. The results here are different
+ // because context items have been modified along the way.
+
+ EXPECT_EQ((111 + 121 + 131), resultCalloutInt(0));
+ EXPECT_EQ(( 231), resultCalloutInt(1));
+ EXPECT_EQ(( 331), resultCalloutInt(2));
+
+ EXPECT_EQ("111121131", resultCalloutString(0));
+ EXPECT_EQ("211221231", resultCalloutString(1));
+ EXPECT_EQ( "331", resultCalloutString(2));
+
+ // Repeat the checks for callout handle 2.
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+
+ EXPECT_EQ((112 + 122 + 132), resultCalloutInt(0));
+ EXPECT_EQ(( 232), resultCalloutInt(1));
+ EXPECT_EQ(( 332), resultCalloutInt(2));
+
+ EXPECT_EQ("112122132", resultCalloutString(0));
+ EXPECT_EQ("212222232", resultCalloutString(1));
+ EXPECT_EQ( "332", resultCalloutString(2));
+
+ // ... and check what the names of the context items are after the callouts
+ // for hook "beta". We know they are in sorted order.
+
+ EXPECT_EQ(2, getItemNames(0).size());
+ EXPECT_EQ(string("int"), getItemNames(0)[0]);
+ EXPECT_EQ(string("string"), getItemNames(0)[1]);
+
+ EXPECT_EQ(1, getItemNames(1).size());
+ EXPECT_EQ(string("string"), getItemNames(1)[0]);
+
+ EXPECT_EQ(0, getItemNames(2).size());
+}
+
+// Tests that the CalloutHandle's constructor and destructor call the
+// context_create and context_destroy callbacks (if registered). For
+// simplicity, we'll use the same callout functions as used above.
+
+TEST_F(HandlesTest, ConstructionDestructionCallouts) {
+
+ // Register context callouts.
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("context_create", callout11);
+ getCalloutManager()->registerCallout("context_create", print1);
+ getCalloutManager()->registerCallout("context_destroy", callout12);
+ getCalloutManager()->registerCallout("context_destroy", print1);
+
+ // Create the CalloutHandle and check that the constructor callout
+ // has run.
+ zero_results();
+ boost::scoped_ptr<CalloutHandle>
+ callout_handle(new CalloutHandle(getCalloutManager()));
+ EXPECT_EQ("110", resultCalloutString(0));
+ EXPECT_EQ(110, resultCalloutInt(0));
+
+ // Check that the destructor callout runs. Note that the "print1" callout
+ // didn't destroy the library context - it only copied it to where it
+ // could be examined. As a result, the destructor callout appends its
+ // elements to the constructor's values and the result is printed.
+ zero_results();
+ callout_handle.reset();
+
+ EXPECT_EQ("110120", resultCalloutString(0));
+ EXPECT_EQ((110 + 120), resultCalloutInt(0));
+}
+
+// Dynamic callout registration and deregistration.
+// The following are the dynamic registration/deregistration callouts.
+
+
+// Add callout_78_alpha - adds a callout to hook alpha that appends "78x"
+// (where "x" is the callout handle) to the current output.
+
+int
+callout78(CalloutHandle& callout_handle) {
+ return (execute(callout_handle, 7, 8));
+}
+
+int
+add_callout78_alpha(CalloutHandle& callout_handle) {
+ callout_handle.getLibraryHandle().registerCallout("alpha", callout78);
+ return (0);
+}
+
+int
+delete_callout78_alpha(CalloutHandle& callout_handle) {
+ static_cast<void>(
+ callout_handle.getLibraryHandle().deregisterCallout("alpha",
+ callout78));
+ return (0);
+}
+
+// Check that a callout can register another callout on a different hook.
+
+TEST_F(HandlesTest, DynamicRegistrationAnotherHook) {
+ // Register callouts for the different libraries.
+ CalloutHandle handle(getCalloutManager());
+
+ // Set up callouts on "alpha".
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("delta", print1);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout21);
+ getCalloutManager()->registerCallout("delta", print2);
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", callout31);
+ getCalloutManager()->registerCallout("delta", print3);
+
+ // ... and on "beta", set up the function to add a hook to alpha (but only
+ // for library 1).
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("beta", add_callout78_alpha);
+
+ // See what we get for calling the callouts on alpha first.
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+ EXPECT_EQ("111", resultCalloutString(0));
+ EXPECT_EQ("211", resultCalloutString(1));
+ EXPECT_EQ("311", resultCalloutString(2));
+
+ // All as expected, now call the callouts on beta. This should add a
+ // callout to the list of callouts for alpha, which we should see when
+ // we run the test again.
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_1);
+
+ // Use a new callout handle so as to get fresh callout context.
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+ EXPECT_EQ("112", resultCalloutString(0));
+ EXPECT_EQ("212782", resultCalloutString(1));
+ EXPECT_EQ("312", resultCalloutString(2));
+}
+
+// Check that a callout can register another callout on the same hook.
+// Note that the registration only applies to a subsequent invocation of
+// callCallouts, not to the current one. In other words, if
+//
+// * the callout list for a library is "A then B then C"
+// * when callCallouts is executed "B" adds "D" to that list,
+//
+// ... the current execution of callCallouts only executes A, B and C. A
+// subsequent invocation will execute A, B, C then D.
+
+TEST_F(HandlesTest, DynamicRegistrationSameHook) {
+ // Register callouts for the different libraries.
+ CalloutHandle handle(getCalloutManager());
+
+ // Set up callouts on "alpha".
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("alpha", add_callout78_alpha);
+ getCalloutManager()->registerCallout("delta", print1);
+
+ // See what we get for calling the callouts on alpha first.
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+ EXPECT_EQ("111", resultCalloutString(0));
+
+ // Run it again - we should have added something to this hook.
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+ EXPECT_EQ("112782", resultCalloutString(0));
+
+ // And a third time...
+ CalloutHandle callout_handle_3(getCalloutManager());
+ callout_handle_3.setArgument("handle_num", static_cast<int>(3));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_3);
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_3);
+ EXPECT_EQ("113783783", resultCalloutString(0));
+}
+
+// Deregistration of a callout from a different hook
+
+TEST_F(HandlesTest, DynamicDeregistrationDifferentHook) {
+ // Register callouts for the different libraries.
+ CalloutHandle handle(getCalloutManager());
+
+ // Set up callouts on "alpha".
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("alpha", callout78);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("delta", print1);
+
+ getCalloutManager()->registerCallout("beta", delete_callout78_alpha);
+
+ // Call the callouts on alpha
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+ EXPECT_EQ("111781111", resultCalloutString(0));
+
+ // Run the callouts on hook beta to remove the callout on alpha.
+ getCalloutManager()->callCallouts(beta_index_, callout_handle_1);
+
+ // The run of the callouts should have altered the callout list on the
+ // first library for hook alpha, so call again to make sure.
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+ EXPECT_EQ("112112", resultCalloutString(0));
+}
+
+// Deregistration of a callout from the same hook
+
+TEST_F(HandlesTest, DynamicDeregistrationSameHook) {
+ // Register callouts for the different libraries.
+ CalloutHandle handle(getCalloutManager());
+
+ // Set up callouts on "alpha".
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout11);
+ getCalloutManager()->registerCallout("alpha", delete_callout78_alpha);
+ getCalloutManager()->registerCallout("alpha", callout78);
+ getCalloutManager()->registerCallout("delta", print1);
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", callout21);
+ getCalloutManager()->registerCallout("alpha", callout78);
+ getCalloutManager()->registerCallout("delta", print2);
+
+ // Call the callouts on alpha
+ CalloutHandle callout_handle_1(getCalloutManager());
+ callout_handle_1.setArgument("handle_num", static_cast<int>(1));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_1);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_1);
+ EXPECT_EQ("111781", resultCalloutString(0));
+ EXPECT_EQ("211781", resultCalloutString(1));
+
+ // The run of the callouts should have altered the callout list on the
+ // first library for hook alpha, so call again to make sure.
+ CalloutHandle callout_handle_2(getCalloutManager());
+ callout_handle_2.setArgument("handle_num", static_cast<int>(2));
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle_2);
+
+ zero_results();
+ getCalloutManager()->callCallouts(delta_index_, callout_handle_2);
+ EXPECT_EQ("112", resultCalloutString(0));
+ EXPECT_EQ("212782", resultCalloutString(1));
+}
+
+// Testing the operation of the "skip" flag. Callouts print the value
+// they see in the flag and either leave it unchanged, set it or clear it.
+
+int
+calloutPrintSkip(CalloutHandle& handle) {
+ static const std::string YES("Y");
+ static const std::string NO("N");
+
+ HandlesTest::common_string_ = HandlesTest::common_string_ +
+ (handle.getSkip() ? YES : NO);
+ return (0);
+}
+
+int
+calloutSetSkip(CalloutHandle& handle) {
+ static_cast<void>(calloutPrintSkip(handle));
+ handle.setSkip(true);
+ return (0);
+}
+
+int
+calloutClearSkip(CalloutHandle& handle) {
+ static_cast<void>(calloutPrintSkip(handle));
+ handle.setSkip(false);
+ return (0);
+}
+
+// Do a series of tests, returning with the skip flag set "true".
+
+TEST_F(HandlesTest, ReturnSkipSet) {
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", calloutPrintSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", calloutPrintSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", calloutPrintSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+
+ CalloutHandle callout_handle(getCalloutManager());
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+
+ // Check result. For each of visual checking, the expected string is
+ // divided into sections corresponding to the blocks of callouts above.
+ EXPECT_EQ(std::string("NNYY" "NNYYN" "NNYN"), common_string_);
+
+ // ... and check that the skip flag on exit from callCallouts is set.
+ EXPECT_TRUE(callout_handle.getSkip());
+}
+
+// Repeat the test, returning with the skip flag clear.
+TEST_F(HandlesTest, ReturnSkipClear) {
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", calloutPrintSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+ getCalloutManager()->registerCallout("alpha", calloutPrintSkip);
+ getCalloutManager()->registerCallout("alpha", calloutSetSkip);
+ getCalloutManager()->registerCallout("alpha", calloutClearSkip);
+
+ CalloutHandle callout_handle(getCalloutManager());
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+
+ // Check result. For each of visual checking, the expected string is
+ // divided into sections corresponding to the blocks of callouts above.
+ EXPECT_EQ(std::string("NYY" "NNYNYN" "NNNY"), common_string_);
+
+ // ... and check that the skip flag on exit from callCallouts is set.
+ EXPECT_FALSE(callout_handle.getSkip());
+}
+
+// Check that the skip flag is cleared when callouts are called - even if
+// there are no callouts.
+
+TEST_F(HandlesTest, NoCalloutsSkipTest) {
+ // Note - no callouts are registered on any hook.
+ CalloutHandle callout_handle(getCalloutManager());
+
+ // Clear the skip flag and call a hook with no callouts.
+ callout_handle.setSkip(false);
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+ EXPECT_FALSE(callout_handle.getSkip());
+
+ // Set the skip flag and call a hook with no callouts.
+ callout_handle.setSkip(true);
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+ EXPECT_FALSE(callout_handle.getSkip());
+}
+
+// The next set of callouts do a similar thing to the above "skip" tests,
+// but alter the value of a string argument. This is for testing that the
+// a callout is able to change an argument and return it to the caller.
+
+const char* MODIFIED_ARG = "modified_arg";
+
+int
+calloutSetArgumentCommon(CalloutHandle& handle, const char* what) {
+ std::string modified_arg = "";
+
+ handle.getArgument(MODIFIED_ARG, modified_arg);
+ modified_arg = modified_arg + std::string(what);
+ handle.setArgument(MODIFIED_ARG, modified_arg);
+ return (0);
+}
+
+int
+calloutSetArgumentYes(CalloutHandle& handle) {
+ return (calloutSetArgumentCommon(handle, "Y"));
+}
+
+int
+calloutSetArgumentNo(CalloutHandle& handle) {
+ return (calloutSetArgumentCommon(handle, "N"));
+}
+
+// ... and a callout to just copy the argument to the "common_string_" variable
+// but otherwise not alter it.
+
+int
+calloutPrintArgument(CalloutHandle& handle) {
+ handle.getArgument(MODIFIED_ARG, HandlesTest::common_string_);
+ return (0);
+}
+
+TEST_F(HandlesTest, CheckModifiedArgument) {
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentYes);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentNo);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentNo);
+
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentYes);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentYes);
+ getCalloutManager()->registerCallout("alpha", calloutPrintArgument);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentNo);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentNo);
+
+ getCalloutManager()->setLibraryIndex(2);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentYes);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentNo);
+ getCalloutManager()->registerCallout("alpha", calloutSetArgumentYes);
+
+ // Create the argument with an initial empty string value. Then call the
+ // sequence of callouts above.
+ CalloutHandle callout_handle(getCalloutManager());
+ std::string modified_arg = "";
+ callout_handle.setArgument(MODIFIED_ARG, modified_arg);
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+
+ // Check the intermediate and results. For visual checking, the expected
+ // string is divided into sections corresponding to the blocks of callouts
+ // above.
+ EXPECT_EQ(std::string("YNN" "YY"), common_string_);
+
+ callout_handle.getArgument(MODIFIED_ARG, modified_arg);
+ EXPECT_EQ(std::string("YNN" "YYNN" "YNY"), modified_arg);
+}
+
+// Test that the CalloutHandle provides the name of the hook to which the
+// callout is attached.
+
+int
+callout_hook_name(CalloutHandle& callout_handle) {
+ HandlesTest::common_string_ = callout_handle.getHookName();
+ return (0);
+}
+
+int
+callout_hook_dummy(CalloutHandle&) {
+ return (0);
+}
+
+TEST_F(HandlesTest, HookName) {
+ getCalloutManager()->setLibraryIndex(0);
+ getCalloutManager()->registerCallout("alpha", callout_hook_name);
+ getCalloutManager()->registerCallout("beta", callout_hook_name);
+
+ // Call alpha and beta callouts and check the hook to which they belong.
+ CalloutHandle callout_handle(getCalloutManager());
+
+ EXPECT_EQ(std::string(""), HandlesTest::common_string_);
+
+ getCalloutManager()->callCallouts(alpha_index_, callout_handle);
+ EXPECT_EQ(std::string("alpha"), HandlesTest::common_string_);
+
+ getCalloutManager()->callCallouts(beta_index_, callout_handle);
+ EXPECT_EQ(std::string("beta"), HandlesTest::common_string_);
+
+ // Make sure that the callout accesses the name even if it is not the
+ // only callout in the list.
+ getCalloutManager()->setLibraryIndex(1);
+ getCalloutManager()->registerCallout("gamma", callout_hook_dummy);
+ getCalloutManager()->registerCallout("gamma", callout_hook_name);
+ getCalloutManager()->registerCallout("gamma", callout_hook_dummy);
+
+ EXPECT_EQ(std::string("beta"), HandlesTest::common_string_);
+ getCalloutManager()->callCallouts(gamma_index_, callout_handle);
+ EXPECT_EQ(std::string("gamma"), HandlesTest::common_string_);
+}
+
+} // Anonymous namespace
+
diff --git a/src/lib/hooks/tests/hooks_manager_unittest.cc b/src/lib/hooks/tests/hooks_manager_unittest.cc
new file mode 100644
index 0000000..136eeae
--- /dev/null
+++ b/src/lib/hooks/tests/hooks_manager_unittest.cc
@@ -0,0 +1,524 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/hooks_manager.h>
+#include <hooks/server_hooks.h>
+
+#include <hooks/tests/common_test_class.h>
+#include <hooks/tests/test_libraries.h>
+
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <string>
+
+
+using namespace isc;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+/// @brief Hooks manager collection test class
+
+class HooksManagerTest : public ::testing::Test,
+ public HooksCommonTestClass {
+public:
+ /// @brief Constructor
+ ///
+ /// Reset the hooks manager. The hooks manager is a singleton, so needs
+ /// to be reset for each test.
+ HooksManagerTest() {
+ HooksManager::unloadLibraries();
+ }
+
+ /// @brief Destructor
+ ///
+ /// Unload all libraries.
+ ~HooksManagerTest() {
+ HooksManager::unloadLibraries();
+ }
+
+
+ /// @brief Call callouts test
+ ///
+ /// See the header for HooksCommonTestClass::execute for details.
+ ///
+ /// @param r0...r3, d1..d3 Values and intermediate values expected. They
+ /// are ordered so that the variables appear in the argument list in
+ /// the order they are used.
+ void executeCallCallouts(int r0, int d1, int r1, int d2, int r2, int d3,
+ int r3) {
+ static const char* COMMON_TEXT = " callout returned the wong value";
+ static const char* RESULT = "result";
+
+ // Get a CalloutHandle for the calculation.
+ CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+
+ // Initialize the argument RESULT. This simplifies testing by
+ // eliminating the generation of an exception when we try the unload
+ // test. In that case, RESULT is unchanged.
+ int result = -1;
+ handle->setArgument(RESULT, result);
+
+ // Seed the calculation.
+ HooksManager::callCallouts(isc::hooks::ServerHooks::CONTEXT_CREATE,
+ *handle);
+ handle->getArgument(RESULT, result);
+ EXPECT_EQ(r0, result) << "context_create" << COMMON_TEXT;
+
+ // Perform the first calculation.
+ handle->setArgument("data_1", d1);
+ HooksManager::callCallouts(hookpt_one_index_, *handle);
+ handle->getArgument(RESULT, result);
+ EXPECT_EQ(r1, result) << "hookpt_one" << COMMON_TEXT;
+
+ // ... the second ...
+ handle->setArgument("data_2", d2);
+ HooksManager::callCallouts(hookpt_two_index_, *handle);
+ handle->getArgument(RESULT, result);
+ EXPECT_EQ(r2, result) << "hookpt_two" << COMMON_TEXT;
+
+ // ... and the third.
+ handle->setArgument("data_3", d3);
+ HooksManager::callCallouts(hookpt_three_index_, *handle);
+ handle->getArgument(RESULT, result);
+ EXPECT_EQ(r3, result) << "hookpt_three" << COMMON_TEXT;
+ }
+
+};
+
+// This is effectively the same test as for LibraryManager, but using the
+// HooksManager object.
+
+TEST_F(HooksManagerTest, LoadLibraries) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Load the libraries.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Execute the callouts. The first library implements the calculation.
+ //
+ // r3 = (7 * d1 - d2) * d3
+ //
+ // The last-loaded library implements the calculation
+ //
+ // r3 = (10 + d1) * d2 - d3
+ //
+ // Putting the processing for each library together in the appropriate
+ // order, we get:
+ //
+ // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
+ {
+ SCOPED_TRACE("Calculation with libraries loaded");
+ executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
+ }
+
+ // Try unloading the libraries.
+ EXPECT_NO_THROW(HooksManager::unloadLibraries());
+
+ // Re-execute the calculation - callouts can be called but as nothing
+ // happens, the result should always be -1.
+ {
+ SCOPED_TRACE("Calculation with libraries not loaded");
+ executeCallCallouts(-1, 3, -1, 22, -1, 83, -1);
+ }
+}
+
+// This is effectively the same test as above, but with a library generating
+// an error when loaded. It is expected that the failing library will not be
+// loaded, but others will be.
+
+TEST_F(HooksManagerTest, LoadLibrariesWithError) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(INCORRECT_VERSION_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Load the libraries. We expect a failure return because one of the
+ // libraries fails to load.
+ EXPECT_FALSE(HooksManager::loadLibraries(library_names));
+}
+
+// Test that we can unload a set of libraries while we have a CalloutHandle
+// created on them in existence, and can delete the handle afterwards.
+
+TEST_F(HooksManagerTest, CalloutHandleUnloadLibrary) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+
+ // Load the libraries.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Execute the callouts. Thiis library implements:
+ //
+ // r3 = (7 * d1 - d2) * d3
+ {
+ SCOPED_TRACE("Calculation with full callout library loaded");
+ executeCallCallouts(7, 4, 28, 8, 20, 2, 40);
+ }
+
+ // Get an outstanding callout handle on this library.
+ CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+
+ // Execute once of the callouts again to ensure that the handle contains
+ // memory allocated by the library.
+ HooksManager::callCallouts(ServerHooks::CONTEXT_CREATE, *handle);
+
+ // Unload the libraries.
+ HooksManager::unloadLibraries();
+
+ // Deleting the callout handle should not cause a segmentation fault.
+ handle.reset();
+}
+
+// Test that we can load a new set of libraries while we have a CalloutHandle
+// created on them in existence, and can delete the handle afterwards.
+
+TEST_F(HooksManagerTest, CalloutHandleLoadLibrary) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+
+ // Load the libraries.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Execute the callouts. Thiis library implements:
+ //
+ // r3 = (7 * d1 - d2) * d3
+ {
+ SCOPED_TRACE("Calculation with full callout library loaded");
+ executeCallCallouts(7, 4, 28, 8, 20, 2, 40);
+ }
+
+ // Get an outstanding callout handle on this library and execute one of
+ // the callouts again to ensure that the handle contains memory allocated
+ // by the library.
+ CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+ HooksManager::callCallouts(ServerHooks::CONTEXT_CREATE, *handle);
+
+ // Load a new library that implements the calculation
+ //
+ // r3 = (10 + d1) * d2 - d3
+ std::vector<std::string> new_library_names;
+ new_library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Load the libraries.
+ EXPECT_TRUE(HooksManager::loadLibraries(new_library_names));
+
+ // Execute the calculation. Note that we still have the CalloutHandle
+ // for the old library: however, this should not affect the new calculation.
+ {
+ SCOPED_TRACE("Calculation with basic callout library loaded");
+ executeCallCallouts(10, 7, 17, 3, 51, 16, 35);
+ }
+
+ // Deleting the old callout handle should not cause a segmentation fault.
+ handle.reset();
+}
+
+// This is effectively the same test as the LoadLibraries test.
+
+TEST_F(HooksManagerTest, ReloadSameLibraries) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Load the libraries.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Execute the callouts. See the LoadLibraries test for an explanation of
+ // the calculation.
+ {
+ SCOPED_TRACE("Calculation with libraries loaded");
+ executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
+ }
+
+ // Try reloading the libraries and re-execute the calculation - we should
+ // get the same results.
+ EXPECT_NO_THROW(HooksManager::loadLibraries(library_names));
+ {
+ SCOPED_TRACE("Calculation with libraries reloaded");
+ executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
+ }
+}
+
+TEST_F(HooksManagerTest, ReloadLibrariesReverseOrder) {
+
+ // Set up the list of libraries to be loaded and load them.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Execute the callouts. The first library implements the calculation.
+ //
+ // r3 = (7 * d1 - d2) * d3
+ //
+ // The last-loaded library implements the calculation
+ //
+ // r3 = (10 + d1) * d2 - d3
+ //
+ // Putting the processing for each library together in the given order
+ // gives.
+ //
+ // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
+ {
+ SCOPED_TRACE("Calculation with libraries loaded");
+ executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
+ }
+
+ // Reload the libraries in the reverse order.
+ std::reverse(library_names.begin(), library_names.end());
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // The calculation in the reverse order gives:
+ //
+ // r3 = ((((7 + d1) * d1) * d2 - d2) - d3) * d3
+ {
+ SCOPED_TRACE("Calculation with libraries loaded in reverse order");
+ executeCallCallouts(7, 3, 30, 3, 87, 7, 560);
+ }
+}
+
+// Local callouts for the test of server-registered callouts.
+
+namespace {
+
+ int
+testPreCallout(CalloutHandle& handle) {
+ handle.setArgument("result", static_cast<int>(1027));
+ return (0);
+}
+
+int
+testPostCallout(CalloutHandle& handle) {
+ int result;
+ handle.getArgument("result", result);
+ result *= 2;
+ handle.setArgument("result", result);
+ return (0);
+}
+
+}
+
+// The next test registers the pre and post- callouts above for hook hookpt_two,
+// and checks they are called.
+
+TEST_F(HooksManagerTest, PrePostCalloutTest) {
+
+ // Load a single library.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+
+ // Load the pre- and post- callouts.
+ HooksManager::preCalloutsLibraryHandle().registerCallout("hookpt_two",
+ testPreCallout);
+ HooksManager::postCalloutsLibraryHandle().registerCallout("hookpt_two",
+ testPostCallout);
+
+ // Execute the callouts. hookpt_two implements the calculation:
+ //
+ // "result - data_2"
+ //
+ // With the pre- and post- callouts above, the result expected is
+ //
+ // (1027 - data_2) * 2
+ CalloutHandlePtr handle = HooksManager::createCalloutHandle();
+ handle->setArgument("result", static_cast<int>(0));
+ handle->setArgument("data_2", static_cast<int>(15));
+
+ HooksManager::callCallouts(hookpt_two_index_, *handle);
+
+ int result = 0;
+ handle->getArgument("result", result);
+ EXPECT_EQ(2024, result);
+
+ // ... and check that the pre- and post- callout functions don't survive a
+ // reload.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+ handle = HooksManager::createCalloutHandle();
+
+ handle->setArgument("result", static_cast<int>(0));
+ handle->setArgument("data_2", static_cast<int>(15));
+
+ HooksManager::callCallouts(hookpt_two_index_, *handle);
+
+ result = 0;
+ handle->getArgument("result", result);
+ EXPECT_EQ(-15, result);
+}
+
+// Check that everything works even with no libraries loaded. First that
+// calloutsPresent() always returns false.
+
+TEST_F(HooksManagerTest, NoLibrariesCalloutsPresent) {
+ // No callouts should be present on any hooks.
+ EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(HooksManager::calloutsPresent(hookpt_three_index_));
+}
+
+TEST_F(HooksManagerTest, NoLibrariesCallCallouts) {
+ executeCallCallouts(-1, 3, -1, 22, -1, 83, -1);
+}
+
+// Test the encapsulation of the ServerHooks::registerHook() method.
+
+TEST_F(HooksManagerTest, RegisterHooks) {
+ ServerHooks::getServerHooks().reset();
+ EXPECT_EQ(2, ServerHooks::getServerHooks().getCount());
+
+ // Check that the hook indexes are as expected. (Use temporary variables
+ // as it appears that Google test can't access the constants.)
+ int sh_cc = ServerHooks::CONTEXT_CREATE;
+ int hm_cc = HooksManager::CONTEXT_CREATE;
+ EXPECT_EQ(sh_cc, hm_cc);
+
+ int sh_cd = ServerHooks::CONTEXT_DESTROY;
+ int hm_cd = HooksManager::CONTEXT_DESTROY;
+ EXPECT_EQ(sh_cd, hm_cd);
+
+ // Register a few hooks and check we have the indexes as expected.
+ EXPECT_EQ(2, HooksManager::registerHook(string("alpha")));
+ EXPECT_EQ(3, HooksManager::registerHook(string("beta")));
+ EXPECT_EQ(4, HooksManager::registerHook(string("gamma")));
+ EXPECT_THROW(static_cast<void>(HooksManager::registerHook(string("alpha"))),
+ DuplicateHook);
+
+ // ... an check the hooks are as we expect.
+ EXPECT_EQ(5, ServerHooks::getServerHooks().getCount());
+ vector<string> names = ServerHooks::getServerHooks().getHookNames();
+ sort(names.begin(), names.end());
+
+ EXPECT_EQ(string("alpha"), names[0]);
+ EXPECT_EQ(string("beta"), names[1]);
+ EXPECT_EQ(string("context_create"), names[2]);
+ EXPECT_EQ(string("context_destroy"), names[3]);
+ EXPECT_EQ(string("gamma"), names[4]);
+}
+
+// Check that we can get the names of the libraries.
+
+TEST_F(HooksManagerTest, LibraryNames) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Check the names before the libraries are loaded.
+ std::vector<std::string> loaded_names = HooksManager::getLibraryNames();
+ EXPECT_TRUE(loaded_names.empty());
+
+ // Load the libraries and check the names again.
+ EXPECT_TRUE(HooksManager::loadLibraries(library_names));
+ loaded_names = HooksManager::getLibraryNames();
+ EXPECT_TRUE(library_names == loaded_names);
+
+ // Unload the libraries and check again.
+ EXPECT_NO_THROW(HooksManager::unloadLibraries());
+ loaded_names = HooksManager::getLibraryNames();
+ EXPECT_TRUE(loaded_names.empty());
+}
+
+// Test the library validation function.
+
+TEST_F(HooksManagerTest, validateLibraries) {
+ // Vector of libraries that failed validation
+ std::vector<std::string> failed;
+
+ // Test different vectors of libraries.
+
+ // No libraries should return a success.
+ std::vector<std::string> libraries;
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Single valid library should validate.
+ libraries.clear();
+ libraries.push_back(BASIC_CALLOUT_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Multiple valid libraries should succeed.
+ libraries.clear();
+ libraries.push_back(BASIC_CALLOUT_LIBRARY);
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(UNLOAD_CALLOUT_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Single invalid library should fail.
+ libraries.clear();
+ libraries.push_back(NOT_PRESENT_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed == libraries);
+
+ // Multiple invalid libraries should fail.
+ libraries.clear();
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+ libraries.push_back(NO_VERSION_LIBRARY);
+ libraries.push_back(FRAMEWORK_EXCEPTION_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed == libraries);
+
+ // Combination of valid and invalid (first one valid) should fail.
+ libraries.clear();
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+ libraries.push_back(NO_VERSION_LIBRARY);
+
+ std::vector<std::string> expected_failures;
+ expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+ expected_failures.push_back(NO_VERSION_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed == expected_failures);
+
+ // Combination of valid and invalid (first one invalid) should fail.
+ libraries.clear();
+ libraries.push_back(NO_VERSION_LIBRARY);
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+
+ expected_failures.clear();
+ expected_failures.push_back(NO_VERSION_LIBRARY);
+ expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+
+ failed = HooksManager::validateLibraries(libraries);
+ EXPECT_TRUE(failed == expected_failures);
+}
+
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/incorrect_version_library.cc b/src/lib/hooks/tests/incorrect_version_library.cc
new file mode 100644
index 0000000..bb6eedf
--- /dev/null
+++ b/src/lib/hooks/tests/incorrect_version_library.cc
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Incorrect version function test
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - It contains the version() framework function only, which returns an
+/// incorrect version number.
+
+#include <hooks/hooks.h>
+
+extern "C" {
+
+int version() {
+ return (BIND10_HOOKS_VERSION + 1);
+}
+
+};
diff --git a/src/lib/hooks/tests/library_manager_collection_unittest.cc b/src/lib/hooks/tests/library_manager_collection_unittest.cc
new file mode 100644
index 0000000..7fdbb7d
--- /dev/null
+++ b/src/lib/hooks/tests/library_manager_collection_unittest.cc
@@ -0,0 +1,251 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_manager.h>
+#include <hooks/library_manager_collection.h>
+
+#include <hooks/tests/common_test_class.h>
+#include <hooks/tests/test_libraries.h>
+
+#include <boost/shared_ptr.hpp>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <string>
+
+
+using namespace isc;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+/// @brief Library manager collection test class
+
+class LibraryManagerCollectionTest : public ::testing::Test,
+ public HooksCommonTestClass {
+};
+
+/// @brief Public library manager collection class
+///
+/// This is an instance of the LibraryManagerCollection class but with the
+/// protected methods made public for test purposes.
+
+class PublicLibraryManagerCollection
+ : public isc::hooks::LibraryManagerCollection {
+public:
+ /// @brief Constructor
+ ///
+ /// @param List of libraries that this collection will manage. The order
+ /// of the libraries is important.
+ PublicLibraryManagerCollection(const std::vector<std::string>& libraries)
+ : LibraryManagerCollection(libraries)
+ {}
+
+ /// Public methods that call protected methods on the superclass.
+ using LibraryManagerCollection::unloadLibraries;
+};
+
+
+// This is effectively the same test as for LibraryManager, but using the
+// LibraryManagerCollection object.
+
+TEST_F(LibraryManagerCollectionTest, LoadLibraries) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Set up the library manager collection and get the callout manager we'll
+ // be using.
+ PublicLibraryManagerCollection lm_collection(library_names);
+
+ // Load the libraries.
+ EXPECT_TRUE(lm_collection.loadLibraries());
+ EXPECT_EQ(2, lm_collection.getLoadedLibraryCount());
+
+ // Execute the callouts. The first library implements the calculation.
+ //
+ // r3 = (7 * d1 - d2) * d3
+ //
+ // The last-loaded library implements the calculation
+ //
+ // r3 = (10 + d1) * d2 - d3
+ //
+ // Putting the processing for each library together in the appropriate
+ // order, we get:
+ //
+ // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
+ boost::shared_ptr<CalloutManager> manager =
+ lm_collection.getCalloutManager();
+ {
+ SCOPED_TRACE("Doing calculation with libraries loaded");
+ executeCallCallouts(manager, 10, 3, 33, 2, 62, 3, 183);
+ }
+
+ // Try unloading the libraries.
+ EXPECT_NO_THROW(lm_collection.unloadLibraries());
+ EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
+
+ // Re-execute the calculation - callouts can be called but as nothing
+ // happens, the result should always be -1.
+ {
+ SCOPED_TRACE("Doing calculation with libraries not loaded");
+ executeCallCallouts(manager, -1, 3, -1, 22, -1, 83, -1);
+ }
+}
+
+// This is effectively the same test as above, but with a library generating
+// an error when loaded. It is expected that no libraries will be loaded.
+
+TEST_F(LibraryManagerCollectionTest, LoadLibrariesWithError) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(INCORRECT_VERSION_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Set up the library manager collection and get the callout manager we'll
+ // be using.
+ PublicLibraryManagerCollection lm_collection(library_names);
+
+ // Load the libraries. We expect a failure status to be returned as
+ // one of the libraries failed to load.
+ EXPECT_FALSE(lm_collection.loadLibraries());
+
+ // Expect no libraries were loaded.
+ EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
+}
+
+// Check that everything works even with no libraries loaded.
+
+TEST_F(LibraryManagerCollectionTest, NoLibrariesLoaded) {
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+
+ // Set up the library manager collection and get the callout manager we'll
+ // be using.
+ LibraryManagerCollection lm_collection(library_names);
+ EXPECT_TRUE(lm_collection.loadLibraries());
+ EXPECT_EQ(0, lm_collection.getLoadedLibraryCount());
+ boost::shared_ptr<CalloutManager> manager =
+ lm_collection.getCalloutManager();
+
+ // Eecute the calculation - callouts can be called but as nothing
+ // happens, the result should always be -1.
+ executeCallCallouts(manager, -1, 3, -1, 22, -1, 83, -1);
+}
+
+// Check that we can get the names of the libraries.
+
+TEST_F(LibraryManagerCollectionTest, LibraryNames) {
+
+ // Set up the list of libraries to be loaded.
+ std::vector<std::string> library_names;
+ library_names.push_back(std::string(FULL_CALLOUT_LIBRARY));
+ library_names.push_back(std::string(BASIC_CALLOUT_LIBRARY));
+
+ // Set up the library manager collection and get the callout manager we'll
+ // be using.
+ PublicLibraryManagerCollection lm_collection(library_names);
+
+ // Check the names before the libraries are loaded.
+ std::vector<std::string> collection_names = lm_collection.getLibraryNames();
+ EXPECT_TRUE(library_names == collection_names);
+
+ // Load the libraries and check the names again.
+ EXPECT_TRUE(lm_collection.loadLibraries());
+ EXPECT_EQ(2, lm_collection.getLoadedLibraryCount());
+ collection_names = lm_collection.getLibraryNames();
+ EXPECT_TRUE(library_names == collection_names);
+}
+
+// Test the library validation function.
+
+TEST_F(LibraryManagerCollectionTest, validateLibraries) {
+ // Vector of libraries that failed validation
+ std::vector<std::string> failed;
+
+ // Test different vectors of libraries.
+
+ // No libraries should return a success.
+ std::vector<std::string> libraries;
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Single valid library should validate.
+ libraries.clear();
+ libraries.push_back(BASIC_CALLOUT_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Multiple valid libraries should succeed.
+ libraries.clear();
+ libraries.push_back(BASIC_CALLOUT_LIBRARY);
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(UNLOAD_CALLOUT_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed.empty());
+
+ // Single invalid library should fail.
+ libraries.clear();
+ libraries.push_back(NOT_PRESENT_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed == libraries);
+
+ // Multiple invalid libraries should fail.
+ libraries.clear();
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+ libraries.push_back(NO_VERSION_LIBRARY);
+ libraries.push_back(FRAMEWORK_EXCEPTION_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed == libraries);
+
+ // Combination of valid and invalid (first one valid) should fail.
+ libraries.clear();
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+ libraries.push_back(NO_VERSION_LIBRARY);
+
+ std::vector<std::string> expected_failures;
+ expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+ expected_failures.push_back(NO_VERSION_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed == expected_failures);
+
+ // Combination of valid and invalid (first one invalid) should fail.
+ libraries.clear();
+ libraries.push_back(NO_VERSION_LIBRARY);
+ libraries.push_back(FULL_CALLOUT_LIBRARY);
+ libraries.push_back(INCORRECT_VERSION_LIBRARY);
+
+ expected_failures.clear();
+ expected_failures.push_back(NO_VERSION_LIBRARY);
+ expected_failures.push_back(INCORRECT_VERSION_LIBRARY);
+
+ failed = LibraryManagerCollection::validateLibraries(libraries);
+ EXPECT_TRUE(failed == expected_failures);
+}
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/library_manager_unittest.cc b/src/lib/hooks/tests/library_manager_unittest.cc
new file mode 100644
index 0000000..9336946
--- /dev/null
+++ b/src/lib/hooks/tests/library_manager_unittest.cc
@@ -0,0 +1,569 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/callout_handle.h>
+#include <hooks/callout_manager.h>
+#include <hooks/library_manager.h>
+#include <hooks/server_hooks.h>
+
+#include <hooks/tests/common_test_class.h>
+#include <hooks/tests/marker_file.h>
+#include <hooks/tests/test_libraries.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <fstream>
+#include <string>
+
+#include <unistd.h>
+
+
+using namespace isc;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+/// @brief Library manager test class
+
+class LibraryManagerTest : public ::testing::Test,
+ public HooksCommonTestClass {
+public:
+ /// @brief Constructor
+ ///
+ /// Initializes the CalloutManager object used in the tests. It sets it
+ /// up with the hooks initialized in the HooksCommonTestClass object and
+ /// with four libraries.
+ LibraryManagerTest() {
+ callout_manager_.reset(new CalloutManager(4));
+
+ // Ensure the marker file is not present at the start of a test.
+ static_cast<void>(unlink(MARKER_FILE));
+ }
+
+ /// @brief Destructor
+ ///
+ /// Ensures a marker file is removed after each test.
+ ~LibraryManagerTest() {
+ static_cast<void>(unlink(MARKER_FILE));
+ }
+
+ /// @brief Marker file present
+ ///
+ /// Convenience function to check whether a marker file is present. It
+ /// does this by opening the file.
+ ///
+ /// @return true if the marker file is present.
+ bool markerFilePresent() const {
+
+ // Try to open it.
+ std::fstream marker;
+ marker.open(MARKER_FILE, std::fstream::in);
+
+ // Check if it is open and close it if so.
+ bool exists = marker.is_open();
+ if (exists) {
+ marker.close();
+ }
+
+ return (exists);
+ }
+
+ /// @brief Call callouts test
+ ///
+ /// A wrapper around the method of the same name in the HooksCommonTestClass
+ /// object, this passes this class's CalloutManager to that method.
+ ///
+ /// @param r0...r3, d1..d3 Values and intermediate values expected. They
+ /// are ordered so that the variables appear in the argument list in
+ /// the order they are used. See HooksCommonTestClass::execute for
+ /// a full description. (rN is used to indicate an expected result,
+ /// dN is data to be passed to the calculation.)
+ void executeCallCallouts(int r0, int d1, int r1, int d2, int r2, int d3,
+ int r3) {
+ HooksCommonTestClass::executeCallCallouts(callout_manager_, r0, d1,
+ r1, d2, r2, d3, r3);
+ }
+
+ /// Callout manager used for the test.
+ boost::shared_ptr<CalloutManager> callout_manager_;
+};
+
+/// @brief Library manager class
+///
+/// This is an instance of the LibraryManager class but with the protected
+/// methods made public for test purposes.
+
+class PublicLibraryManager : public isc::hooks::LibraryManager {
+public:
+ /// @brief Constructor
+ ///
+ /// Stores the library name. The actual loading is done in loadLibrary().
+ ///
+ /// @param name Name of the library to load. This should be an absolute
+ /// path name.
+ /// @param index Index of this library. For all these tests, it will be
+ /// zero, as we are only using one library.
+ /// @param manager CalloutManager object
+ PublicLibraryManager(const std::string& name, int index,
+ const boost::shared_ptr<CalloutManager>& manager)
+ : LibraryManager(name, index, manager)
+ {}
+
+ /// Public methods that call protected methods on the superclass.
+ using LibraryManager::openLibrary;
+ using LibraryManager::closeLibrary;
+ using LibraryManager::checkVersion;
+ using LibraryManager::registerStandardCallouts;
+ using LibraryManager::runLoad;
+ using LibraryManager::runUnload;
+};
+
+
+// Check that openLibrary() reports an error when it can't find the specified
+// library.
+
+TEST_F(LibraryManagerTest, NoLibrary) {
+ PublicLibraryManager lib_manager(std::string(NOT_PRESENT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_FALSE(lib_manager.openLibrary());
+}
+
+// Check that the openLibrary() and closeLibrary() methods work.
+
+TEST_F(LibraryManagerTest, OpenClose) {
+ PublicLibraryManager lib_manager(std::string(BASIC_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+
+ // Open and close the library
+ EXPECT_TRUE(lib_manager.openLibrary());
+ EXPECT_TRUE(lib_manager.closeLibrary());
+
+ // Check that a second close on an already closed library does not report
+ // an error.
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check that the code handles the case of a library with no version function.
+
+TEST_F(LibraryManagerTest, NoVersion) {
+ PublicLibraryManager lib_manager(std::string(NO_VERSION_LIBRARY),
+ 0, callout_manager_);
+ // Open should succeed.
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Version check should fail.
+ EXPECT_FALSE(lib_manager.checkVersion());
+
+ // Tidy up.
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check that the code handles the case of a library with a version function
+// that returns an incorrect version number.
+
+TEST_F(LibraryManagerTest, WrongVersion) {
+ PublicLibraryManager lib_manager(std::string(INCORRECT_VERSION_LIBRARY),
+ 0, callout_manager_);
+ // Open should succeed.
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Version check should fail.
+ EXPECT_FALSE(lib_manager.checkVersion());
+
+ // Tidy up.
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check that the code handles the case of a library where the version function
+// throws an exception.
+
+TEST_F(LibraryManagerTest, VersionException) {
+ PublicLibraryManager lib_manager(std::string(FRAMEWORK_EXCEPTION_LIBRARY),
+ 0, callout_manager_);
+ // Open should succeed.
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Version check should fail.
+ EXPECT_FALSE(lib_manager.checkVersion());
+
+ // Tidy up.
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Tests that checkVersion() function succeeds in the case of a library with a
+// version function that returns the correct version number.
+
+TEST_F(LibraryManagerTest, CorrectVersionReturned) {
+ PublicLibraryManager lib_manager(std::string(BASIC_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ // Open should succeed.
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Version check should succeed.
+ EXPECT_TRUE(lib_manager.checkVersion());
+
+ // Tidy up.
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Checks the registration of standard callouts.
+
+TEST_F(LibraryManagerTest, RegisterStandardCallouts) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(BASIC_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check the version of the library.
+ EXPECT_TRUE(lib_manager.checkVersion());
+
+ // Load the standard callouts
+ EXPECT_NO_THROW(lib_manager.registerStandardCallouts());
+
+ // Now execute the callouts in the order expected. The library performs
+ // the calculation:
+ //
+ // r3 = (10 + d1) * d2 - d3
+ executeCallCallouts(10, 5, 15, 7, 105, 17, 88);
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Test that the "load" function is called correctly.
+
+TEST_F(LibraryManagerTest, CheckLoadCalled) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(LOAD_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check the version of the library.
+ EXPECT_TRUE(lib_manager.checkVersion());
+
+ // Load the standard callouts
+ EXPECT_NO_THROW(lib_manager.registerStandardCallouts());
+
+ // Check that only context_create and hookpt_one have callouts registered.
+ EXPECT_TRUE(callout_manager_->calloutsPresent(
+ ServerHooks::CONTEXT_CREATE));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_three_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(
+ ServerHooks::CONTEXT_DESTROY));
+
+ // Call the runLoad() method to run the load() function.
+ EXPECT_TRUE(lib_manager.runLoad());
+ EXPECT_TRUE(callout_manager_->calloutsPresent(
+ ServerHooks::CONTEXT_CREATE));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_three_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(
+ ServerHooks::CONTEXT_DESTROY));
+
+ // Now execute the callouts in the order expected. The library performs
+ // the calculation:
+ //
+ // r3 = (5 * d1 + d2) * d3
+ executeCallCallouts(5, 5, 25, 7, 32, 10, 320);
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check handling of a "load" function that throws an exception
+
+TEST_F(LibraryManagerTest, CheckLoadException) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(FRAMEWORK_EXCEPTION_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Running the load function should fail.
+ EXPECT_FALSE(lib_manager.runLoad());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check handling of a "load" function that returns an error.
+
+TEST_F(LibraryManagerTest, CheckLoadError) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(LOAD_ERROR_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check that we catch a load error
+ EXPECT_FALSE(lib_manager.runLoad());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// No unload function
+
+TEST_F(LibraryManagerTest, CheckNoUnload) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(BASIC_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check that no unload function returns true.
+ EXPECT_TRUE(lib_manager.runUnload());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Unload function returns an error
+
+TEST_F(LibraryManagerTest, CheckUnloadError) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(LOAD_ERROR_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check that unload function returning an error returns false.
+ EXPECT_FALSE(lib_manager.runUnload());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Unload function throws an exception.
+
+TEST_F(LibraryManagerTest, CheckUnloadException) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(FRAMEWORK_EXCEPTION_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check that we detect that the unload function throws an exception.
+ EXPECT_FALSE(lib_manager.runUnload());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Check that the case of the library's unload() function returning a
+// success is handled correcty.
+
+TEST_F(LibraryManagerTest, CheckUnload) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(UNLOAD_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+
+ // Check that the marker file is not present (at least that the file
+ // open fails).
+ EXPECT_FALSE(markerFilePresent());
+
+ // Check that unload function runs and returns a success
+ EXPECT_TRUE(lib_manager.runUnload());
+
+ // Check that the marker file was created.
+ EXPECT_TRUE(markerFilePresent());
+
+ // Tidy up
+ EXPECT_TRUE(lib_manager.closeLibrary());
+}
+
+// Test the operation of unloadLibrary(). We load a library with a set
+// of callouts then unload it. We need to check that the callouts have been
+// removed. We'll also check that the library's unload() function was called
+// as well.
+
+TEST_F(LibraryManagerTest, LibUnload) {
+
+ // Load the only library, specifying the index of 0 as it's the only
+ // library. This should load all callouts.
+ PublicLibraryManager lib_manager(std::string(FULL_CALLOUT_LIBRARY),
+ 0, callout_manager_);
+ EXPECT_TRUE(lib_manager.openLibrary());
+
+ // Check the version of the library.
+ EXPECT_TRUE(lib_manager.checkVersion());
+
+ // No callouts should be registered at the moment.
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_three_index_));
+
+ // Load the single standard callout and check it is registered correctly.
+ EXPECT_NO_THROW(lib_manager.registerStandardCallouts());
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_three_index_));
+
+ // Call the load function to load the other callouts.
+ EXPECT_TRUE(lib_manager.runLoad());
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_TRUE(callout_manager_->calloutsPresent(hookpt_three_index_));
+
+ // Unload the library and check that the callouts have been removed from
+ // the CalloutManager.
+ lib_manager.unloadLibrary();
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_three_index_));
+}
+
+// Now come the loadLibrary() tests that make use of all the methods tested
+// above. These tests are really to make sure that the methods have been
+// tied together correctly.
+
+// First test the basic error cases - no library, no version function, version
+// function returning an error.
+
+TEST_F(LibraryManagerTest, LoadLibraryNoLibrary) {
+ LibraryManager lib_manager(std::string(NOT_PRESENT_LIBRARY), 0,
+ callout_manager_);
+ EXPECT_FALSE(lib_manager.loadLibrary());
+}
+
+// Check that the code handles the case of a library with no version function.
+
+TEST_F(LibraryManagerTest, LoadLibraryNoVersion) {
+ LibraryManager lib_manager(std::string(NO_VERSION_LIBRARY), 0,
+ callout_manager_);
+ EXPECT_FALSE(lib_manager.loadLibrary());
+}
+
+// Check that the code handles the case of a library with a version function
+// that returns an incorrect version number.
+
+TEST_F(LibraryManagerTest, LoadLibraryWrongVersion) {
+ LibraryManager lib_manager(std::string(INCORRECT_VERSION_LIBRARY), 0,
+ callout_manager_);
+ EXPECT_FALSE(lib_manager.loadLibrary());
+}
+
+// Check that the full loadLibrary call works.
+
+TEST_F(LibraryManagerTest, LoadLibrary) {
+ LibraryManager lib_manager(std::string(FULL_CALLOUT_LIBRARY), 0,
+ callout_manager_);
+ EXPECT_TRUE(lib_manager.loadLibrary());
+
+ // Now execute the callouts in the order expected. The library performs
+ // the calculation:
+ //
+ // r3 = (7 * d1 - d2) * d3
+ executeCallCallouts(7, 5, 35, 9, 26, 3, 78);
+
+ EXPECT_TRUE(lib_manager.unloadLibrary());
+
+ // Check that the callouts have been removed from the callout manager.
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_one_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_two_index_));
+ EXPECT_FALSE(callout_manager_->calloutsPresent(hookpt_three_index_));
+}
+
+// Now test for multiple libraries. We'll load the full callout library
+// first, then load some of the libraries with missing framework functions.
+// This will check that when searching for framework functions, only the
+// specified library is checked, not other loaded libraries. We will
+// load a second library with suitable callouts and check that the callouts
+// are added correctly. Finally, we'll unload one of the libraries and
+// check that only the callouts belonging to that library were removed.
+
+TEST_F(LibraryManagerTest, LoadMultipleLibraries) {
+ // Load a library with all framework functions.
+ LibraryManager lib_manager_1(std::string(FULL_CALLOUT_LIBRARY), 0,
+ callout_manager_);
+ EXPECT_TRUE(lib_manager_1.loadLibrary());
+
+ // Attempt to load a library with no version() function. We should detect
+ // this and not end up calling the function from the already loaded
+ // library.
+ LibraryManager lib_manager_2(std::string(NO_VERSION_LIBRARY), 1,
+ callout_manager_);
+ EXPECT_FALSE(lib_manager_2.loadLibrary());
+
+ // Attempt to load the library with an incorrect version. This should
+ // be detected.
+ LibraryManager lib_manager_3(std::string(INCORRECT_VERSION_LIBRARY), 1,
+ callout_manager_);
+ EXPECT_FALSE(lib_manager_3.loadLibrary());
+
+ // Load the basic callout library. This only has standard callouts so,
+ // if the first library's load() function gets called, some callouts
+ // will be registered twice and lead to incorrect results.
+ LibraryManager lib_manager_4(std::string(BASIC_CALLOUT_LIBRARY), 1,
+ callout_manager_);
+ EXPECT_TRUE(lib_manager_4.loadLibrary());
+
+ // Execute the callouts. The first library implements the calculation.
+ //
+ // r3 = (7 * d1 - d2) * d3
+ //
+ // The last-loaded library implements the calculation
+ //
+ // r3 = (10 + d1) * d2 - d3
+ //
+ // Putting the processing for each library together in the appropriate
+ // order, we get:
+ //
+ // r3 = ((10 * d1 + d1) - d2) * d2 * d3 - d3
+ executeCallCallouts(10, 3, 33, 2, 62, 3, 183);
+
+ // All done, so unload the first library.
+ EXPECT_TRUE(lib_manager_1.unloadLibrary());
+
+ // Now execute the callouts again and check that the results are as
+ // expected for the new calculation.
+ executeCallCallouts(10, 5, 15, 7, 105, 17, 88);
+
+ // ... and tidy up.
+ EXPECT_TRUE(lib_manager_4.unloadLibrary());
+}
+
+// Check that libraries can be validated.
+
+TEST_F(LibraryManagerTest, validateLibraries) {
+ EXPECT_TRUE(LibraryManager::validateLibrary(BASIC_CALLOUT_LIBRARY));
+ EXPECT_TRUE(LibraryManager::validateLibrary(FULL_CALLOUT_LIBRARY));
+ EXPECT_FALSE(LibraryManager::validateLibrary(FRAMEWORK_EXCEPTION_LIBRARY));
+ EXPECT_FALSE(LibraryManager::validateLibrary(INCORRECT_VERSION_LIBRARY));
+ EXPECT_TRUE(LibraryManager::validateLibrary(LOAD_CALLOUT_LIBRARY));
+ EXPECT_TRUE(LibraryManager::validateLibrary(LOAD_ERROR_CALLOUT_LIBRARY));
+ EXPECT_FALSE(LibraryManager::validateLibrary(NOT_PRESENT_LIBRARY));
+ EXPECT_FALSE(LibraryManager::validateLibrary(NO_VERSION_LIBRARY));
+ EXPECT_TRUE(LibraryManager::validateLibrary(UNLOAD_CALLOUT_LIBRARY));
+}
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/load_callout_library.cc b/src/lib/hooks/tests/load_callout_library.cc
new file mode 100644
index 0000000..59a58b5
--- /dev/null
+++ b/src/lib/hooks/tests/load_callout_library.cc
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Basic library with load() function
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - The "version" and "load" framework functions are supplied. One "standard"
+/// callout is supplied ("hookpt_one") and two non-standard ones which are
+/// registered during the call to "load" on the hooks "hookpt_two" and
+/// "hookpt_three".
+///
+/// All callouts do trivial calculations, the result of all being called in
+/// sequence being
+///
+/// @f[ ((5 * data_1) + data_2) * data_3 @f]
+///
+/// ...where data_1, data_2 and data_3 are the values passed in arguments of
+/// the same name to the three callouts (data_1 passed to hookpt_one, data_2
+/// to hookpt_two etc.) and the result is returned in the argument "result".
+
+#include <hooks/hooks.h>
+
+using namespace isc::hooks;
+
+extern "C" {
+
+// Callouts
+
+int
+context_create(CalloutHandle& handle) {
+ handle.setContext("result", static_cast<int>(5));
+ handle.setArgument("result", static_cast<int>(5));
+ return (0);
+}
+
+// First callout adds the passed "data_1" argument to the initialized context
+// value of 5. (Note that the value set by context_create is accessed through
+// context and not the argument, so checking that context is correctly passed
+// between callouts in the same library.)
+
+int
+hookpt_one(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_1", data);
+
+ int result;
+ handle.getContext("result", result);
+
+ result *= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Second callout multiplies the current context value by the "data_2"
+// argument.
+
+static int
+hook_nonstandard_two(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_2", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result += data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Final callout adds "data_3" to the result.
+
+static int
+hook_nonstandard_three(CalloutHandle& handle) {
+ int data;
+ handle.getArgument("data_3", data);
+
+ int result;
+ handle.getArgument("result", result);
+
+ result *= data;
+ handle.setArgument("result", result);
+
+ return (0);
+}
+
+// Framework functions
+
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int load(LibraryHandle& handle) {
+ // Initialize the user library if the main image was statically linked
+#ifdef USE_STATIC_LINK
+ hooksStaticLinkInit();
+#endif
+ // Register the non-standard functions
+ handle.registerCallout("hookpt_two", hook_nonstandard_two);
+ handle.registerCallout("hookpt_three", hook_nonstandard_three);
+
+ return (0);
+}
+
+};
+
diff --git a/src/lib/hooks/tests/load_error_callout_library.cc b/src/lib/hooks/tests/load_error_callout_library.cc
new file mode 100644
index 0000000..b861d7f
--- /dev/null
+++ b/src/lib/hooks/tests/load_error_callout_library.cc
@@ -0,0 +1,49 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Error load library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - All framework functions are supplied. "version" returns the correct
+/// value, but "load" and unload return an error.
+
+#include <hooks/hooks.h>
+
+using namespace isc::hooks;
+
+extern "C" {
+
+// Framework functions
+
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int
+load(LibraryHandle&) {
+ return (1);
+}
+
+int
+unload() {
+ return (1);
+}
+
+};
+
diff --git a/src/lib/hooks/tests/marker_file.h.in b/src/lib/hooks/tests/marker_file.h.in
new file mode 100644
index 0000000..e032cdd
--- /dev/null
+++ b/src/lib/hooks/tests/marker_file.h.in
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef MARKER_FILE_H
+#define MARKER_FILE_H
+
+/// @file
+/// Define a marker file that is used in tests to prove that an "unload"
+/// function has been called.
+
+namespace {
+const char* MARKER_FILE = "@abs_builddir@/marker_file.dat";
+}
+
+#endif // MARKER_FILE_H
+
diff --git a/src/lib/hooks/tests/no_version_library.cc b/src/lib/hooks/tests/no_version_library.cc
new file mode 100644
index 0000000..f5b5b9c
--- /dev/null
+++ b/src/lib/hooks/tests/no_version_library.cc
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief No version function library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - No version() function is present.
+
+extern "C" {
+
+int no_version() {
+ return (0);
+}
+
+};
diff --git a/src/lib/hooks/tests/run_unittests.cc b/src/lib/hooks/tests/run_unittests.cc
new file mode 100644
index 0000000..f68a58d
--- /dev/null
+++ b/src/lib/hooks/tests/run_unittests.cc
@@ -0,0 +1,25 @@
+// Copyright (C) 2009 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/logger_support.h>
+#include <util/unittests/run_all.h>
+
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+ ::testing::InitGoogleTest(&argc, argv);
+ isc::log::initLogger();
+ return (isc::util::unittests::run_all());
+}
diff --git a/src/lib/hooks/tests/server_hooks_unittest.cc b/src/lib/hooks/tests/server_hooks_unittest.cc
new file mode 100644
index 0000000..ca9b6f0
--- /dev/null
+++ b/src/lib/hooks/tests/server_hooks_unittest.cc
@@ -0,0 +1,178 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <hooks/server_hooks.h>
+
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+using namespace isc;
+using namespace isc::hooks;
+using namespace std;
+
+namespace {
+
+// Checks the registration of hooks and the interrogation methods. As the
+// constructor registers two hooks, this is also a test of the constructor.
+
+TEST(ServerHooksTest, RegisterHooks) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ // There should be two hooks already registered, with indexes 0 and 1.
+ EXPECT_EQ(2, hooks.getCount());
+ EXPECT_EQ(0, hooks.getIndex("context_create"));
+ EXPECT_EQ(1, hooks.getIndex("context_destroy"));
+
+ // Check that the constants are as expected. (The intermediate variables
+ // are used because of problems with g++ 4.6.1/Ubuntu 11.10 when resolving
+ // the value of the ServerHooks constants when they appeared within the
+ // gtest macro.)
+ const int create_value = ServerHooks::CONTEXT_CREATE;
+ const int destroy_value = ServerHooks::CONTEXT_DESTROY;
+ EXPECT_EQ(0, create_value);
+ EXPECT_EQ(1, destroy_value);
+
+ // Register another couple of hooks. The test on returned index is based
+ // on knowledge that the hook indexes are assigned in ascending order.
+ int alpha = hooks.registerHook("alpha");
+ EXPECT_EQ(2, alpha);
+ EXPECT_EQ(2, hooks.getIndex("alpha"));
+
+ int beta = hooks.registerHook("beta");
+ EXPECT_EQ(3, beta);
+ EXPECT_EQ(3, hooks.getIndex("beta"));
+
+ // Should be four hooks now
+ EXPECT_EQ(4, hooks.getCount());
+}
+
+// Check that duplicate names cannot be registered.
+
+TEST(ServerHooksTest, DuplicateHooks) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ // Ensure we can't duplicate one of the existing names.
+ EXPECT_THROW(hooks.registerHook("context_create"), DuplicateHook);
+
+ // Check we can't duplicate a newly registered hook.
+ int gamma = hooks.registerHook("gamma");
+ EXPECT_EQ(2, gamma);
+ EXPECT_THROW(hooks.registerHook("gamma"), DuplicateHook);
+}
+
+// Checks that we can get the name of the hooks.
+
+TEST(ServerHooksTest, GetHookNames) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+ vector<string> expected_names;
+
+ // Add names into the hooks object and to the set of expected names.
+ expected_names.push_back("alpha");
+ expected_names.push_back("beta");
+ expected_names.push_back("gamma");
+ expected_names.push_back("delta");
+ for (int i = 0; i < expected_names.size(); ++i) {
+ hooks.registerHook(expected_names[i].c_str());
+ };
+
+ // Update the expected names to include the pre-defined hook names.
+ expected_names.push_back("context_create");
+ expected_names.push_back("context_destroy");
+
+ // Get the actual hook names
+ vector<string> actual_names = hooks.getHookNames();
+
+ // For comparison, sort the names into alphabetical order and do a straight
+ // vector comparison.
+ sort(expected_names.begin(), expected_names.end());
+ sort(actual_names.begin(), actual_names.end());
+
+ EXPECT_TRUE(expected_names == actual_names);
+}
+
+// Test the inverse hooks functionality (i.e. given an index, get the name).
+
+TEST(ServerHooksTest, GetHookIndexes) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ int alpha = hooks.registerHook("alpha");
+ int beta = hooks.registerHook("beta");
+ int gamma = hooks.registerHook("gamma");
+
+ EXPECT_EQ(std::string("context_create"),
+ hooks.getName(ServerHooks::CONTEXT_CREATE));
+ EXPECT_EQ(std::string("context_destroy"),
+ hooks.getName(ServerHooks::CONTEXT_DESTROY));
+ EXPECT_EQ(std::string("alpha"), hooks.getName(alpha));
+ EXPECT_EQ(std::string("beta"), hooks.getName(beta));
+ EXPECT_EQ(std::string("gamma"), hooks.getName(gamma));
+
+ // Check for an invalid index
+ EXPECT_THROW(hooks.getName(-1), NoSuchHook);
+ EXPECT_THROW(hooks.getName(42), NoSuchHook);
+}
+
+// Test the reset functionality.
+
+TEST(ServerHooksTest, Reset) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ int alpha = hooks.registerHook("alpha");
+ int beta = hooks.registerHook("beta");
+ int gamma = hooks.registerHook("gamma");
+
+ // Check the counts before and after a reset.
+ EXPECT_EQ(5, hooks.getCount());
+ hooks.reset();
+ EXPECT_EQ(2, hooks.getCount());
+
+ // ... and check that the hooks are as expected.
+ EXPECT_EQ(0, hooks.getIndex("context_create"));
+ EXPECT_EQ(1, hooks.getIndex("context_destroy"));
+}
+
+// Check that getting an unknown name throws an exception.
+
+TEST(ServerHooksTest, UnknownHookName) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ EXPECT_THROW(static_cast<void>(hooks.getIndex("unknown")), NoSuchHook);
+}
+
+// Check that the count of hooks is correct.
+
+TEST(ServerHooksTest, HookCount) {
+ ServerHooks& hooks = ServerHooks::getServerHooks();
+ hooks.reset();
+
+ // Insert the names into the hooks object
+ hooks.registerHook("alpha");
+ hooks.registerHook("beta");
+ hooks.registerHook("gamma");
+ hooks.registerHook("delta");
+
+ // Should be two more hooks that the number we have registered.
+ EXPECT_EQ(6, hooks.getCount());
+}
+
+} // Anonymous namespace
diff --git a/src/lib/hooks/tests/test_libraries.h.in b/src/lib/hooks/tests/test_libraries.h.in
new file mode 100644
index 0000000..7b5e0e4
--- /dev/null
+++ b/src/lib/hooks/tests/test_libraries.h.in
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef TEST_LIBRARIES_H
+#define TEST_LIBRARIES_H
+
+#include <config.h>
+
+namespace {
+
+// Names of the libraries used in these tests. These libraries are built using
+// libtool, so we need to look in the hidden ".libs" directory to locate the
+// .so file. Note that we access the .so file - libtool creates this as a
+// like to the real shared library.
+
+// Basic library with context_create and three "standard" callouts.
+static const char* BASIC_CALLOUT_LIBRARY = "@abs_builddir@/.libs/libbcl.so";
+
+// Library with context_create and three "standard" callouts, as well as
+// load() and unload() functions.
+static const char* FULL_CALLOUT_LIBRARY = "@abs_builddir@/.libs/libfcl.so";
+
+// Library where the all framework functions throw an exception
+static const char* FRAMEWORK_EXCEPTION_LIBRARY = "@abs_builddir@/.libs/libfxl.so";
+
+// Library where the version() function returns an incorrect result.
+static const char* INCORRECT_VERSION_LIBRARY = "@abs_builddir@/.libs/libivl.so";
+
+// Library where some of the callout registration is done with the load()
+// function.
+static const char* LOAD_CALLOUT_LIBRARY = "@abs_builddir@/.libs/liblcl.so";
+
+// Library where the load() function returns an error.
+static const char* LOAD_ERROR_CALLOUT_LIBRARY =
+ "@abs_builddir@/.libs/liblecl.so";
+
+// Name of a library which is not present.
+static const char* NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
+
+// Library that does not include a version function.
+static const char* NO_VERSION_LIBRARY = "@abs_builddir@/.libs/libnvl.so";
+
+// Library where there is an unload() function.
+static const char* UNLOAD_CALLOUT_LIBRARY = "@abs_builddir@/.libs/libucl.so";
+
+} // anonymous namespace
+
+
+#endif // TEST_LIBRARIES_H
diff --git a/src/lib/hooks/tests/unload_callout_library.cc b/src/lib/hooks/tests/unload_callout_library.cc
new file mode 100644
index 0000000..9baa830
--- /dev/null
+++ b/src/lib/hooks/tests/unload_callout_library.cc
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// @file
+/// @brief Basic unload library
+///
+/// This is source of a test library for various test (LibraryManager and
+/// HooksManager). The characteristics of the library produced from this
+/// file are:
+///
+/// - The "version" and "unload" framework functions are supplied. "version"
+/// returns a valid value and "unload" creates a marker file and returns
+/// success.
+
+#include <hooks/hooks.h>
+#include <hooks/tests/marker_file.h>
+
+#include <fstream>
+
+using namespace isc::hooks;
+
+extern "C" {
+
+// Framework functions
+
+int
+version() {
+ return (BIND10_HOOKS_VERSION);
+}
+
+int
+unload() {
+ // Create the marker file.
+ std::fstream marker;
+ marker.open(MARKER_FILE, std::fstream::out);
+ marker.close();
+
+ return (0);
+}
+
+};
diff --git a/src/lib/log/Makefile.am b/src/lib/log/Makefile.am
index 18d5f90..0bd1b05 100644
--- a/src/lib/log/Makefile.am
+++ b/src/lib/log/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = . compiler tests
+SUBDIRS = interprocess . compiler tests
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
@@ -8,7 +8,6 @@ CLEANFILES = *.gcno *.gcda
lib_LTLIBRARIES = libb10-log.la
libb10_log_la_SOURCES =
-libb10_log_la_SOURCES += dummylog.h dummylog.cc
libb10_log_la_SOURCES += logimpl_messages.cc logimpl_messages.h
libb10_log_la_SOURCES += log_dbglevels.h
libb10_log_la_SOURCES += log_formatter.h log_formatter.cc
@@ -43,12 +42,19 @@ libb10_log_la_CXXFLAGS = $(AM_CXXFLAGS)
if USE_GXX
libb10_log_la_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-libb10_log_la_CXXFLAGS += -Wno-error
-endif
libb10_log_la_CPPFLAGS = $(AM_CPPFLAGS) $(LOG4CPLUS_INCLUDES)
libb10_log_la_LIBADD = $(top_builddir)/src/lib/util/libb10-util.la
-libb10_log_la_LIBADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
+libb10_log_la_LIBADD += interprocess/libb10-log_interprocess.la
libb10_log_la_LIBADD += $(LOG4CPLUS_LIBS)
libb10_log_la_LDFLAGS = -no-undefined -version-info 1:0:0
+
+# Specify the headers for copying into the installation directory tree. User-
+# written libraries only need the definitions for logger.h and dependencies.
+libb10_log_includedir = $(pkgincludedir)/log
+libb10_log_include_HEADERS = \
+ log_formatter.h \
+ logger.h \
+ logger_level.h \
+ macros.h \
+ message_types.h
+
diff --git a/src/lib/log/dummylog.cc b/src/lib/log/dummylog.cc
deleted file mode 100644
index 5f025e1..0000000
--- a/src/lib/log/dummylog.cc
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include "dummylog.h"
-
-#include <iostream>
-
-using namespace std;
-
-namespace isc {
-namespace log {
-
-bool denabled = false;
-string dprefix;
-
-void dlog(const string& message,bool error_flag) {
- if (denabled || error_flag) {
- if (!dprefix.empty()) {
- cerr << "[" << dprefix << "] ";
- }
- cerr << message << endl;
- }
-}
-
-}
-}
diff --git a/src/lib/log/dummylog.h b/src/lib/log/dummylog.h
deleted file mode 100644
index 6f6ae97..0000000
--- a/src/lib/log/dummylog.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef ISC_DUMMYLOG_H
-#define ISC_DUMMYLOG_H 1
-
-#include <string>
-
-namespace isc {
-namespace log {
-
-/// Are we doing logging?
-extern bool denabled;
-/**
- * \short Prefix into logs.
- *
- * The prefix is printed in front of every log message in square brackets.
- * The usual convention is to put the name of program here.
- */
-extern std::string dprefix;
-
-/**
- * \short Temporary interface to logging.
- *
- * This is a temporary function to do logging. It has wrong interface currently
- * and should be replaced by something else. Its main purpose now is to mark
- * places where logging should happen. When it is removed, compiler will do
- * our work of finding the places.
- *
- * The only thing it does is printing the program prefix, message and
- * a newline if denabled is true.
- *
- * There are no tests for this function, since it is only temporary and
- * trivial. Tests will be written for the real logging framework when it is
- * created.
- *
- * It has the d in front of the name so it is unlikely anyone will create
- * a real logging function with the same name and the place wouldn't be found
- * as a compilation error.
- *
- * @param message The message to log. The real interface will probably have
- * more parameters.
- * \param error_flag TODO
- */
-void dlog(const std::string& message, bool error_flag=false);
-
-}
-}
-
-#endif // ISC_DUMMYLOG_H
diff --git a/src/lib/log/interprocess/Makefile.am b/src/lib/log/interprocess/Makefile.am
new file mode 100644
index 0000000..567ff09
--- /dev/null
+++ b/src/lib/log/interprocess/Makefile.am
@@ -0,0 +1,21 @@
+SUBDIRS = . tests
+
+AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
+AM_CPPFLAGS += -DLOCKFILE_DIR=\"${localstatedir}/${PACKAGE_NAME}\"
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+CLEANFILES = *.gcno *.gcda
+
+noinst_LTLIBRARIES = libb10-log_interprocess.la
+
+libb10_log_interprocess_la_SOURCES = interprocess_sync.h
+libb10_log_interprocess_la_SOURCES += interprocess_sync_file.h
+libb10_log_interprocess_la_SOURCES += interprocess_sync_file.cc
+libb10_log_interprocess_la_SOURCES += interprocess_sync_null.h
+libb10_log_interprocess_la_SOURCES += interprocess_sync_null.cc
+
+libb10_log_interprocess_la_LIBADD = $(top_builddir)/src/lib/util/threads/libb10-threads.la
+
+EXTRA_DIST = README
diff --git a/src/lib/log/interprocess/README b/src/lib/log/interprocess/README
new file mode 100644
index 0000000..e910a3a
--- /dev/null
+++ b/src/lib/log/interprocess/README
@@ -0,0 +1,13 @@
+The files in this directory implement a helper sub-library of the
+inter process locking for the log library. We use our own locks
+because such locks are only available in relatively recent versions of
+log4cplus. Also (against our usual practice) we somehow re-invented
+an in-house version of such a general purose library rather than
+existing proven tools such as boost::interprocess. While we decided
+to go with the in-house version for the log library at least until we
+completely swith to log4cplus's native lock support, no other BIND 10
+module should use this; they should use existing external
+tools/libraries.
+
+This sub-library is therefore "hidden" here. As such, none of these
+files should be installed.
diff --git a/src/lib/log/interprocess/interprocess_sync.h b/src/lib/log/interprocess/interprocess_sync.h
new file mode 100644
index 0000000..10453cc
--- /dev/null
+++ b/src/lib/log/interprocess/interprocess_sync.h
@@ -0,0 +1,151 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef INTERPROCESS_SYNC_H
+#define INTERPROCESS_SYNC_H
+
+#include <string>
+
+namespace isc {
+namespace log {
+namespace interprocess {
+
+class InterprocessSyncLocker; // forward declaration
+
+/// \brief Interprocess Sync Class
+///
+/// This class specifies an interface for mutual exclusion among
+/// co-operating processes. This is an abstract class and a real
+/// implementation such as InterprocessSyncFile should be used
+/// in code. Usage is as follows:
+///
+/// 1. Client instantiates a sync object of an implementation (such as
+/// InterprocessSyncFile).
+/// 2. Client then creates an automatic (stack) object of
+/// InterprocessSyncLocker around the sync object. Such an object
+/// destroys itself and releases any acquired lock when it goes out of extent.
+/// 3. Client calls lock() method on the InterprocessSyncLocker.
+/// 4. Client performs task that needs mutual exclusion.
+/// 5. Client frees lock with unlock(), or simply returns from the basic
+/// block which forms the scope for the InterprocessSyncLocker.
+///
+/// NOTE: All implementations of InterprocessSync should keep the
+/// is_locked_ member variable updated whenever their
+/// lock()/tryLock()/unlock() implementations are called.
+class InterprocessSync {
+ // InterprocessSyncLocker is the only code outside this class that
+ // should be allowed to call the lock(), tryLock() and unlock()
+ // methods.
+ friend class InterprocessSyncLocker;
+
+public:
+ /// \brief Constructor
+ ///
+ /// Creates an interprocess synchronization object
+ ///
+ /// \param task_name Name of the synchronization task. This has to be
+ /// identical among the various processes that need to be
+ /// synchronized for the same task.
+ InterprocessSync(const std::string& task_name) :
+ task_name_(task_name), is_locked_(false)
+ {}
+
+ /// \brief Destructor
+ virtual ~InterprocessSync() {}
+
+protected:
+ /// \brief Acquire the lock (blocks if something else has acquired a
+ /// lock on the same task name)
+ ///
+ /// \return Returns true if the lock was acquired, false otherwise.
+ virtual bool lock() = 0;
+
+ /// \brief Try to acquire a lock (doesn't block)
+ ///
+ /// \return Returns true if the lock was acquired, false otherwise.
+ virtual bool tryLock() = 0;
+
+ /// \brief Release the lock
+ ///
+ /// \return Returns true if the lock was released, false otherwise.
+ virtual bool unlock() = 0;
+
+ const std::string task_name_; ///< The task name
+ bool is_locked_; ///< Is the lock taken?
+};
+
+/// \brief Interprocess Sync Locker Class
+///
+/// This class is used for making automatic stack objects to manage
+/// locks that are released automatically when the block is exited
+/// (RAII). It is meant to be used along with InterprocessSync objects. See
+/// the description of InterprocessSync.
+class InterprocessSyncLocker {
+public:
+ /// \brief Constructor
+ ///
+ /// Creates a lock manager around a interprocess synchronization object
+ ///
+ /// \param sync The sync object which has to be locked/unlocked by
+ /// this locker object.
+ InterprocessSyncLocker(InterprocessSync& sync) :
+ sync_(sync)
+ {}
+
+ /// \brief Destructor
+ ~InterprocessSyncLocker() {
+ if (isLocked())
+ unlock();
+ }
+
+ /// \brief Acquire the lock (blocks if something else has acquired a
+ /// lock on the same task name)
+ ///
+ /// \return Returns true if the lock was acquired, false otherwise.
+ bool lock() {
+ return (sync_.lock());
+ }
+
+ /// \brief Try to acquire a lock (doesn't block)
+ ///
+ /// \return Returns true if a new lock could be acquired, false
+ /// otherwise.
+ bool tryLock() {
+ return (sync_.tryLock());
+ }
+
+ /// \brief Check if the lock is taken
+ ///
+ /// \return Returns true if a lock is currently acquired, false
+ /// otherwise.
+ bool isLocked() const {
+ return (sync_.is_locked_);
+ }
+
+ /// \brief Release the lock
+ ///
+ /// \return Returns true if the lock was released, false otherwise.
+ bool unlock() {
+ return (sync_.unlock());
+ }
+
+protected:
+ InterprocessSync& sync_; ///< Ref to underlying sync object
+};
+
+} // namespace interprocess
+} // namespace log
+} // namespace isc
+
+#endif // INTERPROCESS_SYNC_H
diff --git a/src/lib/log/interprocess/interprocess_sync_file.cc b/src/lib/log/interprocess/interprocess_sync_file.cc
new file mode 100644
index 0000000..7f8fcb4
--- /dev/null
+++ b/src/lib/log/interprocess/interprocess_sync_file.cc
@@ -0,0 +1,134 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/interprocess/interprocess_sync_file.h>
+
+#include <string>
+#include <cerrno>
+#include <cstring>
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+namespace isc {
+namespace log {
+namespace interprocess {
+
+InterprocessSyncFile::~InterprocessSyncFile() {
+ if (fd_ != -1) {
+ // This will also release any applied locks.
+ close(fd_);
+ // The lockfile will continue to exist, and we must not delete
+ // it.
+ }
+}
+
+bool
+InterprocessSyncFile::do_lock(int cmd, short l_type) {
+ // Open lock file only when necessary (i.e., here). This is so that
+ // if a default InterprocessSync object is replaced with another
+ // implementation, it doesn't attempt any opens.
+ if (fd_ == -1) {
+ std::string lockfile_path = LOCKFILE_DIR;
+
+ const char* const env = getenv("B10_FROM_BUILD");
+ if (env != NULL) {
+ lockfile_path = env;
+ }
+
+ const char* const env2 = getenv("B10_FROM_BUILD_LOCALSTATEDIR");
+ if (env2 != NULL) {
+ lockfile_path = env2;
+ }
+
+ const char* const env3 = getenv("B10_LOCKFILE_DIR_FROM_BUILD");
+ if (env3 != NULL) {
+ lockfile_path = env3;
+ }
+
+ lockfile_path += "/" + task_name_ + "_lockfile";
+
+ // Open the lockfile in the constructor so it doesn't do the access
+ // checks every time a message is logged.
+ const mode_t mode = umask(0111);
+ fd_ = open(lockfile_path.c_str(), O_CREAT | O_RDWR, 0660);
+ umask(mode);
+
+ if (fd_ == -1) {
+ isc_throw(InterprocessSyncFileError,
+ "Unable to use interprocess sync lockfile ("
+ << std::strerror(errno) << "): " << lockfile_path);
+ }
+ }
+
+ struct flock lock;
+
+ memset(&lock, 0, sizeof (lock));
+ lock.l_type = l_type;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 1;
+
+ return (fcntl(fd_, cmd, &lock) == 0);
+}
+
+bool
+InterprocessSyncFile::lock() {
+ if (is_locked_) {
+ return (true);
+ }
+
+ if (do_lock(F_SETLKW, F_WRLCK)) {
+ is_locked_ = true;
+ return (true);
+ }
+
+ return (false);
+}
+
+bool
+InterprocessSyncFile::tryLock() {
+ if (is_locked_) {
+ return (true);
+ }
+
+ if (do_lock(F_SETLK, F_WRLCK)) {
+ is_locked_ = true;
+ return (true);
+ }
+
+ return (false);
+}
+
+bool
+InterprocessSyncFile::unlock() {
+ if (!is_locked_) {
+ return (true);
+ }
+
+ if (do_lock(F_SETLKW, F_UNLCK)) {
+ is_locked_ = false;
+ return (true);
+ }
+
+ return (false);
+}
+
+} // namespace interprocess
+} // namespace log
+} // namespace isc
diff --git a/src/lib/log/interprocess/interprocess_sync_file.h b/src/lib/log/interprocess/interprocess_sync_file.h
new file mode 100644
index 0000000..454cd8b
--- /dev/null
+++ b/src/lib/log/interprocess/interprocess_sync_file.h
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef INTERPROCESS_SYNC_FILE_H
+#define INTERPROCESS_SYNC_FILE_H
+
+#include <log/interprocess/interprocess_sync.h>
+#include <exceptions/exceptions.h>
+
+namespace isc {
+namespace log {
+namespace interprocess {
+
+/// \brief InterprocessSyncFileError
+///
+/// Exception that is thrown if it's not possible to open the
+/// lock file.
+class InterprocessSyncFileError : public Exception {
+public:
+ InterprocessSyncFileError(const char* file, size_t line,
+ const char* what) :
+ isc::Exception(file, line, what) {}
+};
+
+/// \brief File-based Interprocess Sync Class
+///
+/// This class specifies a concrete implementation for a file-based
+/// interprocess synchronization mechanism. Please see the
+/// InterprocessSync class documentation for usage.
+///
+/// An InterprocessSyncFileError exception may be thrown if there is an
+/// issue opening the lock file.
+///
+/// Lock files are created typically in the local state directory
+/// (var). They are typically named like "<task_name>_lockfile".
+/// This implementation opens lock files lazily (only when
+/// necessary). It also leaves the lock files lying around as multiple
+/// processes may have locks on them.
+class InterprocessSyncFile : public InterprocessSync {
+public:
+ /// \brief Constructor
+ ///
+ /// Creates a file-based interprocess synchronization object
+ ///
+ /// \param task_name Name of the synchronization task. This has to be
+ /// identical among the various processes that need to be
+ /// synchronized for the same task.
+ InterprocessSyncFile(const std::string& task_name) :
+ InterprocessSync(task_name), fd_(-1)
+ {}
+
+ /// \brief Destructor
+ virtual ~InterprocessSyncFile();
+
+protected:
+ /// \brief Acquire the lock (blocks if something else has acquired a
+ /// lock on the same task name)
+ ///
+ /// \return Returns true if the lock was acquired, false otherwise.
+ bool lock();
+
+ /// \brief Try to acquire a lock (doesn't block)
+ ///
+ /// \return Returns true if the lock was acquired, false otherwise.
+ bool tryLock();
+
+ /// \brief Release the lock
+ ///
+ /// \return Returns true if the lock was released, false otherwise.
+ bool unlock();
+
+private:
+ bool do_lock(int cmd, short l_type);
+
+ int fd_; ///< The descriptor for the open file
+};
+
+} // namespace interprocess
+} // namespace log
+} // namespace isc
+
+#endif // INTERPROCESS_SYNC_FILE_H
diff --git a/src/lib/log/interprocess/interprocess_sync_null.cc b/src/lib/log/interprocess/interprocess_sync_null.cc
new file mode 100644
index 0000000..226f722
--- /dev/null
+++ b/src/lib/log/interprocess/interprocess_sync_null.cc
@@ -0,0 +1,44 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/interprocess/interprocess_sync_null.h>
+
+namespace isc {
+namespace log {
+namespace interprocess {
+
+InterprocessSyncNull::~InterprocessSyncNull() {
+}
+
+bool
+InterprocessSyncNull::lock() {
+ is_locked_ = true;
+ return (true);
+}
+
+bool
+InterprocessSyncNull::tryLock() {
+ is_locked_ = true;
+ return (true);
+}
+
+bool
+InterprocessSyncNull::unlock() {
+ is_locked_ = false;
+ return (true);
+}
+
+} // namespace interprocess
+} // namespace log
+} // namespace isc
diff --git a/src/lib/log/interprocess/interprocess_sync_null.h b/src/lib/log/interprocess/interprocess_sync_null.h
new file mode 100644
index 0000000..f046f79
--- /dev/null
+++ b/src/lib/log/interprocess/interprocess_sync_null.h
@@ -0,0 +1,66 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef INTERPROCESS_SYNC_NULL_H
+#define INTERPROCESS_SYNC_NULL_H
+
+#include <log/interprocess/interprocess_sync.h>
+
+namespace isc {
+namespace log {
+namespace interprocess {
+
+/// \brief Null Interprocess Sync Class
+///
+/// This class specifies a concrete implementation for a null (no effect)
+/// interprocess synchronization mechanism. Please see the
+/// InterprocessSync class documentation for usage.
+class InterprocessSyncNull : public InterprocessSync {
+public:
+ /// \brief Constructor
+ ///
+ /// Creates a null interprocess synchronization object
+ ///
+ /// \param task_name Name of the synchronization task. This has to be
+ /// identical among the various processes that need to be
+ /// synchronized for the same task.
+ InterprocessSyncNull(const std::string& task_name) :
+ InterprocessSync(task_name)
+ {}
+
+ /// \brief Destructor
+ virtual ~InterprocessSyncNull();
+
+protected:
+ /// \brief Acquire the lock (never blocks)
+ ///
+ /// \return Always returns true
+ bool lock();
+
+ /// \brief Try to acquire a lock (doesn't block)
+ ///
+ /// \return Always returns true
+ bool tryLock();
+
+ /// \brief Release the lock
+ ///
+ /// \return Always returns true
+ bool unlock();
+};
+
+} // namespace interprocess
+} // namespace log
+} // namespace isc
+
+#endif // INTERPROCESS_SYNC_NULL_H
diff --git a/src/lib/log/interprocess/tests/.gitignore b/src/lib/log/interprocess/tests/.gitignore
new file mode 100644
index 0000000..d6d1ec8
--- /dev/null
+++ b/src/lib/log/interprocess/tests/.gitignore
@@ -0,0 +1 @@
+/run_unittests
diff --git a/src/lib/log/interprocess/tests/Makefile.am b/src/lib/log/interprocess/tests/Makefile.am
new file mode 100644
index 0000000..3013f99
--- /dev/null
+++ b/src/lib/log/interprocess/tests/Makefile.am
@@ -0,0 +1,37 @@
+SUBDIRS = .
+
+AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += $(BOOST_INCLUDES)
+# XXX: we'll pollute the top builddir for creating a temporary test file
+# used to bind a UNIX domain socket so we can minimize the risk of exceeding
+# the limit of file name path size.
+AM_CPPFLAGS += -DTEST_DATA_TOPBUILDDIR=\"$(abs_top_builddir)\"
+AM_CXXFLAGS = $(B10_CXXFLAGS)
+
+if USE_STATIC_LINK
+AM_LDFLAGS = -static
+endif
+
+CLEANFILES = *.gcno *.gcda
+
+TESTS_ENVIRONMENT = \
+ $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND)
+
+TESTS =
+if HAVE_GTEST
+TESTS += run_unittests
+run_unittests_SOURCES = run_unittests.cc
+run_unittests_SOURCES += interprocess_sync_file_unittest.cc
+run_unittests_SOURCES += interprocess_sync_null_unittest.cc
+
+run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
+run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
+
+run_unittests_LDADD = ../libb10-log_interprocess.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
+run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
+run_unittests_LDADD += $(GTEST_LDADD)
+endif
+
+noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/log/interprocess/tests/interprocess_sync_file_unittest.cc b/src/lib/log/interprocess/tests/interprocess_sync_file_unittest.cc
new file mode 100644
index 0000000..4df365e
--- /dev/null
+++ b/src/lib/log/interprocess/tests/interprocess_sync_file_unittest.cc
@@ -0,0 +1,151 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/interprocess/interprocess_sync_file.h>
+
+#include <util/unittests/check_valgrind.h>
+#include <util/unittests/interprocess_util.h>
+#include <gtest/gtest.h>
+#include <unistd.h>
+
+using namespace std;
+using namespace isc::log::interprocess;
+using isc::util::unittests::parentReadState;
+
+namespace {
+TEST(InterprocessSyncFileTest, TestLock) {
+ InterprocessSyncFile sync("test");
+ InterprocessSyncLocker locker(sync);
+
+ EXPECT_FALSE(locker.isLocked());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.isLocked());
+
+ if (!isc::util::unittests::runningOnValgrind()) {
+
+ int fds[2];
+
+ // Here, we check that a lock has been taken by forking and
+ // checking from the child that a lock exists. This has to be
+ // done from a separate process as we test by trying to lock the
+ // range again on the lock file. The lock attempt would pass if
+ // done from the same process for the granted range. The lock
+ // attempt must fail to pass our check.
+
+ EXPECT_EQ(0, pipe(fds));
+
+ if (fork() == 0) {
+ unsigned char locked = 0;
+ // Child writes to pipe
+ close(fds[0]);
+
+ InterprocessSyncFile sync2("test");
+ InterprocessSyncLocker locker2(sync2);
+
+ if (!locker2.tryLock()) {
+ EXPECT_FALSE(locker2.isLocked());
+ locked = 1;
+ } else {
+ EXPECT_TRUE(locker2.isLocked());
+ }
+
+ ssize_t bytes_written = write(fds[1], &locked, sizeof(locked));
+ EXPECT_EQ(sizeof(locked), bytes_written);
+
+ close(fds[1]);
+ exit(0);
+ } else {
+ // Parent reads from pipe
+ close(fds[1]);
+
+ const unsigned char locked = parentReadState(fds[0]);
+
+ close(fds[0]);
+
+ EXPECT_EQ(1, locked);
+ }
+ }
+
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_FALSE(locker.isLocked());
+
+ EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test_lockfile"));
+}
+
+TEST(InterprocessSyncFileTest, TestMultipleFilesDirect) {
+ InterprocessSyncFile sync("test1");
+ InterprocessSyncLocker locker(sync);
+
+ EXPECT_TRUE(locker.lock());
+
+ InterprocessSyncFile sync2("test2");
+ InterprocessSyncLocker locker2(sync2);
+ EXPECT_TRUE(locker2.lock());
+ EXPECT_TRUE(locker2.unlock());
+
+ EXPECT_TRUE(locker.unlock());
+
+ EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test1_lockfile"));
+ EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test2_lockfile"));
+}
+
+TEST(InterprocessSyncFileTest, TestMultipleFilesForked) {
+ InterprocessSyncFile sync("test1");
+ InterprocessSyncLocker locker(sync);
+
+ EXPECT_TRUE(locker.lock());
+
+ if (!isc::util::unittests::runningOnValgrind()) {
+
+ int fds[2];
+
+ EXPECT_EQ(0, pipe(fds));
+
+ if (fork() == 0) {
+ unsigned char locked = 0xff;
+ // Child writes to pipe
+ close(fds[0]);
+
+ InterprocessSyncFile sync2("test2");
+ InterprocessSyncLocker locker2(sync2);
+
+ if (locker2.tryLock()) {
+ locked = 0;
+ }
+
+ ssize_t bytes_written = write(fds[1], &locked, sizeof(locked));
+ EXPECT_EQ(sizeof(locked), bytes_written);
+
+ close(fds[1]);
+ exit(0);
+ } else {
+ // Parent reads from pipe
+ close(fds[1]);
+
+ const unsigned char locked = parentReadState(fds[0]);
+
+ close(fds[0]);
+
+ EXPECT_EQ(0, locked);
+ }
+
+ EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test2_lockfile"));
+ }
+
+ EXPECT_TRUE(locker.unlock());
+
+ EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test1_lockfile"));
+}
+
+} // unnamed namespace
diff --git a/src/lib/log/interprocess/tests/interprocess_sync_null_unittest.cc b/src/lib/log/interprocess/tests/interprocess_sync_null_unittest.cc
new file mode 100644
index 0000000..cc9795c
--- /dev/null
+++ b/src/lib/log/interprocess/tests/interprocess_sync_null_unittest.cc
@@ -0,0 +1,75 @@
+// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <log/interprocess/interprocess_sync_null.h>
+#include <gtest/gtest.h>
+
+using namespace std;
+using namespace isc::log::interprocess;
+
+namespace {
+
+TEST(InterprocessSyncNullTest, TestNull) {
+ InterprocessSyncNull sync("test1");
+ InterprocessSyncLocker locker(sync);
+
+ // Check if the is_locked_ flag is set correctly during lock().
+ EXPECT_FALSE(locker.isLocked());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.isLocked());
+
+ // lock() must always return true (this is called 4 times, just an
+ // arbitrary number)
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.lock());
+
+ // Check if the is_locked_ flag is set correctly during unlock().
+ EXPECT_TRUE(locker.isLocked());
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_FALSE(locker.isLocked());
+
+ // unlock() must always return true (this is called 4 times, just an
+ // arbitrary number)
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.unlock());
+
+ // Check if the is_locked_ flag is set correctly during tryLock().
+ EXPECT_FALSE(locker.isLocked());
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.isLocked());
+
+ // tryLock() must always return true (this is called 4 times, just an
+ // arbitrary number)
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.tryLock());
+
+ // Random order (should all return true)
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.lock());
+ EXPECT_TRUE(locker.tryLock());
+ EXPECT_TRUE(locker.unlock());
+ EXPECT_TRUE(locker.unlock());
+}
+
+}
diff --git a/src/lib/log/interprocess/tests/run_unittests.cc b/src/lib/log/interprocess/tests/run_unittests.cc
new file mode 100644
index 0000000..03fb322
--- /dev/null
+++ b/src/lib/log/interprocess/tests/run_unittests.cc
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <gtest/gtest.h>
+#include <util/unittests/run_all.h>
+#include <stdlib.h>
+
+int
+main(int argc, char* argv[]) {
+ ::testing::InitGoogleTest(&argc, argv);
+
+ setenv("B10_LOCKFILE_DIR_FROM_BUILD", TEST_DATA_TOPBUILDDIR, 1);
+ return (isc::util::unittests::run_all());
+}
diff --git a/src/lib/log/logger.cc b/src/lib/log/logger.cc
index fef5627..f7d6799 100644
--- a/src/lib/log/logger.cc
+++ b/src/lib/log/logger.cc
@@ -43,6 +43,11 @@ void Logger::initLoggerImpl() {
Logger::~Logger() {
delete loggerptr_;
+
+ // The next statement is required for the BIND 10 hooks framework, where
+ // a statically-linked BIND 10 loads and unloads multiple libraries. See
+ // the hooks documentation for more details.
+ loggerptr_ = 0;
}
// Get Name of Logger
@@ -182,7 +187,7 @@ Logger::fatal(const isc::log::MessageID& ident) {
// Replace the interprocess synchronization object
void
-Logger::setInterprocessSync(isc::util::InterprocessSync* sync) {
+Logger::setInterprocessSync(isc::log::interprocess::InterprocessSync* sync) {
getLoggerPtr()->setInterprocessSync(sync);
}
diff --git a/src/lib/log/logger.h b/src/lib/log/logger.h
index e3ba163..de2b304 100644
--- a/src/lib/log/logger.h
+++ b/src/lib/log/logger.h
@@ -25,10 +25,13 @@
#include <log/message_types.h>
#include <log/log_formatter.h>
-#include <util/interprocess_sync.h>
-
namespace isc {
namespace log {
+namespace interprocess {
+// Forward declaration to hide implementation details from normal
+// applications.
+class InterprocessSync;
+}
/// \page LoggingApi Logging API
/// \section LoggingApiOverview Overview
@@ -254,11 +257,16 @@ public:
/// If this method is called with NULL as the argument, it throws a
/// BadInterprocessSync exception.
///
+ /// \note This method is intended to be used only within this log library
+ /// and its tests. Normal application shouldn't use it (in fact,
+ /// normal application shouldn't even be able to instantiate
+ /// InterprocessSync objects).
+ ///
/// \param sync The logger uses this synchronization object for
/// synchronizing output of log messages. It should be deletable and
/// the ownership is transferred to the logger. If NULL is passed,
/// a BadInterprocessSync exception is thrown.
- void setInterprocessSync(isc::util::InterprocessSync* sync);
+ void setInterprocessSync(isc::log::interprocess::InterprocessSync* sync);
/// \brief Equality
///
diff --git a/src/lib/log/logger_impl.cc b/src/lib/log/logger_impl.cc
index 9363738..96f021d 100644
--- a/src/lib/log/logger_impl.cc
+++ b/src/lib/log/logger_impl.cc
@@ -32,16 +32,15 @@
#include <log/logger_manager.h>
#include <log/message_dictionary.h>
#include <log/message_types.h>
+#include <log/interprocess/interprocess_sync_file.h>
#include <util/strutil.h>
-#include <util/interprocess_sync_file.h>
// Note: as log4cplus and the BIND 10 logger have many concepts in common, and
// thus many similar names, to disambiguate types we don't "use" the log4cplus
// namespace: instead, all log4cplus types are explicitly qualified.
using namespace std;
-using namespace isc::util;
namespace isc {
namespace log {
@@ -54,7 +53,7 @@ namespace log {
LoggerImpl::LoggerImpl(const string& name) :
name_(expandLoggerName(name)),
logger_(log4cplus::Logger::getInstance(name_)),
- sync_(new InterprocessSyncFile("logger"))
+ sync_(new interprocess::InterprocessSyncFile("logger"))
{
}
@@ -112,7 +111,8 @@ LoggerImpl::lookupMessage(const MessageID& ident) {
// Replace the interprocess synchronization object
void
-LoggerImpl::setInterprocessSync(isc::util::InterprocessSync* sync) {
+LoggerImpl::setInterprocessSync(isc::log::interprocess::InterprocessSync* sync)
+{
if (sync == NULL) {
isc_throw(BadInterprocessSync,
"NULL was passed to setInterprocessSync()");
@@ -130,7 +130,7 @@ LoggerImpl::outputRaw(const Severity& severity, const string& message) {
// Use an interprocess sync locker for mutual exclusion from other
// processes to avoid log messages getting interspersed.
- InterprocessSyncLocker locker(*sync_);
+ interprocess::InterprocessSyncLocker locker(*sync_);
if (!locker.lock()) {
LOG4CPLUS_ERROR(logger_, "Unable to lock logger lockfile");
diff --git a/src/lib/log/logger_impl.h b/src/lib/log/logger_impl.h
index 7280d5c..60f4c58 100644
--- a/src/lib/log/logger_impl.h
+++ b/src/lib/log/logger_impl.h
@@ -23,6 +23,7 @@
#include <string>
#include <map>
#include <utility>
+#include <boost/noncopyable.hpp>
// log4cplus logger header file
@@ -31,8 +32,7 @@
// BIND-10 logger files
#include <log/logger_level_impl.h>
#include <log/message_types.h>
-
-#include <util/interprocess_sync.h>
+#include <log/interprocess/interprocess_sync.h>
namespace isc {
namespace log {
@@ -62,7 +62,7 @@ namespace log {
/// b) The idea of debug levels is implemented. See logger_level.h and
/// logger_level_impl.h for more details on this.
-class LoggerImpl {
+class LoggerImpl : public boost::noncopyable {
public:
/// \brief Constructor
@@ -178,7 +178,7 @@ public:
/// synchronizing output of log messages. It should be deletable and
/// the ownership is transferred to the logger implementation.
/// If NULL is passed, a BadInterprocessSync exception is thrown.
- void setInterprocessSync(isc::util::InterprocessSync* sync);
+ void setInterprocessSync(isc::log::interprocess::InterprocessSync* sync);
/// \brief Equality
///
@@ -193,7 +193,7 @@ public:
private:
std::string name_; ///< Full name of this logger
log4cplus::Logger logger_; ///< Underlying log4cplus logger
- isc::util::InterprocessSync* sync_;
+ isc::log::interprocess::InterprocessSync* sync_;
};
} // namespace log
diff --git a/src/lib/log/logger_manager.cc b/src/lib/log/logger_manager.cc
index 0857441..047c7dc 100644
--- a/src/lib/log/logger_manager.cc
+++ b/src/lib/log/logger_manager.cc
@@ -28,7 +28,7 @@
#include <log/message_initializer.h>
#include <log/message_reader.h>
#include <log/message_types.h>
-#include "util/interprocess_sync_null.h"
+#include <log/interprocess/interprocess_sync_null.h>
using namespace std;
@@ -157,7 +157,8 @@ LoggerManager::readLocalMessageFile(const char* file) {
// be used by standalone programs which may not have write access to
// the local state directory (to create lock files). So we switch to
// using a null interprocess sync object here.
- logger.setInterprocessSync(new isc::util::InterprocessSyncNull("logger"));
+ logger.setInterprocessSync(
+ new isc::log::interprocess::InterprocessSyncNull("logger"));
try {
diff --git a/src/lib/log/logger_manager.h b/src/lib/log/logger_manager.h
index b09383e..0c49757 100644
--- a/src/lib/log/logger_manager.h
+++ b/src/lib/log/logger_manager.h
@@ -19,6 +19,8 @@
#include <util/threads/sync.h>
#include <log/logger_specification.h>
+#include <boost/noncopyable.hpp>
+
// Generated if, when updating the logging specification, an unknown
// destination is encountered.
class UnknownLoggingDestination : public isc::Exception {
@@ -41,7 +43,7 @@ class LoggerManagerImpl;
/// To isolate the underlying implementation from basic processing, the
/// LoggerManager is implemented using the "pimpl" idiom.
-class LoggerManager {
+class LoggerManager : public boost::noncopyable {
public:
/// \brief Constructor
LoggerManager();
diff --git a/src/lib/log/logger_manager_impl.cc b/src/lib/log/logger_manager_impl.cc
index 711eae9..9fb4d57 100644
--- a/src/lib/log/logger_manager_impl.cc
+++ b/src/lib/log/logger_manager_impl.cc
@@ -35,7 +35,10 @@
#include <log/logger_specification.h>
#include <log/buffer_appender_impl.h>
+#include <boost/lexical_cast.hpp>
+
using namespace std;
+using boost::lexical_cast;
namespace isc {
namespace log {
@@ -121,21 +124,33 @@ LoggerManagerImpl::createConsoleAppender(log4cplus::Logger& logger,
// File appender. Depending on whether a maximum size is given, either
// a standard file appender or a rolling file appender will be created.
+// In the case of the latter, we set "UseLockFile" to true so that
+// log4cplus internally avoids race in rolling over the files by multiple
+// processes. This feature isn't supported in log4cplus 1.0.x, but setting
+// the property unconditionally is okay as unknown properties are simply
+// ignored.
void
LoggerManagerImpl::createFileAppender(log4cplus::Logger& logger,
- const OutputOption& opt)
+ const OutputOption& opt)
{
// Append to existing file
- std::ios::openmode mode = std::ios::app;
+ const std::ios::openmode mode = std::ios::app;
log4cplus::SharedAppenderPtr fileapp;
if (opt.maxsize == 0) {
fileapp = log4cplus::SharedAppenderPtr(new log4cplus::FileAppender(
opt.filename, mode, opt.flush));
} else {
+ log4cplus::helpers::Properties properties;
+ properties.setProperty("File", opt.filename);
+ properties.setProperty("MaxFileSize",
+ lexical_cast<string>(opt.maxsize));
+ properties.setProperty("MaxBackupIndex",
+ lexical_cast<string>(opt.maxver));
+ properties.setProperty("ImmediateFlush", opt.flush ? "true" : "false");
+ properties.setProperty("UseLockFile", "true");
fileapp = log4cplus::SharedAppenderPtr(
- new log4cplus::RollingFileAppender(opt.filename, opt.maxsize,
- opt.maxver, opt.flush));
+ new log4cplus::RollingFileAppender(properties));
}
// use the same console layout for the files.
diff --git a/src/lib/log/message_exception.h b/src/lib/log/message_exception.h
index 5f1ad12..7133cd8 100644
--- a/src/lib/log/message_exception.h
+++ b/src/lib/log/message_exception.h
@@ -62,7 +62,7 @@ public:
/// \param lineno Line number on which error occurred (if > 0).
MessageException(const char* file, size_t line, const char* what,
MessageID id, const std::string& arg1, int lineno)
- : isc::Exception(file, line, what), id_(id)
+ : isc::Exception(file, line, what), id_(id), lineno_(lineno)
{
if (lineno > 0) {
args_.push_back(boost::lexical_cast<std::string>(lineno));
@@ -82,7 +82,7 @@ public:
MessageException(const char* file, size_t line, const char *what,
MessageID id, const std::string& arg1,
const std::string& arg2, int lineno)
- : isc::Exception(file, line, what), id_(id)
+ : isc::Exception(file, line, what), id_(id), lineno_(lineno)
{
if (lineno > 0) {
args_.push_back(boost::lexical_cast<std::string>(lineno));
diff --git a/src/lib/log/message_reader.cc b/src/lib/log/message_reader.cc
index b5a4d35..d54c41d 100644
--- a/src/lib/log/message_reader.cc
+++ b/src/lib/log/message_reader.cc
@@ -127,7 +127,7 @@ void
MessageReader::parsePrefix(const vector<string>& tokens) {
// Should not get here unless there is something in the tokens array.
- assert(tokens.size() > 0);
+ assert(!tokens.empty());
// Process $PREFIX. With no arguments, the prefix is set to the empty
// string. One argument sets the prefix to the to its value and more than
diff --git a/src/lib/log/tests/.gitignore b/src/lib/log/tests/.gitignore
index e7e12d6..05d0a03 100644
--- a/src/lib/log/tests/.gitignore
+++ b/src/lib/log/tests/.gitignore
@@ -15,3 +15,4 @@
/run_unittests
/severity_test.sh
/tempdir.h
+/s-messages
diff --git a/src/lib/log/tests/Makefile.am b/src/lib/log/tests/Makefile.am
index 306d5f9..a26b348 100644
--- a/src/lib/log/tests/Makefile.am
+++ b/src/lib/log/tests/Makefile.am
@@ -10,14 +10,17 @@ if USE_STATIC_LINK
AM_LDFLAGS += -static
endif
-CLEANFILES = *.gcno *.gcda
+CLEANFILES = *.gcno *.gcda *.lock
EXTRA_DIST = log_test_messages.mes
BUILT_SOURCES = log_test_messages.h log_test_messages.cc
-log_test_messages.h log_test_messages.cc: log_test_messages.mes
+log_test_messages.h log_test_messages.cc: s-messages
+
+s-messages: log_test_messages.mes
$(AM_V_GEN) $(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/log/tests/log_test_messages.mes
+ touch $@
-CLEANFILES += log_test_messages.h log_test_messages.cc
+CLEANFILES += log_test_messages.h log_test_messages.cc s-messages
noinst_PROGRAMS = logger_example
logger_example_SOURCES = logger_example.cc
@@ -25,7 +28,6 @@ logger_example_CPPFLAGS = $(AM_CPPFLAGS)
logger_example_LDFLAGS = $(AM_LDFLAGS)
logger_example_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
logger_example_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
-logger_example_LDADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
logger_example_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
logger_example_LDADD += $(AM_LDADD) $(LOG4CPLUS_LIBS)
@@ -35,7 +37,6 @@ init_logger_test_CPPFLAGS = $(AM_CPPFLAGS)
init_logger_test_LDFLAGS = $(AM_LDFLAGS)
init_logger_test_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
init_logger_test_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
-init_logger_test_LDADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
init_logger_test_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
init_logger_test_LDADD += $(AM_LDADD) $(LOG4CPLUS_LIBS)
@@ -45,10 +46,11 @@ buffer_logger_test_CPPFLAGS = $(AM_CPPFLAGS)
buffer_logger_test_LDFLAGS = $(AM_LDFLAGS)
buffer_logger_test_LDADD = $(top_builddir)/src/lib/log/libb10-log.la
buffer_logger_test_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
-buffer_logger_test_LDADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
buffer_logger_test_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
buffer_logger_test_LDADD += $(AM_LDADD) $(LOG4CPLUS_LIBS)
+# This test directly uses libb10-threads, and on some systems it seems to
+# require explicit LDADD (even if libb10-log has indirect dependencies)
noinst_PROGRAMS += logger_lock_test
logger_lock_test_SOURCES = logger_lock_test.cc
nodist_logger_lock_test_SOURCES = log_test_messages.cc log_test_messages.h
@@ -75,7 +77,6 @@ AM_CPPFLAGS += $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES)
AM_LDFLAGS += $(GTEST_LDFLAGS)
AM_LDADD += $(top_builddir)/src/lib/util/libb10-util.la
-AM_LDADD += $(top_builddir)/src/lib/util/threads/libb10-threads.la
AM_LDADD += $(top_builddir)/src/lib/log/libb10-log.la
AM_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
AM_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
diff --git a/src/lib/log/tests/buffer_logger_test.cc b/src/lib/log/tests/buffer_logger_test.cc
index 8d1b3cf..d703e04 100644
--- a/src/lib/log/tests/buffer_logger_test.cc
+++ b/src/lib/log/tests/buffer_logger_test.cc
@@ -16,7 +16,7 @@
#include <log/logger_support.h>
#include <log/logger_manager.h>
#include <log/log_messages.h>
-#include <util/interprocess_sync_null.h>
+#include <log/interprocess/interprocess_sync_null.h>
using namespace isc::log;
@@ -58,7 +58,8 @@ main(int argc, char** argv) {
initLogger("buffertest", isc::log::INFO, 0, NULL, true);
Logger logger("log");
// No need for file interprocess locking in this test
- logger.setInterprocessSync(new isc::util::InterprocessSyncNull("logger"));
+ logger.setInterprocessSync(
+ new isc::log::interprocess::InterprocessSyncNull("logger"));
LOG_INFO(logger, LOG_BAD_SEVERITY).arg("info");
LOG_DEBUG(logger, 50, LOG_BAD_DESTINATION).arg("debug-50");
LOG_INFO(logger, LOG_BAD_SEVERITY).arg("info");
diff --git a/src/lib/log/tests/logger_example.cc b/src/lib/log/tests/logger_example.cc
index 4b20429..daadb7c 100644
--- a/src/lib/log/tests/logger_example.cc
+++ b/src/lib/log/tests/logger_example.cc
@@ -41,11 +41,11 @@
// Include a set of message definitions.
#include <log/log_messages.h>
-#include "util/interprocess_sync_null.h"
+#include <log/interprocess/interprocess_sync_null.h>
using namespace isc::log;
using namespace std;
-
+using isc::log::interprocess::InterprocessSyncNull;
// Print usage information
@@ -286,11 +286,11 @@ int main(int argc, char** argv) {
// have write access to a local state directory to create
// lockfiles).
isc::log::Logger logger_ex(ROOT_NAME);
- logger_ex.setInterprocessSync(new isc::util::InterprocessSyncNull("logger"));
+ logger_ex.setInterprocessSync(new InterprocessSyncNull("logger"));
isc::log::Logger logger_alpha("alpha");
- logger_alpha.setInterprocessSync(new isc::util::InterprocessSyncNull("logger"));
+ logger_alpha.setInterprocessSync(new InterprocessSyncNull("logger"));
isc::log::Logger logger_beta("beta");
- logger_beta.setInterprocessSync(new isc::util::InterprocessSyncNull("logger"));
+ logger_beta.setInterprocessSync(new InterprocessSyncNull("logger"));
LOG_FATAL(logger_ex, LOG_WRITE_ERROR).arg("test1").arg("42");
LOG_ERROR(logger_ex, LOG_READING_LOCAL_FILE).arg("dummy/file");
diff --git a/src/lib/log/tests/logger_lock_test.cc b/src/lib/log/tests/logger_lock_test.cc
index 7fed5c7..9b9ee17 100644
--- a/src/lib/log/tests/logger_lock_test.cc
+++ b/src/lib/log/tests/logger_lock_test.cc
@@ -16,15 +16,17 @@
#include <log/logger_support.h>
#include <log/logger_manager.h>
#include <log/log_messages.h>
-#include "util/interprocess_sync.h"
+#include <log/interprocess/interprocess_sync.h>
#include "log_test_messages.h"
+
+#include <util/threads/sync.h>
#include <iostream>
using namespace std;
using namespace isc::log;
using isc::util::thread::Mutex;
-class MockLoggingSync : public isc::util::InterprocessSync {
+class MockLoggingSync : public isc::log::interprocess::InterprocessSync {
public:
/// \brief Constructor
MockLoggingSync(const std::string& component_name) :
diff --git a/src/lib/log/tests/logger_manager_unittest.cc b/src/lib/log/tests/logger_manager_unittest.cc
index 584d0f5..90f8904 100644
--- a/src/lib/log/tests/logger_manager_unittest.cc
+++ b/src/lib/log/tests/logger_manager_unittest.cc
@@ -36,6 +36,9 @@
#include "tempdir.h"
+#include <sys/types.h>
+#include <regex.h>
+
using namespace isc;
using namespace isc::log;
using namespace std;
@@ -77,7 +80,11 @@ public:
// Destructor, remove the file. This is only a test, so ignore failures
~SpecificationForFileLogger() {
if (! name_.empty()) {
- (void) unlink(name_.c_str());
+ static_cast<void>(unlink(name_.c_str()));
+
+ // Depending on the log4cplus version, a lock file may also be
+ // created.
+ static_cast<void>(unlink((name_ + ".lock").c_str()));
}
}
@@ -319,3 +326,81 @@ TEST_F(LoggerManagerTest, FileSizeRollover) {
(void) unlink(prev_name[i].c_str());
}
}
+
+namespace { // begin unnamed namespace
+
+// When we begin to use C++11, we could replace use of POSIX API with
+// <regex>.
+
+class RegexHolder {
+public:
+ RegexHolder(const char* expr, const int flags = REG_EXTENDED) {
+ const int rc = regcomp(®ex_, expr, flags);
+ if (rc) {
+ regfree(®ex_);
+ throw;
+ }
+ }
+
+ ~RegexHolder() {
+ regfree(®ex_);
+ }
+
+ regex_t* operator*() {
+ return (®ex_);
+ }
+
+private:
+ regex_t regex_;
+};
+
+} // end of unnamed namespace
+
+// Check that the logger correctly outputs the full formatted layout
+// pattern.
+TEST_F(LoggerManagerTest, checkLayoutPattern) {
+ // Create a specification for the file logger and use the manager to
+ // connect the "filelogger" logger to it.
+ SpecificationForFileLogger file_spec;
+
+ // For the first test, we want to check that the file is created
+ // if it does not already exist. So delete the temporary file before
+ // logging the first message.
+ unlink(file_spec.getFileName().c_str());
+
+ // Set up the file appenders.
+ LoggerManager manager;
+ manager.process(file_spec.getSpecification());
+
+ // Try logging to the file. Local scope is set to ensure that the logger
+ // is destroyed before we reset the global logging.
+ {
+ Logger logger(file_spec.getLoggerName().c_str());
+ LOG_FATAL(logger, LOG_DUPLICATE_MESSAGE_ID).arg("test");
+ }
+
+ LoggerManager::reset();
+
+ // Access the file for input
+ const std::string& filename = file_spec.getFileName();
+ ifstream infile(filename.c_str());
+ if (! infile.good()) {
+ FAIL() << "Unable to open the logging file " << filename;
+ }
+
+ std::string line;
+ std::getline(infile, line);
+
+ RegexHolder regex(// %D{%Y-%m-%d %H:%M:%S.%q}
+ "^[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}[[:space:]]"
+ "[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\\.[[:digit:]]+[[:space:]]"
+ // %-5p
+ "[[:alpha:]]{1,5}[[:space:]]"
+ // [%c/%i]
+ "\\[[[:alnum:]\\-\\.]+/[[:digit:]]+\\][[:space:]]"
+ );
+
+ const int re = regexec(*regex, line.c_str(), 0, NULL, 0);
+ ASSERT_EQ(0, re)
+ << "Logged message does not match expected layout pattern";
+}
diff --git a/src/lib/log/tests/logger_unittest.cc b/src/lib/log/tests/logger_unittest.cc
index 7b62d79..77a9d2a 100644
--- a/src/lib/log/tests/logger_unittest.cc
+++ b/src/lib/log/tests/logger_unittest.cc
@@ -20,10 +20,9 @@
#include <log/logger_manager.h>
#include <log/logger_name.h>
#include <log/log_messages.h>
+#include <log/interprocess/interprocess_sync_file.h>
#include "log/tests/log_test_messages.h"
-#include <util/interprocess_sync_file.h>
-
#include <iostream>
#include <string>
@@ -391,7 +390,7 @@ TEST_F(LoggerTest, setInterprocessSync) {
EXPECT_THROW(logger.setInterprocessSync(NULL), BadInterprocessSync);
}
-class MockSync : public isc::util::InterprocessSync {
+class MockSync : public isc::log::interprocess::InterprocessSync {
public:
/// \brief Constructor
MockSync(const std::string& component_name) :
diff --git a/src/lib/nsas/.gitignore b/src/lib/nsas/.gitignore
index 109ef04..691e010 100644
--- a/src/lib/nsas/.gitignore
+++ b/src/lib/nsas/.gitignore
@@ -1,2 +1,3 @@
/nsas_messages.cc
/nsas_messages.h
+/s-messages
diff --git a/src/lib/nsas/Makefile.am b/src/lib/nsas/Makefile.am
index eb8c0c3..d38cf1a 100644
--- a/src/lib/nsas/Makefile.am
+++ b/src/lib/nsas/Makefile.am
@@ -22,8 +22,11 @@ AM_CXXFLAGS += -Wno-unused-parameter
endif
# Define rule to build logging source files from message file
-nsas_messages.h nsas_messages.cc: nsas_messages.mes
+nsas_messages.h nsas_messages.cc: s-messages
+
+s-messages: nsas_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/nsas/nsas_messages.mes
+ touch $@
# What is being built.
lib_LTLIBRARIES = libb10-nsas.la
@@ -38,7 +41,6 @@ BUILT_SOURCES = nsas_messages.h nsas_messages.cc
# Library sources. The generated files will not be in the distribution.
libb10_nsas_la_SOURCES = address_entry.h address_entry.cc
-libb10_nsas_la_SOURCES += asiolink.h
libb10_nsas_la_SOURCES += hash.cc hash.h
libb10_nsas_la_SOURCES += hash_deleter.h
libb10_nsas_la_SOURCES += hash_key.cc hash_key.h
@@ -60,4 +62,4 @@ nodist_libb10_nsas_la_SOURCES = nsas_messages.h nsas_messages.cc
EXTRA_DIST = nsas_messages.mes
# Make sure that the generated files are got rid of in a clean operation
-CLEANFILES = *.gcno *.gcda nsas_messages.h nsas_messages.cc
+CLEANFILES = *.gcno *.gcda nsas_messages.h nsas_messages.cc s-messages
diff --git a/src/lib/nsas/README b/src/lib/nsas/README
index d0598ca..144bdde 100644
--- a/src/lib/nsas/README
+++ b/src/lib/nsas/README
@@ -1,7 +1,2 @@
For an overview of the Nameserver Address Store, see the requirements and design
documents at http://bind10.isc.org/wiki/Resolver.
-
-At the time of writing (19 October 2010), the file asiolink.h is present in this
-directory only for the purposes of development. When the resolver's
-asynchronous I/O code has been finished, this will be removed and the NSAS will
-use the "real" code.
diff --git a/src/lib/nsas/address_request_callback.h b/src/lib/nsas/address_request_callback.h
index e43dfe2..5d86d58 100644
--- a/src/lib/nsas/address_request_callback.h
+++ b/src/lib/nsas/address_request_callback.h
@@ -15,7 +15,6 @@
#ifndef ADDRESS_REQUEST_CALLBACK_H
#define ADDRESS_REQUEST_CALLBACK_H
-#include "asiolink.h"
#include "nameserver_address.h"
namespace isc {
diff --git a/src/lib/nsas/asiolink.h b/src/lib/nsas/asiolink.h
deleted file mode 100644
index b236a0e..0000000
--- a/src/lib/nsas/asiolink.h
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef ASIOLINK_H
-#define ASIOLINK_H
-
-#include <string>
-#include <sys/socket.h>
-
-#endif // ASIOLINK_H
diff --git a/src/lib/nsas/nameserver_address.h b/src/lib/nsas/nameserver_address.h
index 5f5c7c9..33a1169 100644
--- a/src/lib/nsas/nameserver_address.h
+++ b/src/lib/nsas/nameserver_address.h
@@ -19,7 +19,6 @@
#include <exceptions/exceptions.h>
-#include "asiolink.h"
#include "address_entry.h"
#include "nsas_types.h"
diff --git a/src/lib/nsas/nameserver_entry.h b/src/lib/nsas/nameserver_entry.h
index 3ffdf10..5ff72ff 100644
--- a/src/lib/nsas/nameserver_entry.h
+++ b/src/lib/nsas/nameserver_entry.h
@@ -28,7 +28,6 @@
#include <util/lru_list.h>
#include "address_entry.h"
-#include "asiolink.h"
#include "nsas_types.h"
#include "hash_key.h"
#include "fetchable.h"
diff --git a/src/lib/nsas/tests/nameserver_address_store_unittest.cc b/src/lib/nsas/tests/nameserver_address_store_unittest.cc
index ceb5775..a606f26 100644
--- a/src/lib/nsas/tests/nameserver_address_store_unittest.cc
+++ b/src/lib/nsas/tests/nameserver_address_store_unittest.cc
@@ -287,7 +287,10 @@ TEST_F(NameserverAddressStoreTest, emptyLookup) {
EXPECT_EQ(2, resolver_->requests.size());
// Ask another question with different zone but the same nameserver
- authority_->setName(Name("example.com."));
+ authority_.reset(new RRset(Name("example.com."), RRClass::IN(), RRType::NS(),
+ RRTTL(128)));
+ authority_->addRdata(ConstRdataPtr
+ (new rdata::generic::NS(Name("ns.example.com."))));
nsas.lookupAndAnswer("example.com.", RRClass::IN(), authority_,
getCallback());
@@ -339,7 +342,10 @@ TEST_F(NameserverAddressStoreTest, unreachableNS) {
EXPECT_NO_THROW(resolver_->asksIPs(Name("ns.example.com."), 0, 1));
// Ask another question with different zone but the same nameserver
- authority_->setName(Name("example.com."));
+ authority_.reset(new RRset(Name("example.com."), RRClass::IN(), RRType::NS(),
+ RRTTL(128)));
+ authority_->addRdata(ConstRdataPtr
+ (new rdata::generic::NS(Name("ns.example.com."))));
nsas.lookupAndAnswer("example.com.", RRClass::IN(), authority_,
getCallback());
@@ -356,7 +362,10 @@ TEST_F(NameserverAddressStoreTest, unreachableNS) {
// When we ask one same and one other zone with the same nameserver,
// it should generate no questions and answer right away
nsas.lookup("example.net.", RRClass::IN(), getCallback());
- authority_->setName(Name("example.org."));
+ authority_.reset(new RRset(Name("example.org."), RRClass::IN(), RRType::NS(),
+ RRTTL(128)));
+ authority_->addRdata(ConstRdataPtr
+ (new rdata::generic::NS(Name("ns.example.com."))));
nsas.lookupAndAnswer("example.org.", RRClass::IN(), authority_,
getCallback());
@@ -427,7 +436,10 @@ TEST_F(NameserverAddressStoreTest, CombinedTest) {
// But when ask for a different zone with the first nameserver, it should
// ask again, as it is evicted already
- authority_->setName(Name("example.com."));
+ authority_.reset(new RRset(Name("example.com."), RRClass::IN(), RRType::NS(),
+ RRTTL(128)));
+ authority_->addRdata(ConstRdataPtr
+ (new rdata::generic::NS(Name("ns.example.com."))));
nsas.lookupAndAnswer("example.com.", RRClass::IN(), authority_,
getCallback());
EXPECT_EQ(request_count + 2, resolver_->requests.size());
diff --git a/src/lib/nsas/tests/nameserver_entry_unittest.cc b/src/lib/nsas/tests/nameserver_entry_unittest.cc
index e0ec0ad..10f0c20 100644
--- a/src/lib/nsas/tests/nameserver_entry_unittest.cc
+++ b/src/lib/nsas/tests/nameserver_entry_unittest.cc
@@ -30,7 +30,6 @@
#include <dns/name.h>
#include <exceptions/exceptions.h>
-#include "../asiolink.h"
#include "../address_entry.h"
#include "../nameserver_entry.h"
#include "../nameserver_address.h"
diff --git a/src/lib/nsas/tests/zone_entry_unittest.cc b/src/lib/nsas/tests/zone_entry_unittest.cc
index 8754906..d685fbb 100644
--- a/src/lib/nsas/tests/zone_entry_unittest.cc
+++ b/src/lib/nsas/tests/zone_entry_unittest.cc
@@ -23,7 +23,6 @@
#include <dns/rrclass.h>
#include <dns/rdataclass.h>
-#include "../asiolink.h"
#include "../zone_entry.h"
#include "../nameserver_entry.h"
#include "../address_request_callback.h"
@@ -474,11 +473,21 @@ TEST_F(ZoneEntryTest, DirectAnswer) {
EXPECT_EQ(Fetchable::NOT_ASKED, zone->getState());
resolver_->addPresetAnswer(Question(Name(EXAMPLE_CO_UK), RRClass::IN(),
RRType::NS()), rr_single_);
+
Name ns_name("ns.example.net");
- rrv4_->setName(ns_name);
+
+ rrv4_.reset(new RRset(ns_name, RRClass::IN(), RRType::A(), RRTTL(1200)));
+ rrv4_->addRdata(ConstRdataPtr(new RdataTest<A>("1.2.3.4")));
+ rrv4_->addRdata(ConstRdataPtr(new RdataTest<A>("5.6.7.8")));
+ rrv4_->addRdata(ConstRdataPtr(new RdataTest<A>("9.10.11.12")));
+
resolver_->addPresetAnswer(Question(ns_name, RRClass::IN(), RRType::A()),
rrv4_);
- rrv6_->setName(ns_name);
+
+ rrv6_.reset(new RRset(ns_name, RRClass::IN(), RRType::AAAA(), RRTTL(900)));
+ rrv6_->addRdata(ConstRdataPtr(new RdataTest<AAAA>("2001::1002")));
+ rrv6_->addRdata(ConstRdataPtr(new RdataTest<AAAA>("dead:beef:feed::")));
+
resolver_->addPresetAnswer(Question(ns_name, RRClass::IN(),
RRType::AAAA()), rrv6_);
// Reset the results
diff --git a/src/lib/nsas/zone_entry.h b/src/lib/nsas/zone_entry.h
index b0c26c3..d53b321 100644
--- a/src/lib/nsas/zone_entry.h
+++ b/src/lib/nsas/zone_entry.h
@@ -30,7 +30,6 @@
#include "hash_key.h"
#include "nsas_entry.h"
-#include "asiolink.h"
#include "fetchable.h"
#include "nsas_types.h"
#include "glue_hints.h"
diff --git a/src/lib/python/bind10_config.py.in b/src/lib/python/bind10_config.py.in
index e56e908..855dd00 100644
--- a/src/lib/python/bind10_config.py.in
+++ b/src/lib/python/bind10_config.py.in
@@ -17,6 +17,31 @@
# variables to python scripts and libraries.
import os
+def get_specfile_location(module_name):
+ """Return the path to the module spec file following common convetion.
+
+ This method generates the path commonly used by most BIND 10
+ modules, determined by a well known prefix and the module name.
+
+ A specific module can override this method if it uses a different
+ path for the spec file.
+
+ """
+ # First check if it's running under an 'in-source' environment,
+ # then try commonly used paths and file names. If found, use it.
+ for ev in ['B10_FROM_SOURCE', 'B10_FROM_BUILD']:
+ if ev in os.environ:
+ specfile = os.environ[ev] + '/src/bin/' + module_name + \
+ '/' + module_name + '.spec'
+ if os.path.exists(specfile):
+ return specfile
+ # Otherwise, just use the installed path, whether or not it really
+ # exists; leave error handling to the caller.
+ specfile_path = '@datadir@/@PACKAGE@'\
+ .replace('${datarootdir}', '@datarootdir@')\
+ .replace('${prefix}', '@prefix@')
+ return specfile_path + '/' + module_name + '.spec'
+
def reload():
# In a function, for testing purposes
global BIND10_MSGQ_SOCKET_FILE
diff --git a/src/lib/python/isc/Makefile.am b/src/lib/python/isc/Makefile.am
index 712843e..9d0a8ce 100644
--- a/src/lib/python/isc/Makefile.am
+++ b/src/lib/python/isc/Makefile.am
@@ -1,5 +1,9 @@
SUBDIRS = datasrc util cc config dns log net notify testutils acl bind10
SUBDIRS += xfrin log_messages server_common ddns sysinfo statistics
+if USE_SHARED_MEMORY
+# The memory manager is useless without shared memory support
+SUBDIRS += memmgr
+endif
python_PYTHON = __init__.py
diff --git a/src/lib/python/isc/config/ccsession.py b/src/lib/python/isc/config/ccsession.py
index e801309..4dd73b7 100644
--- a/src/lib/python/isc/config/ccsession.py
+++ b/src/lib/python/isc/config/ccsession.py
@@ -220,6 +220,9 @@ class ModuleCCSession(ConfigData):
self._remote_module_configs = {}
self._remote_module_callbacks = {}
+ self._notification_callbacks = {}
+ self._last_notif_id = 0
+
if handle_logging_config:
self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS),
default_logconfig_handler)
@@ -294,8 +297,27 @@ class ModuleCCSession(ConfigData):
configuration update. Calls the corresponding handler
functions if present. Responds on the channel if the
handler returns a message."""
- # should we default to an answer? success-by-default? unhandled error?
- if msg is not None and not CC_PAYLOAD_RESULT in msg:
+ if msg is None:
+ return
+ if CC_PAYLOAD_NOTIFICATION in msg:
+ group_s = env[CC_HEADER_GROUP].split('/', 1)
+ # What to do with these bogus inputs? We just ignore them for now.
+ if len(group_s) != 2:
+ return
+ [prefix, group] = group_s
+ if prefix + '/' != CC_GROUP_NOTIFICATION_PREFIX:
+ return
+ # Now, get the callbacks and call one by one
+ callbacks = self._notification_callbacks.get(group, {})
+ event = msg[CC_PAYLOAD_NOTIFICATION][0]
+ params = None
+ if len(msg[CC_PAYLOAD_NOTIFICATION]) > 1:
+ params = msg[CC_PAYLOAD_NOTIFICATION][1]
+ for key in sorted(callbacks.keys()):
+ callbacks[key](event, params)
+ elif not CC_PAYLOAD_RESULT in msg:
+ # should we default to an answer? success-by-default? unhandled
+ # error?
answer = None
try:
module_name = env[CC_HEADER_GROUP]
@@ -539,6 +561,100 @@ class ModuleCCSession(ConfigData):
raise RPCError(code, value)
return value
+ def notify(self, notification_group, event_name, params=None):
+ """
+ Send a notification to subscribed users.
+
+ Send a notification message to all users subscribed to the given
+ notification group.
+
+ This method does not block.
+
+ See docs/design/ipc-high.txt for details about notifications
+ and the format of messages sent.
+
+ Throws:
+ - CCSessionError: for low-level communication errors.
+ Params:
+ - notification_group (string): This parameter (indirectly) signifies
+ what users should receive the notification. Only users that
+ subscribed to notifications on the same group receive it.
+ - event_name (string): The name of the event to notify about (for
+ example `new_group_member`).
+ - params: Other parameters that describe the event. This might be, for
+ example, the ID of the new member and the name of the group. This can
+ be any data that can be sent over the isc.cc.Session, but it is
+ common for it to be dict.
+ Returns: Nothing
+ """
+ notification = [event_name]
+ if params is not None:
+ notification.append(params)
+ self._session.group_sendmsg({CC_PAYLOAD_NOTIFICATION: notification},
+ CC_GROUP_NOTIFICATION_PREFIX +
+ notification_group,
+ instance=CC_INSTANCE_WILDCARD,
+ to=CC_TO_WILDCARD,
+ want_answer=False)
+
+ def subscribe_notification(self, notification_group, callback):
+ """
+ Subscribe to receive notifications in given notification group. When a
+ notification comes to the group, the callback is called with two
+ parameters, the name of the event (the value of `event_name` parameter
+ passed to `notify`) and the parameters of the event (the value
+ of `params` passed to `notify`).
+
+ This is a fast operation (there may be communication with the message
+ queue daemon, but it does not wait for any remote process).
+
+ The callback may get called multiple times (once for each notification).
+ It is possible to subscribe multiple callbacks for the same notification,
+ by multiple calls of this method, and they will be called in the order
+ of registration when the notification comes.
+
+ Throws:
+ - CCSessionError: for low-level communication errors.
+ Params:
+ - notification_group (string): Notification group to subscribe to.
+ Notification with the same value of the same parameter of `notify`
+ will be received.
+ - callback (callable): The callback to be called whenever the
+ notification comes.
+
+ The callback should not raise exceptions, such exceptions are
+ likely to propagate through the loop and terminate program.
+ Returns: Opaque id of the subscription. It can be used to cancel
+ the subscription by unsubscribe_notification.
+ """
+ self._last_notif_id += 1
+ my_id = self._last_notif_id
+ if notification_group in self._notification_callbacks:
+ self._notification_callbacks[notification_group][my_id] = callback
+ else:
+ self._session.group_subscribe(CC_GROUP_NOTIFICATION_PREFIX +
+ notification_group)
+ self._notification_callbacks[notification_group] = \
+ { my_id: callback }
+ return (notification_group, my_id)
+
+ def unsubscribe_notification(self, nid):
+ """
+ Remove previous subscription for notifications. Pass the id returned
+ from subscribe_notification.
+
+ Throws:
+ - CCSessionError: for low-level communication errors.
+ - KeyError: The id does not correspond to valid subscription.
+ """
+ (group, cid) = nid
+ del self._notification_callbacks[group][cid]
+ if not self._notification_callbacks[group]:
+ # Removed the last one
+ self._session.group_unsubscribe(CC_GROUP_NOTIFICATION_PREFIX +
+ group)
+ del self._notification_callbacks[group]
+
class UIModuleCCSession(MultiConfigData):
"""This class is used in a configuration user interface. It contains
specific functions for getting, displaying, and sending
diff --git a/src/lib/python/isc/config/config_data.py b/src/lib/python/isc/config/config_data.py
index 495d20b..e2b1357 100644
--- a/src/lib/python/isc/config/config_data.py
+++ b/src/lib/python/isc/config/config_data.py
@@ -24,6 +24,7 @@ import isc.cc.data
import isc.config.module_spec
import ast
import copy
+import sys
class ConfigDataError(Exception): pass
@@ -62,10 +63,16 @@ def check_type(spec_part, value):
else:
raise isc.cc.data.DataTypeError(str("Incorrect specification part for type checking"))
- if data_type == "integer" and type(value) != int:
- raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
- elif data_type == "real" and type(value) != float:
- raise isc.cc.data.DataTypeError(str(value) + " is not a real")
+ if data_type == "integer":
+ if type(value) != int:
+ raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
+ if value > sys.maxsize:
+ raise isc.cc.data.DataTypeError(str(value) + " is too large an integer")
+ elif data_type == "real":
+ if type(value) != float:
+ raise isc.cc.data.DataTypeError(str(value) + " is not a real")
+ if float(value) > sys.float_info.max:
+ raise isc.cc.data.DataTypeError(str(value) + " is too large a float")
elif data_type == "boolean" and type(value) != bool:
raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
elif data_type == "string" and type(value) != str:
diff --git a/src/lib/python/isc/config/tests/ccsession_test.py b/src/lib/python/isc/config/tests/ccsession_test.py
index 2a2d790..9c33ef6 100644
--- a/src/lib/python/isc/config/tests/ccsession_test.py
+++ b/src/lib/python/isc/config/tests/ccsession_test.py
@@ -350,6 +350,103 @@ class TestModuleCCSession(unittest.TestCase):
self.assertRaises(RPCRecipientMissing, self.rpc_check,
{"result": [-1, "Error"]})
+ def test_notify(self):
+ """
+ Test the sent notification has the right format.
+ """
+ fake_session = FakeModuleCCSession()
+ mccs = self.create_session("spec1.spec", None, None, fake_session)
+ mccs.notify("group", "event", {"param": True})
+ self.assertEqual([
+ ["notifications/group", "*", {"notification": ["event", {
+ "param": True
+ }]}, False]], fake_session.message_queue)
+
+ def test_notify_no_params(self):
+ """
+ Test the sent notification has the right format, this time
+ without passing parameters.
+ """
+ fake_session = FakeModuleCCSession()
+ mccs = self.create_session("spec1.spec", None, None, fake_session)
+ mccs.notify("group", "event")
+ self.assertEqual([
+ ["notifications/group", "*", {"notification": ["event"]},
+ False]
+ ],
+ fake_session.message_queue)
+
+ def test_notify_receive(self):
+ """
+ Test we can subscribe to notifications, receive them, unsubscribe, etc.
+ """
+ fake_session = FakeModuleCCSession()
+ mccs = self.create_session("spec1.spec", None, None, fake_session)
+ fake_session.group_sendmsg({"notification": ["event", {
+ "param": True
+ }]}, "notifications/group")
+ # Not subscribed to notifications -> not subscribed to
+ # 'notifications/group' -> message not eaten yet
+ mccs.check_command()
+ self.assertEqual(fake_session.message_queue, [['notifications/group',
+ None, {'notification': ['event', {'param': True}]},
+ False]])
+ # Place to log called notifications
+ notifications = []
+ def notified(tag, event, params):
+ notifications.append((tag, event, params))
+ # Subscribe to the notifications. Twice.
+ id1 = mccs.subscribe_notification('group',
+ lambda event, params:
+ notified("first", event, params))
+ id2 = mccs.subscribe_notification('group',
+ lambda event, params:
+ notified("second", event,
+ params))
+ # Now the message gets eaten because we are subscribed, and both
+ # callbacks are called.
+ mccs.check_command()
+ self.assertEqual(fake_session.message_queue, [])
+ self.assertEqual(notifications, [
+ ("first", "event", {'param': True}),
+ ("second", "event", {'param': True})
+ ])
+ del notifications[:]
+ # If a notification for different group comes, it is left untouched.
+ fake_session.group_sendmsg({"notification": ["event", {
+ "param": True
+ }]}, "notifications/other")
+ mccs.check_command()
+ self.assertEqual(notifications, [])
+ self.assertEqual(fake_session.message_queue, [['notifications/other',
+ None, {'notification': ['event', {'param': True}]},
+ False]])
+ del fake_session.message_queue[:]
+ # Unsubscribe one of the notifications and see that only the other
+ # is triggered.
+ mccs.unsubscribe_notification(id2)
+ fake_session.group_sendmsg({"notification": ["event", {
+ "param": True
+ }]}, "notifications/group")
+ mccs.check_command()
+ self.assertEqual(fake_session.message_queue, [])
+ self.assertEqual(notifications, [
+ ("first", "event", {'param': True})
+ ])
+ del notifications[:]
+ # If we try to unsubscribe again, it complains.
+ self.assertRaises(KeyError, mccs.unsubscribe_notification, id2)
+ # Unsubscribe the other one too. From now on, it doesn't eat the
+ # messages again.
+ mccs.unsubscribe_notification(id1)
+ fake_session.group_sendmsg({"notification": ["event", {
+ "param": True
+ }]}, "notifications/group")
+ mccs.check_command()
+ self.assertEqual(fake_session.message_queue, [['notifications/group',
+ None, {'notification': ['event', {'param': True}]},
+ False]])
+
def my_config_handler_ok(self, new_config):
return isc.config.ccsession.create_answer(0)
diff --git a/src/lib/python/isc/config/tests/config_data_test.py b/src/lib/python/isc/config/tests/config_data_test.py
index ddeabb6..83f22c9 100644
--- a/src/lib/python/isc/config/tests/config_data_test.py
+++ b/src/lib/python/isc/config/tests/config_data_test.py
@@ -47,6 +47,7 @@ class TestConfigData(unittest.TestCase):
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+ self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 10000000000000000000000)
spec_part = find_spec_part(config_spec, "value2")
check_type(spec_part, 1.1)
@@ -55,6 +56,7 @@ class TestConfigData(unittest.TestCase):
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+ self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 2.0000000e+308)
spec_part = find_spec_part(config_spec, "value3")
check_type(spec_part, True)
diff --git a/src/lib/python/isc/datasrc/Makefile.am b/src/lib/python/isc/datasrc/Makefile.am
index 28c87ac..4278805 100644
--- a/src/lib/python/isc/datasrc/Makefile.am
+++ b/src/lib/python/isc/datasrc/Makefile.am
@@ -10,6 +10,7 @@ python_PYTHON = __init__.py sqlite3_ds.py
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CPPFLAGS += $(SQLITE_CFLAGS)
+AM_CXXFLAGS = $(B10_CXXFLAGS)
python_LTLIBRARIES = datasrc.la
datasrc_la_SOURCES = datasrc.cc datasrc.h
@@ -21,6 +22,9 @@ datasrc_la_SOURCES += journal_reader_python.cc journal_reader_python.h
datasrc_la_SOURCES += configurableclientlist_python.cc
datasrc_la_SOURCES += configurableclientlist_python.h
datasrc_la_SOURCES += zone_loader_python.cc zone_loader_python.h
+datasrc_la_SOURCES += zonetable_accessor_python.cc zonetable_accessor_python.h
+datasrc_la_SOURCES += zonetable_iterator_python.cc zonetable_iterator_python.h
+datasrc_la_SOURCES += zonewriter_python.cc zonewriter_python.h
datasrc_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
datasrc_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
@@ -32,11 +36,13 @@ datasrc_la_LIBADD += $(top_builddir)/src/lib/dns/python/libb10-pydnspp.la
datasrc_la_LIBADD += $(PYTHON_LIB)
EXTRA_DIST = client_inc.cc
+EXTRA_DIST += configurableclientlist_inc.cc
EXTRA_DIST += finder_inc.cc
EXTRA_DIST += iterator_inc.cc
EXTRA_DIST += updater_inc.cc
EXTRA_DIST += journal_reader_inc.cc
EXTRA_DIST += zone_loader_inc.cc
+EXTRA_DIST += zonewriter_inc.cc
CLEANDIRS = __pycache__
diff --git a/src/lib/python/isc/datasrc/configurableclientlist_inc.cc b/src/lib/python/isc/datasrc/configurableclientlist_inc.cc
new file mode 100644
index 0000000..b16a6bb
--- /dev/null
+++ b/src/lib/python/isc/datasrc/configurableclientlist_inc.cc
@@ -0,0 +1,134 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+namespace {
+
+const char* const ConfigurableClientList_doc = "\
+The list of data source clients\n\
+\n\
+The purpose is to have several data source clients of the same class\
+and then be able to search through them to identify the one containing\
+a given zone.\n\
+\n\
+Unlike the C++ version, we don't have the abstract base class. Abstract\
+classes are not needed due to the duck typing nature of python.\
+";
+
+const char* const ConfigurableClientList_configure_doc = "\
+configure(configuration, allow_cache) -> None\n\
+\n\
+Wrapper around C++ ConfigurableClientList::configure\n\
+\n\
+This sets the active configuration. It fills the ConfigurableClientList with\
+corresponding data source clients.\n\
+\n\
+If any error is detected, an exception is raised and the previous\
+configuration preserved.\n\
+\n\
+Parameters:\n\
+ configuration The configuration, as a JSON encoded string.\
+ allow_cache If caching is allowed.\
+";
+
+const char* const ConfigurableClientList_reset_memory_segment_doc = "\
+reset_memory_segment(datasrc_name, mode, config_params) -> None\n\
+\n\
+This method resets the zone table segment for a datasource with a new\n\
+memory segment.\n\
+\n\
+Parameters:\n\
+ datasrc_name The name of the data source whose segment to reset.\n\
+ mode The open mode for the new memory segment.\n\
+ config_params The configuration for the new memory segment, as a JSON encoded string.\n\
+";
+
+const char* const ConfigurableClientList_get_zone_table_accessor_doc = "\
+get_zone_table_accessor(datasrc_name, use_cache) -> \
+isc.datasrc.ZoneTableAccessor\n\
+\n\
+Create a ZoneTableAccessor object for the specified data source.\n\
+\n\
+Parameters:\n\
+ datasrc_name If not empty, the name of the data source\n\
+ use_cache If true, create a zone table for in-memory cache.\n\
+";
+
+const char* const ConfigurableClientList_get_cached_zone_writer_doc = "\
+get_cached_zone_writer(zone, catch_load_error, datasrc_name) -> status, zone_writer\n\
+\n\
+This method returns a ZoneWriter that can be used to (re)load a zone.\n\
+\n\
+By default this method identifies the first data source in the list\n\
+that should serve the zone of the given name, and returns a ZoneWriter\n\
+object that can be used to load the content of the zone, in a specific\n\
+way for that data source.\n\
+\n\
+If the optional datasrc_name parameter is provided with a non empty\n\
+string, this method only tries to load the specified zone into or with\n\
+the data source which has the given name, regardless where in the list\n\
+that data source is placed. Even if the given name of zone doesn't\n\
+exist in the data source, other data sources are not searched and\n\
+this method simply returns ZONE_NOT_FOUND in the first element\n\
+of the pair.\n\
+\n\
+Two elements are returned. The first element is a status\n\
+indicating if it worked or not (and in case it didn't, also why). If the\n\
+status is ZONE_SUCCESS, the second element contains a ZoneWriter object. If\n\
+the status is anything else, the second element is None.\n\
+\n\
+Parameters:\n\
+ zone The origin of the zone to (re)load.\n\
+ catch_load_error Whether to catch load errors, or to raise them as exceptions.\n\
+ datasrc_name The name of the data source where the zone is to be loaded (optional).\n\
+";
+
+const char* const ConfigurableClientList_get_status_doc = "\
+get_status() -> list of tuples\n\
+\n\
+This method returns a list of tuples, with each tuple containing the\n\
+status of a data source client. If there are no data source clients\n\
+present, an empty list is returned.\n\
+\n\
+The tuples contain (name, segment_type, segment_state):\n\
+ name The name of the data source.\n\
+ segment_type A string indicating the type of memory segment in use.\n\
+ segment_state The state of the memory segment.\n\
+\n\
+If segment_state is SEGMENT_UNUSED, None is returned for the segment_type.\n\
+";
+
+const char* const ConfigurableClientList_find_doc = "\
+find(zone, want_exact_match=False, want_finder=True) -> datasrc_client,\
+zone_finder, exact_match\n\
+\n\
+Look for a data source containing the given zone.\n\
+\n\
+It searches through the contained data sources and returns a data source\
+containing the zone, the zone finder of the zone and a boolean if the answer\
+is an exact match.\n\
+\n\
+The first parameter is isc.dns.Name object of a name in the zone. If the\
+want_exact_match is True, only zone with this exact origin is returned.\
+If it is False, the best matching zone is returned.\n\
+\n\
+If the want_finder is False, the returned zone_finder might be None even\
+if the data source is identified (in such case, the datasrc_client is not\
+None). Setting it to false allows the client list some optimisations, if\
+you don't need it, but if you do need it, it is better to set it to True\
+instead of getting it from the datasrc_client later.\n\
+\n\
+If no answer is found, the datasrc_client and zone_finder are None.\
+";
+
+} // unnamed namespace
diff --git a/src/lib/python/isc/datasrc/configurableclientlist_python.cc b/src/lib/python/isc/datasrc/configurableclientlist_python.cc
index 81da7d8..a8c6d5f 100644
--- a/src/lib/python/isc/datasrc/configurableclientlist_python.cc
+++ b/src/lib/python/isc/datasrc/configurableclientlist_python.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -27,6 +27,7 @@
#include <dns/python/rrclass_python.h>
#include <dns/python/name_python.h>
+#include <dns/python/pydnspp_common.h>
#include <datasrc/client_list.h>
@@ -34,11 +35,18 @@
#include "datasrc.h"
#include "finder_python.h"
#include "client_python.h"
+#include "zonetable_accessor_python.h"
+#include "zonewriter_python.h"
+
+#include "configurableclientlist_inc.cc"
using namespace std;
using namespace isc::util::python;
using namespace isc::datasrc;
+using namespace isc::datasrc::memory;
using namespace isc::datasrc::python;
+using namespace isc::datasrc::memory::python;
+using namespace isc::dns::python;
//
// ConfigurableClientList
@@ -64,7 +72,8 @@ ConfigurableClientList_init(PyObject* po_self, PyObject* args, PyObject*) {
return (0);
}
} catch (const exception& ex) {
- const string ex_what = "Failed to construct ConfigurableClientList object: " +
+ const string ex_what =
+ "Failed to construct ConfigurableClientList object: " +
string(ex.what());
PyErr_SetString(getDataSourceException("Error"), ex_what.c_str());
return (-1);
@@ -116,6 +125,129 @@ ConfigurableClientList_configure(PyObject* po_self, PyObject* args) {
}
PyObject*
+ConfigurableClientList_resetMemorySegment(PyObject* po_self, PyObject* args) {
+ s_ConfigurableClientList* self =
+ static_cast<s_ConfigurableClientList*>(po_self);
+ try {
+ const char* datasrc_name_p;
+ int mode_int;
+ const char* config_p;
+ if (PyArg_ParseTuple(args, "sis", &datasrc_name_p, &mode_int,
+ &config_p)) {
+ const std::string datasrc_name(datasrc_name_p);
+ const isc::data::ConstElementPtr
+ config(isc::data::Element::fromJSON(std::string(config_p)));
+ ZoneTableSegment::MemorySegmentOpenMode mode =
+ static_cast<ZoneTableSegment::MemorySegmentOpenMode>
+ (mode_int);
+ self->cppobj->resetMemorySegment(datasrc_name, mode, config);
+ Py_RETURN_NONE;
+ }
+ } catch (const isc::data::JSONError& jse) {
+ const string ex_what(std::string("JSON parse error in memory segment"
+ " configuration: ") + jse.what());
+ PyErr_SetString(getDataSourceException("Error"), ex_what.c_str());
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ }
+
+ return (NULL);
+}
+
+PyObject*
+ConfigurableClientList_getCachedZoneWriter(PyObject* po_self, PyObject* args) {
+ s_ConfigurableClientList* self =
+ static_cast<s_ConfigurableClientList*>(po_self);
+ try {
+ PyObject* name_obj;
+ int catch_load_error;
+ const char* datasrc_name_p = "";
+#if (PY_VERSION_HEX >= 0x030300f0)
+ // The 'p' specifier for predicate (boolean) is available from
+ // Python 3.3 (final) only.
+ if (PyArg_ParseTuple(args, "O!p|s", &isc::dns::python::name_type,
+ &name_obj, &catch_load_error, &datasrc_name_p)) {
+#else
+ if (PyArg_ParseTuple(args, "O!i|s", &isc::dns::python::name_type,
+ &name_obj, &catch_load_error, &datasrc_name_p)) {
+#endif
+ const isc::dns::Name&
+ name(isc::dns::python::PyName_ToName(name_obj));
+ const std::string datasrc_name(datasrc_name_p);
+
+ const ConfigurableClientList::ZoneWriterPair result =
+ self->cppobj->getCachedZoneWriter(name, catch_load_error,
+ datasrc_name);
+
+ PyObjectContainer writer;
+ if (!result.second) {
+ // Use the Py_BuildValue, as it takes care of the
+ // reference counts correctly.
+ writer.reset(Py_BuildValue(""));
+ } else {
+ // Make sure it keeps the writer alive.
+ writer.reset(createZoneWriterObject(result.second,
+ po_self));
+ }
+
+ return (Py_BuildValue("IO", result.first, writer.get()));
+ } else {
+ return (NULL);
+ }
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+}
+
+PyObject*
+ConfigurableClientList_getStatus(PyObject* po_self, PyObject*) {
+ s_ConfigurableClientList* self =
+ static_cast<s_ConfigurableClientList*>(po_self);
+ try {
+ const std::vector<DataSourceStatus> status = self->cppobj->getStatus();
+
+ PyObjectContainer slist(PyList_New(status.size()));
+
+ for (size_t i = 0; i < status.size(); ++i) {
+ PyObjectContainer segment_type;
+
+ if (status[i].getSegmentState() != SEGMENT_UNUSED) {
+ segment_type.reset(Py_BuildValue(
+ "s", status[i].getSegmentType().c_str()));
+ } else {
+ Py_INCREF(Py_None);
+ segment_type.reset(Py_None);
+ }
+
+ PyObjectContainer tup(Py_BuildValue("(sOI)",
+ status[i].getName().c_str(),
+ segment_type.get(),
+ status[i].getSegmentState()));
+ // The following "steals" our reference on tup, so we must
+ // not decref.
+ PyList_SET_ITEM(slist.get(), i, tup.release());
+ }
+
+ return (slist.release());
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+}
+
+PyObject*
ConfigurableClientList_find(PyObject* po_self, PyObject* args) {
s_ConfigurableClientList* self =
static_cast<s_ConfigurableClientList*>(po_self);
@@ -125,7 +257,7 @@ ConfigurableClientList_find(PyObject* po_self, PyObject* args) {
int want_finder = 1;
if (PyArg_ParseTuple(args, "O!|ii", &isc::dns::python::name_type,
&name_obj, &want_exact_match, &want_finder)) {
- const isc::dns::Name
+ const isc::dns::Name&
name(isc::dns::python::PyName_ToName(name_obj));
const ClientList::FindResult
result(self->cppobj->find(name, want_exact_match,
@@ -170,6 +302,37 @@ ConfigurableClientList_find(PyObject* po_self, PyObject* args) {
}
}
+PyObject*
+ConfigurableClientList_getZoneTableAccessor(PyObject* po_self, PyObject* args) {
+ s_ConfigurableClientList* self =
+ static_cast<s_ConfigurableClientList*>(po_self);
+ try {
+ const char* datasrc_name;
+ int use_cache;
+ if (PyArg_ParseTuple(args, "zi", &datasrc_name, &use_cache)) {
+ // python 'None' will be read as NULL, which we convert to an
+ // empty string, meaning "any data source"
+ const std::string name(datasrc_name ? datasrc_name : "");
+ const ConstZoneTableAccessorPtr
+ z(self->cppobj->getZoneTableAccessor(name, use_cache));
+ if (z == NULL) {
+ Py_RETURN_NONE;
+ } else {
+ return (createZoneTableAccessorObject(z, po_self));
+ }
+ } else {
+ return (NULL);
+ }
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+}
+
// This list contains the actual set of functions we have in
// python. Each entry has
// 1. Python method name
@@ -177,55 +340,21 @@ ConfigurableClientList_find(PyObject* po_self, PyObject* args) {
// 3. Argument type
// 4. Documentation
PyMethodDef ConfigurableClientList_methods[] = {
- { "configure", ConfigurableClientList_configure, METH_VARARGS,
- "configure(configuration, allow_cache) -> None\n\
-\n\
-Wrapper around C++ ConfigurableClientList::configure\n\
-\n\
-This sets the active configuration. It fills the ConfigurableClientList with\
-corresponding data source clients.\n\
-\n\
-If any error is detected, an exception is raised and the previous\
-configuration preserved.\n\
-\n\
-Parameters:\n\
- configuration The configuration, as a JSON encoded string.\
- allow_cache If caching is allowed." },
- { "find", ConfigurableClientList_find, METH_VARARGS,
-"find(zone, want_exact_match=False, want_finder=True) -> datasrc_client,\
-zone_finder, exact_match\n\
-\n\
-Look for a data source containing the given zone.\n\
-\n\
-It searches through the contained data sources and returns a data source\
-containing the zone, the zone finder of the zone and a boolean if the answer\
-is an exact match.\n\
-\n\
-The first parameter is isc.dns.Name object of a name in the zone. If the\
-want_exact_match is True, only zone with this exact origin is returned.\
-If it is False, the best matching zone is returned.\n\
-\n\
-If the want_finder is False, the returned zone_finder might be None even\
-if the data source is identified (in such case, the datasrc_client is not\
-None). Setting it to false allows the client list some optimisations, if\
-you don't need it, but if you do need it, it is better to set it to True\
-instead of getting it from the datasrc_client later.\n\
-\n\
-If no answer is found, the datasrc_client and zone_finder are None." },
+ { "configure", ConfigurableClientList_configure,
+ METH_VARARGS, ConfigurableClientList_configure_doc },
+ { "reset_memory_segment", ConfigurableClientList_resetMemorySegment,
+ METH_VARARGS, ConfigurableClientList_reset_memory_segment_doc },
+ { "get_zone_table_accessor", ConfigurableClientList_getZoneTableAccessor,
+ METH_VARARGS, ConfigurableClientList_get_zone_table_accessor_doc },
+ { "get_cached_zone_writer", ConfigurableClientList_getCachedZoneWriter,
+ METH_VARARGS, ConfigurableClientList_get_cached_zone_writer_doc },
+ { "get_status", ConfigurableClientList_getStatus,
+ METH_NOARGS, ConfigurableClientList_get_status_doc },
+ { "find", ConfigurableClientList_find,
+ METH_VARARGS, ConfigurableClientList_find_doc },
{ NULL, NULL, 0, NULL }
};
-const char* const ConfigurableClientList_doc = "\
-The list of data source clients\n\
-\n\
-The purpose is to have several data source clients of the same class\
-and then be able to search through them to identify the one containing\
-a given zone.\n\
-\n\
-Unlike the C++ version, we don't have the abstract base class. Abstract\
-classes are not needed due to the duck typing nature of python.\
-";
-
} // end of unnamed namespace
namespace isc {
@@ -237,9 +366,9 @@ namespace python {
PyTypeObject configurableclientlist_type = {
PyVarObject_HEAD_INIT(NULL, 0)
"datasrc.ConfigurableClientList",
- sizeof(s_ConfigurableClientList), // tp_basicsize
+ sizeof(s_ConfigurableClientList), // tp_basicsize
0, // tp_itemsize
- ConfigurableClientList_destroy, // tp_dealloc
+ ConfigurableClientList_destroy, // tp_dealloc
NULL, // tp_print
NULL, // tp_getattr
NULL, // tp_setattr
@@ -262,7 +391,7 @@ PyTypeObject configurableclientlist_type = {
0, // tp_weaklistoffset
NULL, // tp_iter
NULL, // tp_iternext
- ConfigurableClientList_methods, // tp_methods
+ ConfigurableClientList_methods, // tp_methods
NULL, // tp_members
NULL, // tp_getset
NULL, // tp_base
@@ -270,7 +399,7 @@ PyTypeObject configurableclientlist_type = {
NULL, // tp_descr_get
NULL, // tp_descr_set
0, // tp_dictoffset
- ConfigurableClientList_init, // tp_init
+ ConfigurableClientList_init, // tp_init
NULL, // tp_alloc
PyType_GenericNew, // tp_new
NULL, // tp_free
@@ -300,6 +429,66 @@ initModulePart_ConfigurableClientList(PyObject* mod) {
}
Py_INCREF(&configurableclientlist_type);
+ try {
+ // ConfigurableClientList::CacheStatus enum
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_CACHE_DISABLED",
+ Py_BuildValue("I", ConfigurableClientList::CACHE_DISABLED));
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_ZONE_NOT_CACHED",
+ Py_BuildValue("I", ConfigurableClientList::ZONE_NOT_CACHED));
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_ZONE_NOT_FOUND",
+ Py_BuildValue("I", ConfigurableClientList::ZONE_NOT_FOUND));
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_CACHE_NOT_WRITABLE",
+ Py_BuildValue("I", ConfigurableClientList::CACHE_NOT_WRITABLE));
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_DATASRC_NOT_FOUND",
+ Py_BuildValue("I", ConfigurableClientList::DATASRC_NOT_FOUND));
+ installClassVariable
+ (configurableclientlist_type,
+ "CACHE_STATUS_ZONE_SUCCESS",
+ Py_BuildValue("I", ConfigurableClientList::ZONE_SUCCESS));
+
+ // MemorySegmentState enum
+ installClassVariable(configurableclientlist_type,
+ "SEGMENT_UNUSED",
+ Py_BuildValue("I", SEGMENT_UNUSED));
+ installClassVariable(configurableclientlist_type,
+ "SEGMENT_WAITING",
+ Py_BuildValue("I", SEGMENT_WAITING));
+ installClassVariable(configurableclientlist_type,
+ "SEGMENT_INUSE",
+ Py_BuildValue("I", SEGMENT_INUSE));
+
+ // FIXME: These should eventually be moved to the
+ // ZoneTableSegment class when we add Python bindings for the
+ // memory data source specific bits. But for now, we add these
+ // enums here to support reloading a zone table segment.
+ installClassVariable(configurableclientlist_type, "CREATE",
+ Py_BuildValue("I", ZoneTableSegment::CREATE));
+ installClassVariable(configurableclientlist_type, "READ_WRITE",
+ Py_BuildValue("I", ZoneTableSegment::READ_WRITE));
+ installClassVariable(configurableclientlist_type, "READ_ONLY",
+ Py_BuildValue("I", ZoneTableSegment::READ_ONLY));
+ } catch (const std::exception& ex) {
+ const std::string ex_what =
+ "Unexpected failure in ConfigurableClientList initialization: " +
+ std::string(ex.what());
+ PyErr_SetString(po_IscException, ex_what.c_str());
+ return (false);
+ } catch (...) {
+ PyErr_SetString(PyExc_SystemError,
+ "Unexpected failure in ConfigurableClientList initialization");
+ return (false);
+ }
+
return (true);
}
diff --git a/src/lib/python/isc/datasrc/datasrc.cc b/src/lib/python/isc/datasrc/datasrc.cc
index f2cbae3..11c3e7c 100644
--- a/src/lib/python/isc/datasrc/datasrc.cc
+++ b/src/lib/python/isc/datasrc/datasrc.cc
@@ -33,6 +33,9 @@
#include "journal_reader_python.h"
#include "configurableclientlist_python.h"
#include "zone_loader_python.h"
+#include "zonetable_accessor_python.h"
+#include "zonetable_iterator_python.h"
+#include "zonewriter_python.h"
#include <util/python/pycppwrapper_util.h>
#include <dns/python/pydnspp_common.h>
@@ -42,6 +45,7 @@
using namespace isc::datasrc;
using namespace isc::datasrc::python;
+using namespace isc::datasrc::memory::python;
using namespace isc::util::python;
using namespace isc::dns::python;
@@ -255,6 +259,42 @@ initModulePart_ZoneJournalReader(PyObject* mod) {
return (true);
}
+bool
+initModulePart_ZoneTableAccessor(PyObject* mod) {
+ // We initialize the static description object with PyType_Ready(),
+ // then add it to the module. This is not just a check! (leaving
+ // this out results in segmentation faults)
+ if (PyType_Ready(&zonetableaccessor_type) < 0) {
+ return (false);
+ }
+ void* p = &zonetableaccessor_type;
+ if (PyModule_AddObject(mod, "ZoneTableAccessor",
+ static_cast<PyObject*>(p)) < 0) {
+ return (false);
+ }
+ Py_INCREF(&zonetableaccessor_type);
+
+ return (true);
+}
+
+bool
+initModulePart_ZoneTableIterator(PyObject* mod) {
+ // We initialize the static description object with PyType_Ready(),
+ // then add it to the module. This is not just a check! (leaving
+ // this out results in segmentation faults)
+ if (PyType_Ready(&zonetableiterator_type) < 0) {
+ return (false);
+ }
+ void* p = &zonetableiterator_type;
+ if (PyModule_AddObject(mod, "ZoneTableIterator",
+ static_cast<PyObject*>(p)) < 0) {
+ return (false);
+ }
+ Py_INCREF(&zonetableiterator_type);
+
+ return (true);
+}
+
PyObject* po_DataSourceError;
PyObject* po_MasterFileError;
PyObject* po_NotImplemented;
@@ -340,5 +380,20 @@ PyInit_datasrc(void) {
return (NULL);
}
+ if (!initModulePart_ZoneTableAccessor(mod)) {
+ Py_DECREF(mod);
+ return (NULL);
+ }
+
+ if (!initModulePart_ZoneTableIterator(mod)) {
+ Py_DECREF(mod);
+ return (NULL);
+ }
+
+ if (!initModulePart_ZoneWriter(mod)) {
+ Py_DECREF(mod);
+ return (NULL);
+ }
+
return (mod);
}
diff --git a/src/lib/python/isc/datasrc/finder_python.cc b/src/lib/python/isc/datasrc/finder_python.cc
index ef1bcc1..5a8e84f 100644
--- a/src/lib/python/isc/datasrc/finder_python.cc
+++ b/src/lib/python/isc/datasrc/finder_python.cc
@@ -64,7 +64,7 @@ getFindResultFlags(const ZoneFinder::Context& context) {
namespace isc_datasrc_internal {
// This is the shared code for the find() call in the finder and the updater
-// Is is intentionally not available through any header, nor at our standard
+// It is intentionally not available through any header, nor at our standard
// namespace, as it is not supposed to be called anywhere but from finder and
// updater
PyObject* ZoneFinder_helper(ZoneFinder* finder, PyObject* args) {
@@ -184,7 +184,7 @@ public:
ZoneFinderPtr cppobj;
// This is a reference to a base object; if the object of this class
// depends on another object to be in scope during its lifetime,
- // we use INCREF the base object upon creation, and DECREF it at
+ // we INCREF the base object upon creation, and DECREF it at
// the end of the destructor
// This is an optional argument to createXXX(). If NULL, it is ignored.
PyObject* base_obj;
diff --git a/src/lib/python/isc/datasrc/iterator_python.cc b/src/lib/python/isc/datasrc/iterator_python.cc
index 9757a3b..0b80f20 100644
--- a/src/lib/python/isc/datasrc/iterator_python.cc
+++ b/src/lib/python/isc/datasrc/iterator_python.cc
@@ -61,7 +61,7 @@ typedef CPPPyObjectContainer<s_ZoneIterator, ZoneIterator>
// General creation and destruction
int
-ZoneIterator_init(s_ZoneIterator* self, PyObject* args) {
+ZoneIterator_init(s_ZoneIterator*, PyObject*) {
// can't be called directly
PyErr_SetString(PyExc_TypeError,
"ZoneIterator cannot be constructed directly");
diff --git a/src/lib/python/isc/datasrc/tests/Makefile.am b/src/lib/python/isc/datasrc/tests/Makefile.am
index a86c2b4..256ca62 100644
--- a/src/lib/python/isc/datasrc/tests/Makefile.am
+++ b/src/lib/python/isc/datasrc/tests/Makefile.am
@@ -1,17 +1,10 @@
+SUBDIRS = testdata
+
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
PYTESTS = datasrc_test.py sqlite3_ds_test.py
PYTESTS += clientlist_test.py zone_loader_test.py
EXTRA_DIST = $(PYTESTS)
-EXTRA_DIST += testdata/brokendb.sqlite3
-EXTRA_DIST += testdata/example.com.sqlite3
-EXTRA_DIST += testdata/example.com.source.sqlite3
-EXTRA_DIST += testdata/newschema.sqlite3
-EXTRA_DIST += testdata/oldschema.sqlite3
-EXTRA_DIST += testdata/new_minor_schema.sqlite3
-EXTRA_DIST += testdata/example.com
-EXTRA_DIST += testdata/example.com.ch
-EXTRA_DIST += testdata/static.zone
CLEANFILES = $(abs_builddir)/rwtest.sqlite3.copied
CLEANFILES += $(abs_builddir)/zoneloadertest.sqlite3
@@ -35,13 +28,18 @@ if ENABLE_PYTHON_COVERAGE
rm -f .coverage
${LN_S} $(abs_top_srcdir)/.coverage .coverage
endif
+if USE_SHARED_MEMORY
+HAVE_SHARED_MEMORY=yes
+else
+HAVE_SHARED_MEMORY=no
+endif
for pytest in $(PYTESTS) ; do \
echo Running test: $$pytest ; \
$(LIBRARY_PATH_PLACEHOLDER) \
PYTHONPATH=:$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/python/isc/log:$(abs_top_builddir)/src/lib/python/isc/datasrc/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs \
TESTDATA_PATH=$(abs_srcdir)/testdata \
TESTDATA_WRITE_PATH=$(abs_builddir) \
- GLOBAL_TESTDATA_PATH=$(abs_top_srcdir)/src/lib/testutils/testdata \
+ HAVE_SHARED_MEMORY=$(HAVE_SHARED_MEMORY) \
B10_FROM_BUILD=$(abs_top_builddir) \
$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
done
diff --git a/src/lib/python/isc/datasrc/tests/clientlist_test.py b/src/lib/python/isc/datasrc/tests/clientlist_test.py
index bdac69c..e8056a5 100644
--- a/src/lib/python/isc/datasrc/tests/clientlist_test.py
+++ b/src/lib/python/isc/datasrc/tests/clientlist_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 Internet Systems Consortium.
+# Copyright (C) 2012-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -20,7 +20,8 @@ import unittest
import os
import sys
-TESTDATA_PATH = os.environ['GLOBAL_TESTDATA_PATH'] + os.sep
+TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
+MAPFILE_PATH = os.environ['TESTDATA_WRITE_PATH'] + os.sep + 'test.mapped'
class ClientListTest(unittest.TestCase):
"""
@@ -36,8 +37,18 @@ class ClientListTest(unittest.TestCase):
# last.
self.dsrc = None
self.finder = None
+
+ # If a test created a ZoneWriter with a mapped memory segment,
+ # the writer will hold a reference to the client list which will
+ # need the mapfile to exist until it's destroyed. So we'll make
+ # sure to destroy the writer (by resetting it) before removing
+ # the mapfile below.
+ self.__zone_writer = None
self.clist = None
+ if os.path.exists(MAPFILE_PATH):
+ os.unlink(MAPFILE_PATH)
+
def test_constructors(self):
"""
Test the constructor. It should accept an RRClass. Check it
@@ -54,32 +65,35 @@ class ClientListTest(unittest.TestCase):
self.assertRaises(TypeError, isc.datasrc.ConfigurableClientList,
isc.dns.RRClass.IN, isc.dns.RRClass.IN)
+ def configure_helper(self):
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {
+ "example.com": "''' + TESTDATA_PATH + '''example.com"
+ },
+ "cache-enable": true
+ }]''', True)
+
def test_configure(self):
"""
Test we can configure the client list. This tests if the valid
- ones are acceptend and invalid rejected. We check the changes
+ ones are accepted and invalid rejected. We check the changes
have effect.
"""
self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
# This should be NOP now
self.clist.configure("[]", True)
# Check the zone is not there yet
- dsrc, finder, exact = self.clist.find(isc.dns.Name("example.org"))
+ dsrc, finder, exact = self.clist.find(isc.dns.Name("example.com"))
self.assertIsNone(dsrc)
self.assertIsNone(finder)
self.assertFalse(exact)
# We can use this type, as it is not loaded dynamically.
- self.clist.configure('''[{
- "type": "MasterFiles",
- "params": {
- "example.org": "''' + TESTDATA_PATH + '''example.org.zone"
- },
- "cache-enable": true
- }]''', True)
+ self.configure_helper()
# Check the zone is there now. Proper tests of find are in other
# test methods.
self.dsrc, self.finder, exact = \
- self.clist.find(isc.dns.Name("example.org"))
+ self.clist.find(isc.dns.Name("example.com"))
self.assertIsNotNone(self.dsrc)
self.assertTrue(isinstance(self.dsrc, isc.datasrc.DataSourceClient))
self.assertIsNotNone(self.finder)
@@ -88,29 +102,14 @@ class ClientListTest(unittest.TestCase):
self.assertRaises(isc.datasrc.Error, self.clist.configure,
'"bad type"', True)
self.assertRaises(isc.datasrc.Error, self.clist.configure, '''[{
- "type": "bad type"
- }]''', True)
- self.assertRaises(isc.datasrc.Error, self.clist.configure, '''[{
bad JSON,
}]''', True)
self.assertRaises(TypeError, self.clist.configure, [], True)
self.assertRaises(TypeError, self.clist.configure, "[]")
self.assertRaises(TypeError, self.clist.configure, "[]", "true")
- def test_find(self):
- """
- Test the find accepts the right arguments, some of them can be omitted,
- etc.
- """
- self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
- self.clist.configure('''[{
- "type": "MasterFiles",
- "params": {
- "example.org": "''' + TESTDATA_PATH + '''example.org.zone"
- },
- "cache-enable": true
- }]''', True)
- dsrc, finder, exact = self.clist.find(isc.dns.Name("sub.example.org"))
+ def find_helper(self):
+ dsrc, finder, exact = self.clist.find(isc.dns.Name("sub.example.com"))
self.assertIsNotNone(dsrc)
self.assertTrue(isinstance(dsrc, isc.datasrc.DataSourceClient))
self.assertIsNotNone(finder)
@@ -124,33 +123,296 @@ class ClientListTest(unittest.TestCase):
# We check an exact match in test_configure already
self.assertFalse(exact)
self.dsrc, self.finder, exact = \
- self.clist.find(isc.dns.Name("sub.example.org"), False)
+ self.clist.find(isc.dns.Name("sub.example.com"), False)
self.assertIsNotNone(self.dsrc)
self.assertTrue(isinstance(self.dsrc, isc.datasrc.DataSourceClient))
self.assertIsNotNone(self.finder)
self.assertTrue(isinstance(self.finder, isc.datasrc.ZoneFinder))
self.assertFalse(exact)
self.dsrc, self.finder, exact = \
- self.clist.find(isc.dns.Name("sub.example.org"), True)
+ self.clist.find(isc.dns.Name("sub.example.com"), True)
self.assertIsNone(self.dsrc)
self.assertIsNone(self.finder)
self.assertFalse(exact)
self.dsrc, self.finder, exact = \
- self.clist.find(isc.dns.Name("sub.example.org"), False, False)
+ self.clist.find(isc.dns.Name("sub.example.com"), False, False)
self.assertIsNotNone(self.dsrc)
self.assertTrue(isinstance(self.dsrc, isc.datasrc.DataSourceClient))
self.assertIsNotNone(self.finder)
self.assertTrue(isinstance(self.finder, isc.datasrc.ZoneFinder))
self.assertFalse(exact)
self.dsrc, self.finder, exact = \
- self.clist.find(isc.dns.Name("sub.example.org"), True, False)
+ self.clist.find(isc.dns.Name("sub.example.com"), True, False)
self.assertIsNone(self.dsrc)
self.assertIsNone(self.finder)
self.assertFalse(exact)
# Some invalid inputs
- self.assertRaises(TypeError, self.clist.find, "example.org")
+ self.assertRaises(TypeError, self.clist.find, "example.com")
self.assertRaises(TypeError, self.clist.find)
+ def test_get_zone_table_accessor(self):
+ """
+ Test that we can get the zone table accessor and, thereby,
+ the zone table iterator.
+ """
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+
+ # null configuration
+ self.clist.configure("[]", True)
+ self.assertIsNone(self.clist.get_zone_table_accessor(None, True))
+
+ # empty configuration
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {},
+ "cache-enable": true
+ }]''', True)
+ # bogus datasrc
+ self.assertIsNone(self.clist.get_zone_table_accessor("bogus", True))
+ # first datasrc - empty zone table
+ table = self.clist.get_zone_table_accessor(None, True)
+ self.assertIsNotNone(table)
+ iterator = iter(table)
+ self.assertIsNotNone(iterator)
+ self.assertEqual(0, len(list(iterator)))
+
+ # normal configuration
+ self.configure_helper()
+ # !use_cache => NotImplemented
+ self.assertRaises(isc.datasrc.Error,
+ self.clist.get_zone_table_accessor, None, False)
+ # bogus datasrc
+ self.assertIsNone(self.clist.get_zone_table_accessor("bogus", True))
+
+ # first datasrc
+ table = self.clist.get_zone_table_accessor(None, True)
+ self.assertIsNotNone(table)
+ zonelist = list(table)
+ self.assertEqual(1, len(zonelist))
+ self.assertEqual(zonelist[0][1], isc.dns.Name("example.com"))
+
+ # named datasrc
+ table = self.clist.get_zone_table_accessor("MasterFiles", True)
+ self.assertEqual(zonelist, list(table))
+
+ # longer zone list for non-trivial iteration
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {
+ "example.org": "''' + TESTDATA_PATH + '''example.org.zone",
+ "example.com": "''' + TESTDATA_PATH + '''example.com.zone",
+ "example.net": "''' + TESTDATA_PATH + '''example.net.zone",
+ "example.biz": "''' + TESTDATA_PATH + '''example.biz.zone",
+ "example.edu": "''' + TESTDATA_PATH + '''example.edu.zone"
+ },
+ "cache-enable": true
+ }]''', True)
+ zonelist = list(self.clist.get_zone_table_accessor(None, True))
+ self.assertEqual(5, len(zonelist))
+ self.assertTrue((0, isc.dns.Name("example.net.")) in zonelist)
+
+ # ensure the iterator returns exactly and only the zones we expect
+ zonelist = [
+ isc.dns.Name("example.org"),
+ isc.dns.Name("example.com"),
+ isc.dns.Name("example.net"),
+ isc.dns.Name("example.biz"),
+ isc.dns.Name("example.edu")]
+ table = self.clist.get_zone_table_accessor("MasterFiles", True)
+ for index, zone in table:
+ self.assertTrue(zone in zonelist)
+ zonelist.remove(zone)
+ self.assertEqual(0, len(zonelist))
+
+ def test_find(self):
+ """
+ Test the find accepts the right arguments, some of them can be omitted,
+ etc.
+ """
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.configure_helper()
+ self.find_helper()
+
+ @unittest.skipIf(os.environ['HAVE_SHARED_MEMORY'] != 'yes',
+ 'shared memory is not available')
+ def test_find_mapped(self):
+ """
+ Test find on a mapped segment.
+ """
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {
+ "example.com": "''' + TESTDATA_PATH + '''example.com"
+ },
+ "cache-enable": true,
+ "cache-type": "mapped"
+ }]''', True)
+
+ map_params = '{"mapped-file": "' + MAPFILE_PATH + '"}'
+ self.clist.reset_memory_segment("MasterFiles",
+ isc.datasrc.ConfigurableClientList.CREATE,
+ map_params)
+ result, self.__zone_writer = \
+ self.clist.get_cached_zone_writer(isc.dns.Name("example.com"),
+ False)
+ self.assertEqual(isc.datasrc.ConfigurableClientList.CACHE_STATUS_ZONE_SUCCESS,
+ result)
+ err_msg = self.__zone_writer.load()
+ self.assertIsNone(err_msg)
+ self.__zone_writer.install()
+ self.__zone_writer.cleanup()
+
+ self.clist.reset_memory_segment("MasterFiles",
+ isc.datasrc.ConfigurableClientList.READ_ONLY,
+ map_params)
+ result, self.__zone_writer = \
+ self.clist.get_cached_zone_writer(isc.dns.Name("example.com"),
+ False)
+ self.assertEqual(isc.datasrc.ConfigurableClientList.CACHE_STATUS_CACHE_NOT_WRITABLE,
+ result)
+
+ # The segment is still in READ_ONLY mode.
+ self.find_helper()
+
+ def test_zone_writer_load_twice(self):
+ """
+ Test that the zone writer throws when load() is called more than
+ once.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.configure_helper()
+
+ result, self.__zone_writer = \
+ self.clist.get_cached_zone_writer(isc.dns.Name("example.com"),
+ False)
+ self.assertEqual(isc.datasrc.ConfigurableClientList.CACHE_STATUS_ZONE_SUCCESS,
+ result)
+ err_msg = self.__zone_writer.load()
+ self.assertIsNone(err_msg)
+ self.assertRaises(isc.datasrc.Error, self.__zone_writer.load)
+ self.__zone_writer.cleanup()
+
+ def test_zone_writer_load_without_raise(self):
+ """
+ Test that the zone writer does not throw when asked not to.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {
+ "example.com": "''' + TESTDATA_PATH + '''example.com-broken.zone"
+ },
+ "cache-enable": true
+ }]''', True)
+
+ result, self.__zone_writer = \
+ self.clist.get_cached_zone_writer(isc.dns.Name("example.com"),
+ True)
+ self.assertEqual(isc.datasrc.ConfigurableClientList.CACHE_STATUS_ZONE_SUCCESS,
+ result)
+ err_msg = self.__zone_writer.load()
+ self.assertIsNotNone(err_msg)
+ self.assertTrue('Errors found when validating zone' in err_msg)
+ self.__zone_writer.cleanup()
+
+ def test_zone_writer_install_without_load(self):
+ """
+ Test that the zone writer throws when install() is called
+ without calling load() first.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.configure_helper()
+
+ result, self.__zone_writer = \
+ self.clist.get_cached_zone_writer(isc.dns.Name("example.com"),
+ False)
+ self.assertEqual(isc.datasrc.ConfigurableClientList.CACHE_STATUS_ZONE_SUCCESS,
+ result)
+ self.assertRaises(isc.datasrc.Error, self.__zone_writer.install)
+ self.__zone_writer.cleanup()
+
+ def test_get_status(self):
+ """
+ Test getting status of various data sources.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+
+ status = self.clist.get_status()
+ self.assertIsNotNone(status)
+ self.assertTrue(isinstance(status, list))
+ self.assertEqual(0, len(status))
+
+ self.configure_helper()
+
+ status = self.clist.get_status()
+ self.assertIsNotNone(status)
+ self.assertTrue(isinstance(status, list))
+ self.assertEqual(1, len(status))
+ self.assertTrue(isinstance(status[0], tuple))
+ self.assertTupleEqual(('MasterFiles', 'local',
+ isc.datasrc.ConfigurableClientList.SEGMENT_INUSE),
+ status[0])
+
+ @unittest.skipIf(os.environ['HAVE_SHARED_MEMORY'] != 'yes',
+ 'shared memory is not available')
+ def test_get_status_unused(self):
+ """
+ Test getting status when segment type is mapped, but the cache
+ is disabled.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.clist.configure('''[{
+ "type": "sqlite3",
+ "params": {
+ "database_file": "''' + TESTDATA_PATH + '''example.com.sqlite3"
+ },
+ "cache-zones" : ["example.com"],
+ "cache-type": "mapped",
+ "cache-enable": false
+ }]''', True)
+
+ status = self.clist.get_status()
+ self.assertIsNotNone(status)
+ self.assertTrue(isinstance(status, list))
+ self.assertEqual(1, len(status))
+ self.assertTrue(isinstance(status[0], tuple))
+ self.assertTupleEqual(('sqlite3', None,
+ isc.datasrc.ConfigurableClientList.SEGMENT_UNUSED),
+ status[0])
+
+ @unittest.skipIf(os.environ['HAVE_SHARED_MEMORY'] != 'yes',
+ 'shared memory is not available')
+ def test_get_status_waiting(self):
+ """
+ Test getting status when segment type is mapped and it has not
+ been reset yet.
+ """
+
+ self.clist = isc.datasrc.ConfigurableClientList(isc.dns.RRClass.IN)
+ self.clist.configure('''[{
+ "type": "MasterFiles",
+ "params": {
+ "example.com": "''' + TESTDATA_PATH + '''example.com"
+ },
+ "cache-enable": true,
+ "cache-type": "mapped"
+ }]''', True)
+
+ status = self.clist.get_status()
+ self.assertIsNotNone(status)
+ self.assertTrue(isinstance(status, list))
+ self.assertEqual(1, len(status))
+ self.assertTrue(isinstance(status[0], tuple))
+ self.assertTupleEqual(('MasterFiles', 'mapped',
+ isc.datasrc.ConfigurableClientList.SEGMENT_WAITING),
+ status[0])
+
if __name__ == "__main__":
isc.log.init("bind10")
isc.log.resetUnitTestRootLogger()
diff --git a/src/lib/python/isc/datasrc/tests/testdata/Makefile.am b/src/lib/python/isc/datasrc/tests/testdata/Makefile.am
new file mode 100644
index 0000000..8365a24
--- /dev/null
+++ b/src/lib/python/isc/datasrc/tests/testdata/Makefile.am
@@ -0,0 +1,12 @@
+EXTRA_DIST = \
+ brokendb.sqlite3 \
+ example.com \
+ example.com-broken.zone \
+ example.com.ch \
+ example.com.source.sqlite3 \
+ example.com.sqlite3 \
+ Makefile.am \
+ new_minor_schema.sqlite3 \
+ newschema.sqlite3 \
+ oldschema.sqlite3 \
+ static.zone
diff --git a/src/lib/python/isc/datasrc/tests/testdata/example.com-broken.zone b/src/lib/python/isc/datasrc/tests/testdata/example.com-broken.zone
new file mode 100644
index 0000000..079b400
--- /dev/null
+++ b/src/lib/python/isc/datasrc/tests/testdata/example.com-broken.zone
@@ -0,0 +1,6 @@
+; This zone is broken (contains no NS records).
+example.com. 1000 IN SOA a.dns.example.com. mail.example.com. 2 1 1 1 1
+a.dns.example.com. 1000 IN A 1.1.1.1
+b.dns.example.com. 1000 IN A 3.3.3.3
+b.dns.example.com. 1000 IN AAAA 4:4::4:4
+b.dns.example.com. 1000 IN AAAA 5:5::5:5
diff --git a/src/lib/python/isc/datasrc/zonetable_accessor_python.cc b/src/lib/python/isc/datasrc/zonetable_accessor_python.cc
new file mode 100644
index 0000000..d0583b9
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonetable_accessor_python.cc
@@ -0,0 +1,182 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <datasrc/zone_table_accessor.h>
+
+#include "datasrc.h"
+#include "zonetable_accessor_python.h"
+#include "zonetable_iterator_python.h"
+
+using namespace std;
+using namespace isc::datasrc;
+using namespace isc::datasrc::python;
+
+namespace {
+// The s_* Class simply covers one instantiation of the object
+class s_ZoneTableAccessor : public PyObject {
+public:
+ s_ZoneTableAccessor() :
+ cppobj(ConstZoneTableAccessorPtr()),
+ base_obj(NULL)
+ {}
+
+ ConstZoneTableAccessorPtr cppobj;
+ // This is a reference to a base object; if the object of this class
+ // depends on another object to be in scope during its lifetime,
+ // we use INCREF the base object upon creation, and DECREF it at
+ // the end of the destructor
+ // This is an optional argument to createXXX(). If NULL, it is ignored.
+ PyObject* base_obj;
+};
+
+int
+ZoneTableAccessor_init(PyObject*, PyObject*, PyObject*) {
+ // can't be called directly
+ PyErr_SetString(PyExc_TypeError,
+ "ZoneTableAccessor cannot be constructed directly");
+
+ return (-1);
+}
+
+void
+ZoneTableAccessor_destroy(PyObject* po_self) {
+ s_ZoneTableAccessor* const self =
+ static_cast<s_ZoneTableAccessor*>(po_self);
+ // cppobj is a shared ptr, but to make sure things are not destroyed in
+ // the wrong order, we reset it here.
+ self->cppobj.reset();
+ if (self->base_obj != NULL) {
+ Py_DECREF(self->base_obj);
+ }
+ Py_TYPE(self)->tp_free(self);
+}
+
+PyObject*
+ZoneTableAccessor_iter(PyObject* po_self) {
+ s_ZoneTableAccessor* const self =
+ static_cast<s_ZoneTableAccessor*>(po_self);
+ try {
+ return (createZoneTableIteratorObject(self->cppobj->getIterator(),
+ po_self));
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unexpected exception");
+ return (NULL);
+ }
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef ZoneTableAccessor_methods[] = {
+ { NULL, NULL, 0, NULL }
+};
+
+const char* const ZoneTableAccessor_doc = "\
+An accessor to a zone table for a data source.\n\
+\n\
+This class object is intended to be used by applications that load zones\
+into memory, so that the application can get a list of zones to be loaded.";
+
+} // end anonymous namespace
+
+namespace isc {
+namespace datasrc {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_ZoneTableAccessor
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject zonetableaccessor_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "datasrc.ZoneTableAccessor",
+ sizeof(s_ZoneTableAccessor), // tp_basicsize
+ 0, // tp_itemsize
+ ZoneTableAccessor_destroy, // tp_dealloc
+ NULL, // tp_print
+ NULL, // tp_getattr
+ NULL, // tp_setattr
+ NULL, // tp_reserved
+ NULL, // tp_repr
+ NULL, // tp_as_number
+ NULL, // tp_as_sequence
+ NULL, // tp_as_mapping
+ NULL, // tp_hash
+ NULL, // tp_call
+ NULL, // tp_str
+ NULL, // tp_getattro
+ NULL, // tp_setattro
+ NULL, // tp_as_buffer
+ Py_TPFLAGS_DEFAULT, // tp_flags
+ ZoneTableAccessor_doc, // tp_doc
+ NULL, // tp_traverse
+ NULL, // tp_clear
+ NULL, // tp_richcompare
+ 0, // tp_weaklistoffset
+ ZoneTableAccessor_iter, // tp_iter
+ NULL, // tp_iternext
+ ZoneTableAccessor_methods, // tp_methods
+ NULL, // tp_members
+ NULL, // tp_getset
+ NULL, // tp_base
+ NULL, // tp_dict
+ NULL, // tp_descr_get
+ NULL, // tp_descr_set
+ 0, // tp_dictoffset
+ ZoneTableAccessor_init, // tp_init
+ NULL, // tp_alloc
+ PyType_GenericNew, // tp_new
+ NULL, // tp_free
+ NULL, // tp_is_gc
+ NULL, // tp_bases
+ NULL, // tp_mro
+ NULL, // tp_cache
+ NULL, // tp_subclasses
+ NULL, // tp_weaklist
+ NULL, // tp_del
+ 0 // tp_version_tag
+};
+
+PyObject*
+createZoneTableAccessorObject(isc::datasrc::ConstZoneTableAccessorPtr source,
+ PyObject* base_obj)
+{
+ s_ZoneTableAccessor* py_zt = static_cast<s_ZoneTableAccessor*>(
+ zonetableaccessor_type.tp_alloc(&zonetableaccessor_type, 0));
+ if (py_zt != NULL) {
+ py_zt->cppobj = source;
+ py_zt->base_obj = base_obj;
+ if (base_obj != NULL) {
+ Py_INCREF(base_obj);
+ }
+ }
+ return (py_zt);
+}
+
+} // namespace python
+} // namespace datasrc
+} // namespace isc
diff --git a/src/lib/python/isc/datasrc/zonetable_accessor_python.h b/src/lib/python/isc/datasrc/zonetable_accessor_python.h
new file mode 100644
index 0000000..6ebcd92
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonetable_accessor_python.h
@@ -0,0 +1,43 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PYTHON_ZONETABLE_ACCESSOR_H
+#define PYTHON_ZONETABLE_ACCESSOR_H 1
+
+#include <Python.h>
+
+namespace isc {
+namespace datasrc {
+namespace python {
+
+extern PyTypeObject zonetableaccessor_type;
+
+/// \brief Create a ZoneTableAccessor python object
+///
+/// \param source The zone iterator pointer to wrap
+/// \param base_obj An optional PyObject that this ZoneFinder depends on
+/// Its refcount is increased, and will be decreased when
+/// this zone iterator is destroyed, making sure that the
+/// base object is never destroyed before this zonefinder.
+PyObject* createZoneTableAccessorObject(
+ isc::datasrc::ConstZoneTableAccessorPtr source, PyObject* base_obj);
+
+} // namespace python
+} // namespace datasrc
+} // namespace isc
+#endif // PYTHON_ZONETABLE_ACCESSOR_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/python/isc/datasrc/zonetable_iterator_python.cc b/src/lib/python/isc/datasrc/zonetable_iterator_python.cc
new file mode 100644
index 0000000..fbf1ebf
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonetable_iterator_python.cc
@@ -0,0 +1,197 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <datasrc/zone_table_accessor.h>
+#include <dns/python/name_python.h>
+
+#include "datasrc.h"
+#include "zonetable_iterator_python.h"
+
+using namespace std;
+using namespace isc::datasrc;
+using namespace isc::datasrc::python;
+
+namespace {
+// The s_* Class simply covers one instantiation of the object
+class s_ZoneTableIterator : public PyObject {
+public:
+ s_ZoneTableIterator() :
+ cppobj(ZoneTableAccessor::IteratorPtr()), base_obj(NULL)
+ {};
+
+ ZoneTableAccessor::IteratorPtr cppobj;
+ // This is a reference to a base object; if the object of this class
+ // depends on another object to be in scope during its lifetime,
+ // we use INCREF the base object upon creation, and DECREF it at
+ // the end of the destructor
+ // This is an optional argument to createXXX(). If NULL, it is ignored.
+ PyObject* base_obj;
+};
+
+// General creation and destruction
+int
+ZoneTableIterator_init(s_ZoneTableIterator*, PyObject*) {
+ // can't be called directly
+ PyErr_SetString(PyExc_TypeError,
+ "ZoneTableIterator cannot be constructed directly");
+
+ return (-1);
+}
+
+void
+ZoneTableIterator_destroy(s_ZoneTableIterator* const self) {
+ // cppobj is a shared ptr, but to make sure things are not destroyed in
+ // the wrong order, we reset it here.
+ self->cppobj.reset();
+ if (self->base_obj != NULL) {
+ Py_DECREF(self->base_obj);
+ }
+ Py_TYPE(self)->tp_free(self);
+}
+
+//
+// We declare the functions here, the definitions are below
+// the type definition of the object, since both can use the other
+//
+PyObject*
+ZoneTableIterator_iter(PyObject *self) {
+ Py_INCREF(self);
+ return (self);
+}
+
+PyObject*
+ZoneTableIterator_next(PyObject* po_self) {
+ s_ZoneTableIterator* self = static_cast<s_ZoneTableIterator*>(po_self);
+ if (!self->cppobj || self->cppobj->isLast()) {
+ return (NULL);
+ }
+ try {
+ const isc::datasrc::ZoneSpec& zs = self->cppobj->getCurrent();
+ PyObject* result =
+ Py_BuildValue("iO", zs.index,
+ isc::dns::python::createNameObject(zs.origin));
+ self->cppobj->next();
+ return (result);
+ } catch (const std::exception& exc) {
+ // isc::InvalidOperation is thrown when we call getCurrent()
+ // when we are already done iterating ('iterating past end')
+ // We could also simply return None again
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unexpected exception");
+ return (NULL);
+ }
+}
+
+PyMethodDef ZoneTableIterator_methods[] = {
+ { NULL, NULL, 0, NULL }
+};
+
+const char* const ZoneTableIterator_doc = "\
+Read-only iterator to a zone table.\n\
+\n\
+You can get an instance of the ZoneTableIterator from the\
+ZoneTableAccessor.get_iterator() method.\n\
+\n\
+There's no way to start iterating from the beginning again or return.\n\
+\n\
+The ZoneTableIterator is a Python iterator, and can be iterated over\
+directly.\n\
+";
+
+} // end of unnamed namespace
+
+namespace isc {
+namespace datasrc {
+namespace python {
+PyTypeObject zonetableiterator_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "datasrc.ZoneTableIterator",
+ sizeof(s_ZoneTableIterator), // tp_basicsize
+ 0, // tp_itemsize
+ reinterpret_cast<destructor>(ZoneTableIterator_destroy),// tp_dealloc
+ NULL, // tp_print
+ NULL, // tp_getattr
+ NULL, // tp_setattr
+ NULL, // tp_reserved
+ NULL, // tp_repr
+ NULL, // tp_as_number
+ NULL, // tp_as_sequence
+ NULL, // tp_as_mapping
+ NULL, // tp_hash
+ NULL, // tp_call
+ NULL, // tp_str
+ NULL, // tp_getattro
+ NULL, // tp_setattro
+ NULL, // tp_as_buffer
+ Py_TPFLAGS_DEFAULT, // tp_flags
+ ZoneTableIterator_doc, // tp_doc
+ NULL, // tp_traverse
+ NULL, // tp_clear
+ NULL, // tp_richcompare
+ 0, // tp_weaklistoffset
+ ZoneTableIterator_iter, // tp_iter
+ ZoneTableIterator_next, // tp_iternext
+ ZoneTableIterator_methods, // tp_methods
+ NULL, // tp_members
+ NULL, // tp_getset
+ NULL, // tp_base
+ NULL, // tp_dict
+ NULL, // tp_descr_get
+ NULL, // tp_descr_set
+ 0, // tp_dictoffset
+ reinterpret_cast<initproc>(ZoneTableIterator_init),// tp_init
+ NULL, // tp_alloc
+ PyType_GenericNew, // tp_new
+ NULL, // tp_free
+ NULL, // tp_is_gc
+ NULL, // tp_bases
+ NULL, // tp_mro
+ NULL, // tp_cache
+ NULL, // tp_subclasses
+ NULL, // tp_weaklist
+ NULL, // tp_del
+ 0 // tp_version_tag
+};
+
+PyObject*
+createZoneTableIteratorObject(ZoneTableAccessor::IteratorPtr source,
+ PyObject* base_obj)
+{
+ s_ZoneTableIterator* py_zi = static_cast<s_ZoneTableIterator*>(
+ zonetableiterator_type.tp_alloc(&zonetableiterator_type, 0));
+ if (py_zi != NULL) {
+ py_zi->cppobj = source;
+ py_zi->base_obj = base_obj;
+ if (base_obj != NULL) {
+ Py_INCREF(base_obj);
+ }
+ }
+ return (py_zi);
+}
+
+} // namespace python
+} // namespace datasrc
+} // namespace isc
+
diff --git a/src/lib/python/isc/datasrc/zonetable_iterator_python.h b/src/lib/python/isc/datasrc/zonetable_iterator_python.h
new file mode 100644
index 0000000..887861e
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonetable_iterator_python.h
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PYTHON_ZONETABLE_ITERATOR_H
+#define PYTHON_ZONETABLE_ITERATOR_H 1
+
+#include <Python.h>
+
+namespace isc {
+namespace datasrc {
+namespace python {
+
+extern PyTypeObject zonetableiterator_type;
+
+/// \brief Create a ZoneTableIterator python object
+///
+/// \param source The zone table iterator pointer to wrap
+/// \param base_obj An optional PyObject that this ZoneIterator depends on
+/// Its refcount is increased, and will be decreased when
+/// this zone iterator is destroyed, making sure that the
+/// base object is never destroyed before this zone iterator.
+PyObject* createZoneTableIteratorObject(
+ isc::datasrc::ZoneTableAccessor::IteratorPtr source,
+ PyObject* base_obj = NULL);
+
+} // namespace python
+} // namespace datasrc
+} // namespace isc
+#endif // PYTHON_ZONETABLE_ITERATOR_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/python/isc/datasrc/zonewriter_inc.cc b/src/lib/python/isc/datasrc/zonewriter_inc.cc
new file mode 100644
index 0000000..1f10a9a
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonewriter_inc.cc
@@ -0,0 +1,103 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+namespace {
+
+const char* const ZoneWriter_doc = "\
+Does an update to a zone.\n\
+\n\
+This represents the work of a (re)load of a zone. The work is divided\n\
+into three stages load(), install() and cleanup(). They should be\n\
+called in this order for the effect to take place.\n\
+\n\
+We divide them so the update of zone data can be done asynchronously,\n\
+in a different thread. The install() operation is the only one that\n\
+needs to be done in a critical section.\n\
+\n\
+This class provides strong exception guarantee for each public method.\n\
+That is, when any of the methods throws, the entire state stays the\n\
+same as before the call.\n\
+\n\
+ZoneWriter objects cannot be constructed directly. They have to be\n\
+obtained by using get_cached_zone_writer() on a ConfigurableClientList.\n\
+\n\
+";
+
+const char* const ZoneWriter_load_doc = "\
+load() -> err_msg\n\
+\n\
+Get the zone data into memory.\n\
+\n\
+This is the part that does the time-consuming loading into the memory.\n\
+This can be run in a separate thread, for example. It has no effect on\n\
+the data actually served, it only prepares them for future use.\n\
+\n\
+This is the first method you should call on the object. Never call it\n\
+multiple times.\n\
+\n\
+If the zone loads successfully, this method returns None. In the case of\n\
+a load error, if the ZoneWriter was constructed with the\n\
+catch_load_error option (see\n\
+ConfigurableClientList.get_cached_zone_writer()), this method will\n\
+return an error message string; if it was created without the\n\
+catch_load_error option, this method will raise a DataSourceError\n\
+exception.\n\
+\n\
+Exceptions:\n\
+ isc.InvalidOperation if called second time.\n\
+ DataSourceError load related error (not thrown if constructed not to).\n\
+\n\
+";
+
+const char* const ZoneWriter_install_doc = "\
+install() -> void\n\
+\n\
+Put the changes to effect.\n\
+\n\
+This replaces the old version of zone with the one previously prepared\n\
+by load(). It takes ownership of the old zone data, if any.\n\
+\n\
+You may call it only after successful load() and at most once. It\n\
+includes the case the writer is constructed to allow load errors,\n\
+and load() encountered and caught a DataSourceError exception.\n\
+In this case this method installs a special empty zone to\n\
+the table.\n\
+\n\
+The operation is expected to be fast and is meant to be used inside a\n\
+critical section.\n\
+\n\
+This may throw in rare cases. If it throws, you still need to call\n\
+cleanup().\n\
+\n\
+Exceptions:\n\
+ isc.InvalidOperation if called without previous load() or for the\n\
+ second time or cleanup() was called already.\n\
+\n\
+";
+
+const char* const ZoneWriter_cleanup_doc = "\
+cleanup() -> void\n\
+\n\
+Clean up resources.\n\
+\n\
+This releases all resources held by owned zone data. That means the\n\
+one loaded by load() in case install() was not called or was not\n\
+successful, or the one replaced in install().\n\
+\n\
+Exceptions:\n\
+ none\n\
+\n\
+";
+
+} // unnamed namespace
diff --git a/src/lib/python/isc/datasrc/zonewriter_python.cc b/src/lib/python/isc/datasrc/zonewriter_python.cc
new file mode 100644
index 0000000..b3783e8
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonewriter_python.cc
@@ -0,0 +1,253 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+// Enable this if you use s# variants with PyArg_ParseTuple(), see
+// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers
+//#define PY_SSIZE_T_CLEAN
+
+// Python.h needs to be placed at the head of the program file, see:
+// http://docs.python.org/py3k/extending/extending.html#a-simple-example
+#include <Python.h>
+
+#include <string>
+#include <stdexcept>
+
+#include <util/python/pycppwrapper_util.h>
+
+#include <datasrc/memory/zone_writer.h>
+
+#include "zonewriter_python.h"
+#include "datasrc.h"
+
+#include "zonewriter_inc.cc"
+
+using namespace std;
+using namespace isc::util::python;
+using namespace isc::datasrc;
+using namespace isc::datasrc::memory;
+using namespace isc::datasrc::python;
+using namespace isc::datasrc::memory::python;
+
+//
+// ZoneWriter
+//
+
+namespace {
+
+// The s_* Class simply covers one instantiation of the object
+class s_ZoneWriter : public PyObject {
+public:
+ s_ZoneWriter() :
+ cppobj(ConfigurableClientList::ZoneWriterPtr()),
+ base_obj(NULL)
+ {}
+
+ ConfigurableClientList::ZoneWriterPtr cppobj;
+ // This is a reference to a base object; if the object of this class
+ // depends on another object to be in scope during its lifetime,
+ // we use INCREF the base object upon creation, and DECREF it at
+ // the end of the destructor
+ // This is an optional argument to createXXX(). If NULL, it is ignored.
+ PyObject* base_obj;
+};
+
+int
+ZoneWriter_init(PyObject*, PyObject*, PyObject*) {
+ // can't be called directly
+ PyErr_SetString(PyExc_TypeError,
+ "ZoneWriter cannot be constructed directly");
+
+ return (-1);
+}
+
+void
+ZoneWriter_destroy(PyObject* po_self) {
+ s_ZoneWriter* self = static_cast<s_ZoneWriter*>(po_self);
+ // cppobj is a shared ptr, but to make sure things are not destroyed in
+ // the wrong order, we reset it here.
+ self->cppobj.reset();
+ if (self->base_obj != NULL) {
+ Py_DECREF(self->base_obj);
+ }
+ Py_TYPE(self)->tp_free(self);
+}
+
+PyObject*
+ZoneWriter_load(PyObject* po_self, PyObject*) {
+ s_ZoneWriter* self = static_cast<s_ZoneWriter*>(po_self);
+ try {
+ std::string error_msg;
+ self->cppobj->load(&error_msg);
+ if (!error_msg.empty()) {
+ return (Py_BuildValue("s", error_msg.c_str()));
+ }
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+
+ Py_RETURN_NONE;
+}
+
+PyObject*
+ZoneWriter_install(PyObject* po_self, PyObject*) {
+ s_ZoneWriter* self = static_cast<s_ZoneWriter*>(po_self);
+ try {
+ self->cppobj->install();
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+
+ Py_RETURN_NONE;
+}
+
+PyObject*
+ZoneWriter_cleanup(PyObject* po_self, PyObject*) {
+ s_ZoneWriter* self = static_cast<s_ZoneWriter*>(po_self);
+ try {
+ self->cppobj->cleanup();
+ } catch (const std::exception& exc) {
+ PyErr_SetString(getDataSourceException("Error"), exc.what());
+ return (NULL);
+ } catch (...) {
+ PyErr_SetString(getDataSourceException("Error"),
+ "Unknown C++ exception");
+ return (NULL);
+ }
+
+ Py_RETURN_NONE;
+}
+
+// This list contains the actual set of functions we have in
+// python. Each entry has
+// 1. Python method name
+// 2. Our static function here
+// 3. Argument type
+// 4. Documentation
+PyMethodDef ZoneWriter_methods[] = {
+ { "load", ZoneWriter_load, METH_NOARGS,
+ ZoneWriter_load_doc },
+ { "install", ZoneWriter_install, METH_NOARGS,
+ ZoneWriter_install_doc },
+ { "cleanup", ZoneWriter_cleanup, METH_NOARGS,
+ ZoneWriter_cleanup_doc },
+ { NULL, NULL, 0, NULL }
+};
+
+} // end of unnamed namespace
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+namespace python {
+// This defines the complete type for reflection in python and
+// parsing of PyObject* to s_ZoneWriter
+// Most of the functions are not actually implemented and NULL here.
+PyTypeObject zonewriter_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "datasrc.ZoneWriter",
+ sizeof(s_ZoneWriter), // tp_basicsize
+ 0, // tp_itemsize
+ ZoneWriter_destroy, // tp_dealloc
+ NULL, // tp_print
+ NULL, // tp_getattr
+ NULL, // tp_setattr
+ NULL, // tp_reserved
+ NULL, // tp_repr
+ NULL, // tp_as_number
+ NULL, // tp_as_sequence
+ NULL, // tp_as_mapping
+ NULL, // tp_hash
+ NULL, // tp_call
+ NULL, // tp_str
+ NULL, // tp_getattro
+ NULL, // tp_setattro
+ NULL, // tp_as_buffer
+ Py_TPFLAGS_DEFAULT, // tp_flags
+ ZoneWriter_doc,
+ NULL, // tp_traverse
+ NULL, // tp_clear
+ NULL, // tp_richcompare
+ 0, // tp_weaklistoffset
+ NULL, // tp_iter
+ NULL, // tp_iternext
+ ZoneWriter_methods, // tp_methods
+ NULL, // tp_members
+ NULL, // tp_getset
+ NULL, // tp_base
+ NULL, // tp_dict
+ NULL, // tp_descr_get
+ NULL, // tp_descr_set
+ 0, // tp_dictoffset
+ ZoneWriter_init, // tp_init
+ NULL, // tp_alloc
+ PyType_GenericNew, // tp_new
+ NULL, // tp_free
+ NULL, // tp_is_gc
+ NULL, // tp_bases
+ NULL, // tp_mro
+ NULL, // tp_cache
+ NULL, // tp_subclasses
+ NULL, // tp_weaklist
+ NULL, // tp_del
+ 0 // tp_version_tag
+};
+
+// Module Initialization, all statics are initialized here
+bool
+initModulePart_ZoneWriter(PyObject* mod) {
+ // We initialize the static description object with PyType_Ready(),
+ // then add it to the module. This is not just a check! (leaving
+ // this out results in segmentation faults)
+ if (PyType_Ready(&zonewriter_type) < 0) {
+ return (false);
+ }
+ void* p = &zonewriter_type;
+ if (PyModule_AddObject(mod, "ZoneWriter", static_cast<PyObject*>(p)) < 0) {
+ return (false);
+ }
+ Py_INCREF(&zonewriter_type);
+
+ return (true);
+}
+
+PyObject*
+createZoneWriterObject(ConfigurableClientList::ZoneWriterPtr source,
+ PyObject* base_obj)
+{
+ s_ZoneWriter* py_zf = static_cast<s_ZoneWriter*>(
+ zonewriter_type.tp_alloc(&zonewriter_type, 0));
+ if (py_zf != NULL) {
+ py_zf->cppobj = source;
+ py_zf->base_obj = base_obj;
+ if (base_obj != NULL) {
+ Py_INCREF(base_obj);
+ }
+ }
+ return (py_zf);
+}
+
+} // namespace python
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
diff --git a/src/lib/python/isc/datasrc/zonewriter_python.h b/src/lib/python/isc/datasrc/zonewriter_python.h
new file mode 100644
index 0000000..a7c97b3
--- /dev/null
+++ b/src/lib/python/isc/datasrc/zonewriter_python.h
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PYTHON_ZONEWRITER_H
+#define PYTHON_ZONEWRITER_H 1
+
+#include <Python.h>
+#include <datasrc/client_list.h>
+
+namespace isc {
+namespace datasrc {
+namespace memory {
+namespace python {
+
+extern PyTypeObject zonewriter_type;
+
+bool initModulePart_ZoneWriter(PyObject* mod);
+
+/// \brief Create a ZoneWriter python object
+///
+/// \param source The zone writer pointer to wrap
+/// \param base_obj An optional PyObject that this ZoneWriter depends on
+/// Its refcount is increased, and will be decreased when
+/// this zone iterator is destroyed, making sure that the
+/// base object is never destroyed before this ZoneWriter.
+PyObject* createZoneWriterObject(
+ isc::datasrc::ConfigurableClientList::ZoneWriterPtr source,
+ PyObject* base_obj = NULL);
+
+} // namespace python
+} // namespace memory
+} // namespace datasrc
+} // namespace isc
+
+#endif // PYTHON_ZONEWRITER_H
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/python/isc/ddns/libddns_messages.mes b/src/lib/python/isc/ddns/libddns_messages.mes
index ab982ad..86af4c3 100644
--- a/src/lib/python/isc/ddns/libddns_messages.mes
+++ b/src/lib/python/isc/ddns/libddns_messages.mes
@@ -212,3 +212,13 @@ To make sure DDNS service is not interrupted, this problem is caught instead
of reraised; The update is aborted, and a SERVFAIL is sent back to the client.
This is most probably a bug in the DDNS code, but *could* be caused by
the data source.
+
+% LIBDDNS_ZONE_INVALID_ERROR Newly received zone data %1/%2 fails validation: %3
+The zone data was received successfully, but the zone when updated with
+this zone data fails validation.
+
+% LIBDDNS_ZONE_INVALID_WARN Newly received zone data %1/%2 has a problem: %3
+The zone data was received successfully, but when checking the zone when
+updated with this zone data, it was discovered there's some issue with
+it. It might be correct, but it should be checked and possibly
+fixed. The problem does not stop the zone from being used.
diff --git a/src/lib/python/isc/ddns/session.py b/src/lib/python/isc/ddns/session.py
index febdfdc..6f2b7cf 100644
--- a/src/lib/python/isc/ddns/session.py
+++ b/src/lib/python/isc/ddns/session.py
@@ -812,6 +812,20 @@ class UpdateSession:
self.__diff.delete_data(old_soa)
self.__diff.add_data(new_soa)
+ def __validate_error(self, reason):
+ '''
+ Used as error callback below.
+ '''
+ logger.error(LIBDDNS_ZONE_INVALID_ERROR, self.__zname, self.__zclass,
+ reason)
+
+ def __validate_warning(self, reason):
+ '''
+ Used as warning callback below.
+ '''
+ logger.warn(LIBDDNS_ZONE_INVALID_WARN, self.__zname, self.__zclass,
+ reason)
+
def __do_update(self):
'''Scan, check, and execute the Update section in the
DDNS Update message.
@@ -849,8 +863,17 @@ class UpdateSession:
elif rrset.get_class() == RRClass.NONE:
self.__do_update_delete_rrs_from_rrset(rrset)
+ if not check_zone(self.__zname, self.__zclass,
+ self.__diff.get_rrset_collection(),
+ (self.__validate_error, self.__validate_warning)):
+ raise UpdateError('Validation of the new zone failed',
+ self.__zname, self.__zclass, Rcode.REFUSED)
self.__diff.commit()
return Rcode.NOERROR
+ except UpdateError:
+ # Propagate UpdateError exceptions (don't catch them in the
+ # blocks below)
+ raise
except isc.datasrc.Error as dse:
logger.info(LIBDDNS_UPDATE_DATASRC_COMMIT_FAILED, dse)
return Rcode.SERVFAIL
diff --git a/src/lib/python/isc/ddns/tests/session_tests.py b/src/lib/python/isc/ddns/tests/session_tests.py
index 4880d72..49bf672 100644
--- a/src/lib/python/isc/ddns/tests/session_tests.py
+++ b/src/lib/python/isc/ddns/tests/session_tests.py
@@ -1481,6 +1481,16 @@ class SessionTest(SessionTestBase):
self.assertEqual(Rcode.SERVFAIL.to_text(),
self._session._UpdateSession__do_update().to_text())
+ def test_check_zone_failure(self):
+ # ns3.example.org. is an NS and should not have a CNAME
+ # record. This would cause check_zone() to fail.
+ self.__initialize_update_rrsets()
+ new_cname = create_rrset("ns3.example.org", TEST_RRCLASS,
+ RRType.CNAME, 3600,
+ [ "cname.example.org." ])
+
+ self.check_full_handle_result(Rcode.REFUSED, [ new_cname ])
+
class SessionACLTest(SessionTestBase):
'''ACL related tests for update session.'''
def test_update_acl_check(self):
diff --git a/src/lib/python/isc/log_messages/Makefile.am b/src/lib/python/isc/log_messages/Makefile.am
index c8b9c7a..eab82aa 100644
--- a/src/lib/python/isc/log_messages/Makefile.am
+++ b/src/lib/python/isc/log_messages/Makefile.am
@@ -4,6 +4,8 @@ EXTRA_DIST = __init__.py
EXTRA_DIST += init_messages.py
EXTRA_DIST += cmdctl_messages.py
EXTRA_DIST += ddns_messages.py
+EXTRA_DIST += libmemmgr_messages.py
+EXTRA_DIST += memmgr_messages.py
EXTRA_DIST += stats_messages.py
EXTRA_DIST += stats_httpd_messages.py
EXTRA_DIST += xfrin_messages.py
@@ -19,11 +21,14 @@ EXTRA_DIST += server_common_messages.py
EXTRA_DIST += dbutil_messages.py
EXTRA_DIST += msgq_messages.py
EXTRA_DIST += pycc_messages.py
+EXTRA_DIST += util_messages.py
CLEANFILES = __init__.pyc
CLEANFILES += init_messages.pyc
CLEANFILES += cmdctl_messages.pyc
CLEANFILES += ddns_messages.pyc
+CLEANFILES += libmemmgr_messages.pyc
+CLEANFILES += memmgr_messages.pyc
CLEANFILES += stats_messages.pyc
CLEANFILES += stats_httpd_messages.pyc
CLEANFILES += xfrin_messages.pyc
@@ -39,6 +44,7 @@ CLEANFILES += server_common_messages.pyc
CLEANFILES += dbutil_messages.pyc
CLEANFILES += msgq_messages.pyc
CLEANFILES += pycc_messages.pyc
+CLEANFILES += util_messages.pyc
CLEANDIRS = __pycache__
diff --git a/src/lib/python/isc/log_messages/libmemmgr_messages.py b/src/lib/python/isc/log_messages/libmemmgr_messages.py
new file mode 100644
index 0000000..3aedc3f
--- /dev/null
+++ b/src/lib/python/isc/log_messages/libmemmgr_messages.py
@@ -0,0 +1 @@
+from work.libmemmgr_messages import *
diff --git a/src/lib/python/isc/log_messages/memmgr_messages.py b/src/lib/python/isc/log_messages/memmgr_messages.py
new file mode 100644
index 0000000..8c59cc9
--- /dev/null
+++ b/src/lib/python/isc/log_messages/memmgr_messages.py
@@ -0,0 +1 @@
+from work.memmgr_messages import *
diff --git a/src/lib/python/isc/log_messages/util_messages.py b/src/lib/python/isc/log_messages/util_messages.py
new file mode 100644
index 0000000..4298729
--- /dev/null
+++ b/src/lib/python/isc/log_messages/util_messages.py
@@ -0,0 +1 @@
+from work.util_messages import *
diff --git a/src/lib/python/isc/log_messages/work/Makefile.am b/src/lib/python/isc/log_messages/work/Makefile.am
index ad5ee0c..b49ce69 100644
--- a/src/lib/python/isc/log_messages/work/Makefile.am
+++ b/src/lib/python/isc/log_messages/work/Makefile.am
@@ -8,5 +8,7 @@ pythondir = $(pyexecdir)/isc/log_messages/
CLEANFILES = __init__.pyc __init__.pyo
CLEANDIRS = __pycache__
+EXTRA_DIST = README
+
clean-local:
rm -rf $(CLEANDIRS)
diff --git a/src/lib/python/isc/log_messages/work/README b/src/lib/python/isc/log_messages/work/README
new file mode 100644
index 0000000..37b4534
--- /dev/null
+++ b/src/lib/python/isc/log_messages/work/README
@@ -0,0 +1,5 @@
+The __init__.py.in in this directory is meant to be processed by
+configure so that the generated __init__.py ends up in the builddir, and
+not the srcdir. This is because Python associates a module with a
+directory, and you can't have portions of the module in two separate
+directories.
diff --git a/src/lib/python/isc/memmgr/Makefile.am b/src/lib/python/isc/memmgr/Makefile.am
new file mode 100644
index 0000000..5529570
--- /dev/null
+++ b/src/lib/python/isc/memmgr/Makefile.am
@@ -0,0 +1,25 @@
+SUBDIRS = . tests
+
+python_PYTHON = __init__.py builder.py datasrc_info.py logger.py
+
+pythondir = $(pyexecdir)/isc/memmgr
+
+BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/libmemmgr_messages.py
+
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/libmemmgr_messages.py
+
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
+
+CLEANFILES = $(PYTHON_LOGMSGPKG_DIR)/work/libmemmgr_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/libmemmgr_messages.pyc
+
+EXTRA_DIST = libmemmgr_messages.mes
+
+$(PYTHON_LOGMSGPKG_DIR)/work/libmemmgr_messages.py : libmemmgr_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message \
+ -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/libmemmgr_messages.mes
+
+CLEANDIRS = __pycache__
+
+clean-local:
+ rm -rf $(CLEANDIRS)
diff --git a/src/lib/python/isc/memmgr/__init__.py b/src/lib/python/isc/memmgr/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/python/isc/memmgr/builder.py b/src/lib/python/isc/memmgr/builder.py
new file mode 100644
index 0000000..9c3738e
--- /dev/null
+++ b/src/lib/python/isc/memmgr/builder.py
@@ -0,0 +1,185 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import json
+from isc.datasrc import ConfigurableClientList
+from isc.memmgr.datasrc_info import SegmentInfo
+
+from isc.log_messages.libmemmgr_messages import *
+from isc.memmgr.logger import logger
+
+class MemorySegmentBuilder:
+ """The builder runs in a different thread in the memory manager. It
+ waits for commands from the memory manager, and then executes them
+ in the given order sequentially.
+ """
+
+ def __init__(self, sock, cv, command_queue, response_queue):
+ """ The constructor takes the following arguments:
+
+ sock: A socket using which this builder object notifies the
+ main thread that it has a response waiting for it.
+
+ cv: A condition variable object that is used by the main
+ thread to tell this builder object that new commands are
+ available to it. Note that this is also used for
+ synchronizing access to the queues, so code that uses
+ MemorySegmentBuilder must use this condition variable's
+ lock object to synchronize its access to the queues.
+
+ command_queue: A list of commands sent by the main thread to
+ this object. Commands should be executed
+ sequentially in the given order by this
+ object.
+
+ response_queue: A list of responses sent by this object to
+ the main thread. The format of this is
+ currently not strictly defined. Future
+ tickets will be able to define it based on
+ how it's used.
+ """
+
+ self._sock = sock
+ self._cv = cv
+ self._command_queue = command_queue
+ self._response_queue = response_queue
+ self._shutdown = False
+
+ def __handle_shutdown(self):
+ # This method is called when handling the 'shutdown' command. The
+ # following tuple is passed:
+ #
+ # ('shutdown',)
+ self._shutdown = True
+
+ def __handle_bad_command(self, bad_command):
+ # A bad command was received. Raising an exception is not useful
+ # in this case as we are likely running in a different thread
+ # from the main thread which would need to be notified. Instead
+ # return this in the response queue.
+ logger.error(LIBMEMMGR_BUILDER_BAD_COMMAND_ERROR, bad_command)
+ self._response_queue.append(('bad_command',))
+ self._shutdown = True
+
+ def __handle_load(self, zone_name, dsrc_info, rrclass, dsrc_name):
+ # This method is called when handling the 'load' command. The
+ # following tuple is passed:
+ #
+ # ('load', zone_name, dsrc_info, rrclass, dsrc_name)
+ #
+ # where:
+ #
+ # * zone_name is None or isc.dns.Name, specifying the zone name
+ # to load. If it's None, it means all zones to be cached in
+ # the specified data source (used for initialization).
+ #
+ # * dsrc_info is a DataSrcInfo object corresponding to the
+ # generation ID of the set of data sources for this loading.
+ #
+ # * rrclass is an isc.dns.RRClass object, the RR class of the
+ # data source.
+ #
+ # * dsrc_name is a string, specifying a data source name.
+
+ clist = dsrc_info.clients_map[rrclass]
+ sgmt_info = dsrc_info.segment_info_map[(rrclass, dsrc_name)]
+ params = json.dumps(sgmt_info.get_reset_param(SegmentInfo.WRITER))
+ clist.reset_memory_segment(dsrc_name,
+ ConfigurableClientList.READ_WRITE,
+ params)
+
+ if zone_name is not None:
+ zones = [(None, zone_name)]
+ else:
+ zones = clist.get_zone_table_accessor(dsrc_name, True)
+
+ for _, zone_name in zones:
+ catch_load_error = (zone_name is None) # install empty zone initially
+ result, writer = clist.get_cached_zone_writer(zone_name, catch_load_error,
+ dsrc_name)
+ if result != ConfigurableClientList.CACHE_STATUS_ZONE_SUCCESS:
+ logger.error(LIBMEMMGR_BUILDER_GET_ZONE_WRITER_ERROR, zone_name, dsrc_name)
+ continue
+
+ try:
+ error = writer.load()
+ if error is not None:
+ logger.error(LIBMEMMGR_BUILDER_ZONE_WRITER_LOAD_1_ERROR, zone_name, dsrc_name, error)
+ continue
+ except Exception as e:
+ logger.error(LIBMEMMGR_BUILDER_ZONE_WRITER_LOAD_2_ERROR, zone_name, dsrc_name, str(e))
+ continue
+ writer.install()
+ writer.cleanup()
+
+ # need to reset the segment so readers can read it (note: memmgr
+ # itself doesn't have to keep it open, but there's currently no
+ # public API to just clear the segment)
+ clist.reset_memory_segment(dsrc_name,
+ ConfigurableClientList.READ_ONLY,
+ params)
+
+ self._response_queue.append(('load-completed', dsrc_info, rrclass,
+ dsrc_name))
+
+ def run(self):
+ """ This is the method invoked when the builder thread is
+ started. In this thread, be careful when modifying
+ variables passed-by-reference in the constructor. If they
+ are reassigned, they will not refer to the main thread's
+ objects any longer. Any use of command_queue and
+ response_queue must be synchronized by acquiring the lock in
+ the condition variable. This method must normally terminate
+ only when the 'shutdown' command is sent to it.
+ """
+
+ # Acquire the condition variable while running the loop.
+ with self._cv:
+ while not self._shutdown:
+ while not self._command_queue:
+ self._cv.wait()
+ # Move the queue content to a local queue. Be careful of
+ # not making assignments to reference variables.
+ local_command_queue = self._command_queue[:]
+ del self._command_queue[:]
+
+ # Run commands passed in the command queue sequentially
+ # in the given order. For now, it only supports the
+ # "shutdown" command, which just exits the thread.
+ for command_tuple in local_command_queue:
+ command = command_tuple[0]
+ if command == 'load':
+ # See the comments for __handle_load() for
+ # details of the tuple passed to the "load"
+ # command.
+ _, zone_name, dsrc_info, rrclass, dsrc_name = command_tuple
+ self.__handle_load(zone_name, dsrc_info, rrclass, dsrc_name)
+ elif command == 'shutdown':
+ self.__handle_shutdown()
+ # When the shutdown command is received, we do
+ # not process any further commands.
+ break
+ else:
+ self.__handle_bad_command(command)
+ # When a bad command is received, we do not
+ # process any further commands.
+ break
+
+ # Notify (any main thread) on the socket about a
+ # response. Otherwise, the main thread may wait in its
+ # loop without knowing there was a problem.
+ if self._response_queue:
+ while self._sock.send(b'x') != 1:
+ continue
diff --git a/src/lib/python/isc/memmgr/datasrc_info.py b/src/lib/python/isc/memmgr/datasrc_info.py
new file mode 100644
index 0000000..db4a515
--- /dev/null
+++ b/src/lib/python/isc/memmgr/datasrc_info.py
@@ -0,0 +1,413 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import os
+from collections import deque
+
+class SegmentInfoError(Exception):
+ """An exception raised for general errors in the SegmentInfo class."""
+ pass
+
+class SegmentInfo:
+ """A base class to maintain information about memory segments.
+
+ An instance of this class corresponds to the memory segment used
+ for in-memory cache of a specific single data source. It manages
+ information to set/reset the latest effective segment (such as
+ path to a memory mapped file) and sets of other modules using the
+ segment.
+
+ Since there can be several different types of memory segments,
+ the top level class provides abstract interfaces independent from
+ segment-type specific details. Such details are expected to be
+ delegated to subclasses corresponding to specific types of segments.
+
+ A summarized (and simplified) state transition diagram (for __state)
+ would be as follows:
+ +--sync_reader()/remove_reader()
+ | still have old readers
+ | |
+ UPDATING-----complete_--->SYNCHRONIZING<---+
+ ^ update() |
+ start_update()| | sync_reader()/remove_reader()
+ events | V no more old reader
+ exist READY<------complete_----------COPYING
+ update()
+
+ """
+ # Common constants of user type: reader or writer
+ READER = 0
+ WRITER = 1
+
+ # Enumerated values for state:
+ UPDATING = 0 # the segment is being updated (by the builder thread,
+ # although SegmentInfo won't care about this level of
+ # details).
+ SYNCHRONIZING = 1 # one pair of underlying segments has been
+ # updated, and readers are now migrating to the
+ # updated version of the segment.
+ COPYING = 2 # all readers that used the old version of segment have
+ # been migrated to the updated version, and the old
+ # segment is now being updated.
+ READY = 3 # both segments of the pair have been updated. it can now
+ # handle further updates (e.g., from xfrin).
+
+ def __init__(self):
+ # Holds the state of SegmentInfo. See the class description
+ # above for the state transition diagram.
+ self.__state = self.READY
+ # __readers is a set of 'reader_session_id' private to
+ # SegmentInfo. It consists of the (ID of) reader modules that
+ # are using the "current" reader version of the segment.
+ self.__readers = set()
+ # __old_readers is a set of 'reader_session_id' private to
+ # SegmentInfo for write (update), but publicly readable. It can
+ # be non empty only in the SYNCHRONIZING state, and consists of
+ # (ID of) reader modules that are using the old version of the
+ # segments (and have to migrate to the updated version).
+ self.__old_readers = set()
+ # __events is a FIFO queue of opaque data for pending update
+ # events. Update events can come at any time (e.g., after
+ # xfr-in), but can be only handled if SegmentInfo is in the
+ # READY state. This maintains such pending events in the order
+ # they arrived. SegmentInfo doesn't have to know the details of
+ # the stored data; it only matters for the memmgr.
+ self.__events = deque()
+
+ def get_state(self):
+ """Returns the state of SegmentInfo (UPDATING, SYNCHRONIZING,
+ COPYING or READY)."""
+ return self.__state
+
+ def get_readers(self):
+ """Returns a set of IDs of the reader modules that are using the
+ "current" reader version of the segment. This method is mainly
+ useful for testing purposes."""
+ return self.__readers
+
+ def get_old_readers(self):
+ """Returns a set of IDs of reader modules that are using the old
+ version of the segments and have to be migrated to the updated
+ version."""
+ return self.__old_readers
+
+ def get_events(self):
+ """Returns a list of pending events in the order they arrived."""
+ return list(self.__events)
+
+ # Helper method used in complete_update(), sync_reader() and
+ # remove_reader().
+ def __sync_reader_helper(self, new_state):
+ if not self.__old_readers:
+ self.__state = new_state
+ if self.__events:
+ return self.__events.popleft()
+
+ return None
+
+ def add_event(self, event_data):
+ """Add an event to the end of the pending events queue. The
+ event_data is not used internally by this class, and is returned
+ as-is by other methods. The format of event_data only matters in
+ the memmgr. This method must be called by memmgr when it
+ receives a request for reloading a zone. No state transition
+ happens."""
+ self.__events.append(event_data)
+
+ def add_reader(self, reader_session_id):
+ """Add the reader module ID to an internal set of reader modules
+ that are using the "current" reader version of the segment. It
+ must be called by memmgr when it first gets the pre-existing
+ readers or when it's notified of a new reader. No state
+ transition happens.
+
+ When the SegmentInfo is not in the READY state, if memmgr gets
+ notified of a new reader (such as b10-auth) subscribing to the
+ readers group and calls add_reader(), we assume the new reader
+ is using the new mapped file and not the old one. For making
+ sure there is no race, memmgr should make SegmentInfo updates in
+ the main thread itself (which also handles communications) and
+ only have the builder in a different thread."""
+ if reader_session_id in self.__readers:
+ raise SegmentInfoError('Reader session ID is already in readers set: ' +
+ str(reader_session_id))
+
+ self.__readers.add(reader_session_id)
+
+ def start_update(self):
+ """If the current state is READY and there are pending events,
+ it changes the state to UPDATING and returns the head (oldest)
+ event (without removing it from the pending events queue). This
+ tells the caller (memmgr) that it should initiate the update
+ process with the builder. In all other cases it returns None."""
+ if self.__state == self.READY:
+ if self.__events:
+ self.__state = self.UPDATING
+ return self.__events[0]
+ else:
+ return None
+
+ raise SegmentInfoError('start_update() called in ' +
+ 'incorrect state: ' + str(self.__state))
+
+ def complete_update(self):
+ """This method should be called when memmgr is notified by the
+ builder of the completion of segment update. It changes the
+ state from UPDATING to SYNCHRONIZING, and COPYING to READY. In
+ the former case, set of reader modules that are using the
+ "current" reader version of the segment are moved to the set
+ that are using an "old" version of segment. If there are no such
+ readers using the "old" version of segment, it pops the head
+ (oldest) event from the pending events queue and returns it. It
+ is an error if this method is called in other states than
+ UPDATING and COPYING."""
+ if self.__state == self.UPDATING:
+ self.__state = self.SYNCHRONIZING
+ self.__old_readers = self.__readers
+ self.__readers = set()
+ return self.__sync_reader_helper(self.READY)
+ elif self.__state == self.COPYING:
+ self.__state = self.READY
+ return None
+ else:
+ raise SegmentInfoError('complete_update() called in ' +
+ 'incorrect state: ' + str(self.__state))
+
+ def sync_reader(self, reader_session_id):
+ """This method must only be called in the SYNCHRONIZING
+ state. memmgr should call it when it receives the
+ "segment_update_ack" message from a reader module. It moves the
+ given ID from the set of reader modules that are using the "old"
+ version of the segment to the set of reader modules that are
+ using the "current" version of the segment, and if there are no
+ reader modules using the "old" version of the segment, the state
+ is changed to COPYING. If the state has changed to COPYING, it
+ pops the head (oldest) event from the pending events queue and
+ returns it; otherwise it returns None."""
+ if self.__state != self.SYNCHRONIZING:
+ raise SegmentInfoError('sync_reader() called in ' +
+ 'incorrect state: ' + str(self.__state))
+ if reader_session_id not in self.__old_readers:
+ raise SegmentInfoError('Reader session ID is not in old readers set: ' +
+ str(reader_session_id))
+ if reader_session_id in self.__readers:
+ raise SegmentInfoError('Reader session ID is already in readers set: ' +
+ str(reader_session_id))
+
+ self.__old_readers.remove(reader_session_id)
+ self.__readers.add(reader_session_id)
+
+ return self.__sync_reader_helper(self.COPYING)
+
+ def remove_reader(self, reader_session_id):
+ """This method must only be called in the SYNCHRONIZING
+ state. memmgr should call it when it's notified that an existing
+ reader has unsubscribed. It removes the given reader ID from
+ either the set of readers that use the "current" version of the
+ segment or the "old" version of the segment (wherever the reader
+ belonged), and in the latter case, if there are no reader
+ modules using the "old" version of the segment, the state is
+ changed to COPYING. If the state has changed to COPYING, it pops
+ the head (oldest) event from the pending events queue and
+ returns it; otherwise it returns None."""
+ if self.__state != self.SYNCHRONIZING:
+ raise SegmentInfoError('remove_reader() called in ' +
+ 'incorrect state: ' + str(self.__state))
+ if reader_session_id in self.__old_readers:
+ self.__old_readers.remove(reader_session_id)
+ return self.__sync_reader_helper(self.COPYING)
+ elif reader_session_id in self.__readers:
+ self.__readers.remove(reader_session_id)
+ return None
+ else:
+ raise SegmentInfoError('Reader session ID is not in current ' +
+ 'readers or old readers set: ' +
+ str(reader_session_id))
+
+ def create(type, genid, rrclass, datasrc_name, mgr_config):
+ """Factory of specific SegmentInfo subclass instance based on the
+ segment type.
+
+ This is specifically for the memmgr, and segments that are not of
+ its interest will be ignored. This method returns None in these
+ cases. At least 'local' type segments will be ignored this way.
+
+ If an unknown type of segment is specified, this method throws an
+ SegmentInfoError exception. The assumption is that this method
+ is called after the corresponding data source configuration has been
+ validated, at which point such unknown segments should have been
+ rejected.
+
+ Parameters:
+ type (str or None): The type of memory segment; None if the segment
+ isn't used.
+ genid (int): The generation ID of the corresponding data source
+ configuration.
+ rrclass (isc.dns.RRClass): The RR class of the data source.
+ datasrc_name (str): The name of the data source.
+ mgr_config (dict): memmgr configuration related to memory segment
+ information. The content of the dict is type specific; each
+ subclass is expected to know which key is necessary and the
+ semantics of its value.
+
+ """
+ if type == 'mapped':
+ return MappedSegmentInfo(genid, rrclass, datasrc_name, mgr_config)
+ elif type is None or type == 'local':
+ return None
+ raise SegmentInfoError('unknown segment type to create info: ' + type)
+
+ def get_reset_param(self, user_type):
+ """Return parameters to reset the zone table memory segment.
+
+ It returns a dict object that consists of parameter mappings
+ (string to parameter value) for the specified type of user to
+ reset a zone table segment with
+ isc.datasrc.ConfigurableClientList.reset_memory_segment(). It
+ can also be passed to the user module as part of command
+ parameters. Note that reset_memory_segment() takes a json
+ expression encoded as a string, so the return value of this method
+ will have to be converted with json.dumps().
+
+ Each subclass must implement this method.
+
+ Parameter:
+ user_type (READER or WRITER): specifies the type of user to reset
+ the segment.
+
+ """
+ raise SegmentInfoError('get_reset_param is not implemented')
+
+ def switch_versions(self):
+ """Switch internal information for the reader segment and writer
+ segment.
+
+ This method is expected to be called when the writer on one version
+ of memory segment completes updates and the memmgr is going to
+ have readers switch to the updated version. Details of the
+ information to be switched would depend on the segment type, and
+ are delegated to the specific subclass.
+
+ Each subclass must implement this method.
+
+ """
+ raise SegmentInfoError('switch_versions is not implemented')
+
+class MappedSegmentInfo(SegmentInfo):
+ """SegmentInfo implementation of 'mapped' type memory segments.
+
+ It maintains paths to mapped files both readers and the writer.
+
+ While objets of this class are expected to be shared by multiple
+ threads, it assumes operations are serialized through message passing,
+ so access to this class itself is not protected by any explicit
+ synchronization mechanism.
+
+ """
+ def __init__(self, genid, rrclass, datasrc_name, mgr_config):
+ super().__init__()
+
+ # Something like "/var/bind10/zone-IN-1-sqlite3-mapped"
+ self.__mapped_file_base = mgr_config['mapped_file_dir'] + os.sep + \
+ 'zone-' + str(rrclass) + '-' + str(genid) + '-' + datasrc_name + \
+ '-mapped'
+
+ # Current versions (suffix of the mapped files) for readers and the
+ # writer. In this initial implementation we assume that all possible
+ # readers are waiting for a new version (not using pre-existing one),
+ # and the writer is expected to build a new segment as version "0".
+ self.__reader_ver = None # => 0 => 1 => 0 => 1 ...
+ self.__writer_ver = 0 # => 1 => 0 => 1 => 0 ...
+
+ def get_reset_param(self, user_type):
+ ver = self.__reader_ver if user_type == self.READER else \
+ self.__writer_ver
+ if ver is None:
+ return None
+ mapped_file = self.__mapped_file_base + '.' + str(ver)
+ return {'mapped-file': mapped_file}
+
+ def switch_versions(self):
+ # Swith the versions as noted in the constructor.
+ self.__writer_ver = 1 - self.__writer_ver
+
+ if self.__reader_ver is None:
+ self.__reader_ver = 0
+ else:
+ self.__reader_ver = 1 - self.__reader_ver
+
+ # Versions should be different
+ assert(self.__reader_ver != self.__writer_ver)
+
+class DataSrcInfo:
+ """A container for datasrc.ConfigurableClientLists and associated
+ in-memory segment information corresponding to a given geration of
+ configuration.
+
+ This class maintains all datasrc.ConfigurableClientLists in a form
+ of dict from RR classes corresponding to a given single generation
+ of data source configuration, along with sets of memory segment
+ information that needs to be used by memmgr.
+
+ Once constructed, mappings do not change (different generation of
+ configuration will result in another DataSrcInfo objects). Status
+ of SegmentInfo objects stored in this class object may change over time.
+
+ Attributes: these are all constant and read only. For dict objects,
+ mapping shouldn't be modified either.
+ gen_id (int): The corresponding configuration generation ID.
+ clients_map (dict, isc.dns.RRClass=>isc.datasrc.ConfigurableClientList):
+ The configured client lists for all RR classes of the generation.
+ segment_info_map (dict, (isc.dns.RRClass, str)=>SegmentInfo):
+ SegmentInfo objects managed in the DataSrcInfo objects. Can be
+ retrieved by (RRClass, <data source name>).
+
+ """
+ def __init__(self, genid, clients_map, mgr_config):
+ """Constructor.
+
+ As long as given parameters are of valid type and have been
+ validated, this constructor shouldn't raise an exception.
+
+ Parameters:
+ genid (int): see gen_id attribute
+ clients_map (dict): see clients_map attribute
+ mgr_config (dict, str=>key-dependent-value): A copy of the current
+ memmgr configuration, in case it's needed to construct a specific
+ type of SegmentInfo. The specific SegmentInfo class is expected
+ to know the key-value mappings that it needs.
+
+ """
+ self.__gen_id = genid
+ self.__clients_map = clients_map
+ self.__segment_info_map = {}
+ for (rrclass, client_list) in clients_map.items():
+ for (name, sgmt_type, _) in client_list.get_status():
+ sgmt_info = SegmentInfo.create(sgmt_type, genid, rrclass, name,
+ mgr_config)
+ if sgmt_info is not None:
+ self.__segment_info_map[(rrclass, name)] = sgmt_info
+
+ @property
+ def gen_id(self):
+ return self.__gen_id
+
+ @property
+ def clients_map(self):
+ return self.__clients_map
+
+ @property
+ def segment_info_map(self):
+ return self.__segment_info_map
diff --git a/src/lib/python/isc/memmgr/libmemmgr_messages.mes b/src/lib/python/isc/memmgr/libmemmgr_messages.mes
new file mode 100644
index 0000000..c8fcf05
--- /dev/null
+++ b/src/lib/python/isc/memmgr/libmemmgr_messages.mes
@@ -0,0 +1,35 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the config_messages python module.
+
+% LIBMEMMGR_BUILDER_BAD_COMMAND_ERROR MemorySegmentBuilder received bad command '%1'
+The MemorySegmentBuilder has received a bad command in its input command
+queue. This is likely a programming error. If the builder runs in a
+separate thread, this would cause it to exit the thread.
+
+% LIBMEMMGR_BUILDER_GET_ZONE_WRITER_ERROR Unable to get zone writer for zone '%1', data source '%2'. Skipping.
+The MemorySegmentBuilder was unable to get a ZoneWriter for the
+specified zone when handling the load command. This zone will be
+skipped.
+
+% LIBMEMMGR_BUILDER_ZONE_WRITER_LOAD_1_ERROR Error loading zone '%1', data source '%2': '%3'
+The MemorySegmentBuilder failed to load the specified zone when handling
+the load command. This zone will be skipped.
+
+% LIBMEMMGR_BUILDER_ZONE_WRITER_LOAD_2_ERROR Error loading zone '%1', data source '%2': '%3'
+An exception occured when the MemorySegmentBuilder tried to load the
+specified zone when handling the load command. This zone will be
+skipped.
diff --git a/src/lib/python/isc/memmgr/logger.py b/src/lib/python/isc/memmgr/logger.py
new file mode 100644
index 0000000..eb324cf
--- /dev/null
+++ b/src/lib/python/isc/memmgr/logger.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''Common definitions regarding logging for the memmgr package.'''
+
+import isc.log
+
+logger = isc.log.Logger("libmemmgr")
diff --git a/src/lib/python/isc/memmgr/tests/Makefile.am b/src/lib/python/isc/memmgr/tests/Makefile.am
new file mode 100644
index 0000000..b171cb1
--- /dev/null
+++ b/src/lib/python/isc/memmgr/tests/Makefile.am
@@ -0,0 +1,36 @@
+SUBDIRS = testdata
+PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
+PYTESTS = builder_tests.py datasrc_info_tests.py
+EXTRA_DIST = $(PYTESTS)
+
+# If necessary (rare cases), explicitly specify paths to dynamic libraries
+# required by loadable python modules.
+if SET_ENV_LIBRARY_PATH
+LIBRARY_PATH_PLACEHOLDER = $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/acl/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
+endif
+
+# Some tests require backend shared memory support
+if USE_SHARED_MEMORY
+HAVE_SHARED_MEMORY=yes
+else
+HAVE_SHARED_MEMORY=no
+endif
+
+# test using command-line arguments, so use check-local target instead of TESTS
+# B10_FROM_BUILD is necessary to load data source backend from the build tree.
+check-local:
+if ENABLE_PYTHON_COVERAGE
+ touch $(abs_top_srcdir)/.coverage
+ rm -f .coverage
+ ${LN_S} $(abs_top_srcdir)/.coverage .coverage
+endif
+ for pytest in $(PYTESTS) ; do \
+ echo Running test: $$pytest ; \
+ $(LIBRARY_PATH_PLACEHOLDER) \
+ TESTDATA_PATH=$(abs_srcdir)/testdata \
+ TESTDATA_WRITE_PATH=$(builddir) \
+ B10_FROM_BUILD=$(abs_top_builddir) \
+ HAVE_SHARED_MEMORY=$(HAVE_SHARED_MEMORY) \
+ PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
+ $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
+ done
diff --git a/src/lib/python/isc/memmgr/tests/builder_tests.py b/src/lib/python/isc/memmgr/tests/builder_tests.py
new file mode 100644
index 0000000..b5122cb
--- /dev/null
+++ b/src/lib/python/isc/memmgr/tests/builder_tests.py
@@ -0,0 +1,240 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import os
+import socket
+import select
+import threading
+
+import isc.log
+from isc.dns import *
+import isc.datasrc
+from isc.memmgr.builder import *
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr
+from isc.memmgr.datasrc_info import *
+
+TESTDATA_PATH = os.environ['TESTDATA_PATH'] + os.sep
+
+# Defined for easier tests with DataSrcClientsMgr.reconfigure(), which
+# only needs get_value() method
+class MockConfigData:
+ def __init__(self, data):
+ self.__data = data
+
+ def get_value(self, identifier):
+ return self.__data[identifier], False
+
+class TestMemorySegmentBuilder(unittest.TestCase):
+ def _create_builder_thread(self):
+ (self._master_sock, self._builder_sock) = \
+ socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+
+ self._builder_command_queue = []
+ self._builder_response_queue = []
+
+ self._builder_lock = threading.Lock()
+ self._builder_cv = threading.Condition(lock=self._builder_lock)
+
+ self._builder = MemorySegmentBuilder(self._builder_sock,
+ self._builder_cv,
+ self._builder_command_queue,
+ self._builder_response_queue)
+ self._builder_thread = threading.Thread(target=self._builder.run)
+
+ def setUp(self):
+ self._create_builder_thread()
+ self.__mapped_file_path = None
+
+ def tearDown(self):
+ # It's the tests' responsibility to stop and join the builder
+ # thread if they start it.
+ self.assertFalse(self._builder_thread.isAlive())
+
+ self._master_sock.close()
+ self._builder_sock.close()
+
+ if self.__mapped_file_path is not None:
+ if os.path.exists(self.__mapped_file_path):
+ os.unlink(self.__mapped_file_path)
+
+ def test_bad_command(self):
+ """Tests what happens when a bad command is passed to the
+ MemorySegmentBuilder.
+ """
+
+ self._builder_thread.start()
+
+ # Now that the builder thread is running, send it a bad
+ # command. The thread should exit its main loop and be joinable.
+ with self._builder_cv:
+ self._builder_command_queue.append(('bad_command',))
+ self._builder_cv.notify_all()
+
+ # Wait 5 seconds to receive a notification on the socket from
+ # the builder.
+ (reads, _, _) = select.select([self._master_sock], [], [], 5)
+ self.assertTrue(self._master_sock in reads)
+
+ # Reading 1 byte should not block us here, especially as the
+ # socket is ready to read. It's a hack, but this is just a
+ # testcase.
+ got = self._master_sock.recv(1)
+ self.assertEqual(got, b'x')
+
+ # Wait 5 seconds at most for the main loop of the builder to
+ # exit.
+ self._builder_thread.join(5)
+ self.assertFalse(self._builder_thread.isAlive())
+
+ # The command queue must be cleared, and the response queue must
+ # contain a response that a bad command was sent. The thread is
+ # no longer running, so we can use the queues without a lock.
+ self.assertEqual(len(self._builder_command_queue), 0)
+ self.assertEqual(len(self._builder_response_queue), 1)
+
+ response = self._builder_response_queue[0]
+ self.assertTrue(isinstance(response, tuple))
+ self.assertTupleEqual(response, ('bad_command',))
+ del self._builder_response_queue[:]
+
+ def test_shutdown(self):
+ """Tests that shutdown command exits the MemorySegmentBuilder
+ loop.
+ """
+
+ self._builder_thread.start()
+
+ # Now that the builder thread is running, send it the "shutdown"
+ # command. The thread should exit its main loop and be joinable.
+ with self._builder_cv:
+ self._builder_command_queue.append(('shutdown',))
+ # Commands after 'shutdown' must be ignored.
+ self._builder_command_queue.append(('bad_command_1',))
+ self._builder_command_queue.append(('bad_command_2',))
+ self._builder_cv.notify_all()
+
+ # Wait 5 seconds at most for the main loop of the builder to
+ # exit.
+ self._builder_thread.join(5)
+ self.assertFalse(self._builder_thread.isAlive())
+
+ # The command queue must be cleared, and the response queue must
+ # be untouched (we don't use it in this test). The thread is no
+ # longer running, so we can use the queues without a lock.
+ self.assertEqual(len(self._builder_command_queue), 0)
+ self.assertEqual(len(self._builder_response_queue), 0)
+
+ @unittest.skipIf(os.environ['HAVE_SHARED_MEMORY'] != 'yes',
+ 'shared memory is not available')
+ def test_load(self):
+ """
+ Test "load" command.
+ """
+
+ mapped_file_dir = os.environ['TESTDATA_WRITE_PATH']
+ mgr_config = {'mapped_file_dir': mapped_file_dir}
+
+ cfg_data = MockConfigData(
+ {"classes":
+ {"IN": [{"type": "MasterFiles",
+ "params": { "example.com": TESTDATA_PATH + "example.com.zone" },
+ "cache-enable": True,
+ "cache-type": "mapped"}]
+ }
+ })
+ cmgr = DataSrcClientsMgr(use_cache=True)
+ cmgr.reconfigure({}, cfg_data)
+
+ genid, clients_map = cmgr.get_clients_map()
+ datasrc_info = DataSrcInfo(genid, clients_map, mgr_config)
+
+ self.assertEqual(1, datasrc_info.gen_id)
+ self.assertEqual(clients_map, datasrc_info.clients_map)
+ self.assertEqual(1, len(datasrc_info.segment_info_map))
+ sgmt_info = datasrc_info.segment_info_map[(RRClass.IN, 'MasterFiles')]
+ self.assertIsNone(sgmt_info.get_reset_param(SegmentInfo.READER))
+ self.assertIsNotNone(sgmt_info.get_reset_param(SegmentInfo.WRITER))
+
+ param = sgmt_info.get_reset_param(SegmentInfo.WRITER)
+ self.__mapped_file_path = param['mapped-file']
+
+ self._builder_thread.start()
+
+ # Now that the builder thread is running, send it the "load"
+ # command. We should be notified when the load operation is
+ # complete.
+ with self._builder_cv:
+ self._builder_command_queue.append(('load',
+ isc.dns.Name("example.com"),
+ datasrc_info, RRClass.IN,
+ 'MasterFiles'))
+ self._builder_cv.notify_all()
+
+ # Wait 60 seconds to receive a notification on the socket from
+ # the builder.
+ (reads, _, _) = select.select([self._master_sock], [], [], 60)
+ self.assertTrue(self._master_sock in reads)
+
+ # Reading 1 byte should not block us here, especially as the
+ # socket is ready to read. It's a hack, but this is just a
+ # testcase.
+ got = self._master_sock.recv(1)
+ self.assertEqual(got, b'x')
+
+ with self._builder_lock:
+ # The command queue must be cleared, and the response queue
+ # must contain a response that a bad command was sent. The
+ # thread is no longer running, so we can use the queues
+ # without a lock.
+ self.assertEqual(len(self._builder_command_queue), 0)
+ self.assertEqual(len(self._builder_response_queue), 1)
+
+ response = self._builder_response_queue[0]
+ self.assertTrue(isinstance(response, tuple))
+ self.assertTupleEqual(response, ('load-completed', datasrc_info,
+ RRClass.IN, 'MasterFiles'))
+ del self._builder_response_queue[:]
+
+ # Now try looking for some loaded data
+ clist = datasrc_info.clients_map[RRClass.IN]
+ dsrc, finder, exact = clist.find(isc.dns.Name("example.com"))
+ self.assertIsNotNone(dsrc)
+ self.assertTrue(isinstance(dsrc, isc.datasrc.DataSourceClient))
+ self.assertIsNotNone(finder)
+ self.assertTrue(isinstance(finder, isc.datasrc.ZoneFinder))
+ self.assertTrue(exact)
+
+ # Send the builder thread the "shutdown" command. The thread
+ # should exit its main loop and be joinable.
+ with self._builder_cv:
+ self._builder_command_queue.append(('shutdown',))
+ self._builder_cv.notify_all()
+
+ # Wait 5 seconds at most for the main loop of the builder to
+ # exit.
+ self._builder_thread.join(5)
+ self.assertFalse(self._builder_thread.isAlive())
+
+ # The command queue must be cleared, and the response queue must
+ # be untouched (we don't use it in this test). The thread is no
+ # longer running, so we can use the queues without a lock.
+ self.assertEqual(len(self._builder_command_queue), 0)
+ self.assertEqual(len(self._builder_response_queue), 0)
+
+if __name__ == "__main__":
+ isc.log.init("bind10-test")
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/lib/python/isc/memmgr/tests/datasrc_info_tests.py b/src/lib/python/isc/memmgr/tests/datasrc_info_tests.py
new file mode 100644
index 0000000..538f375
--- /dev/null
+++ b/src/lib/python/isc/memmgr/tests/datasrc_info_tests.py
@@ -0,0 +1,469 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import os
+import unittest
+
+from isc.dns import *
+import isc.config
+import isc.datasrc
+import isc.log
+from isc.server_common.datasrc_clients_mgr import DataSrcClientsMgr
+from isc.memmgr.datasrc_info import *
+
+# Defined for easier tests with DataSrcClientsMgr.reconfigure(), which
+# only needs get_value() method
+class MockConfigData:
+ def __init__(self, data):
+ self.__data = data
+
+ def get_value(self, identifier):
+ return self.__data[identifier], False
+
+class TestSegmentInfo(unittest.TestCase):
+ def setUp(self):
+ self.__mapped_file_dir = os.environ['TESTDATA_WRITE_PATH']
+ self.__sgmt_info = SegmentInfo.create('mapped', 0, RRClass.IN,
+ 'sqlite3',
+ {'mapped_file_dir':
+ self.__mapped_file_dir})
+
+ def __check_sgmt_reset_param(self, user_type, expected_ver):
+ """Common check on the return value of get_reset_param() for
+ MappedSegmentInfo.
+
+ Unless it's expected to return None, it should be a map that
+ maps "mapped-file" to the expected version of mapped-file.
+
+ """
+ if expected_ver is None:
+ self.assertIsNone(self.__sgmt_info.get_reset_param(user_type))
+ return
+ param = self.__sgmt_info.get_reset_param(user_type)
+ self.assertEqual(self.__mapped_file_dir +
+ '/zone-IN-0-sqlite3-mapped.' + str(expected_ver),
+ param['mapped-file'])
+
+ def test_initial_params(self):
+ self.__check_sgmt_reset_param(SegmentInfo.WRITER, 0)
+ self.__check_sgmt_reset_param(SegmentInfo.READER, None)
+
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+ self.assertEqual(len(self.__sgmt_info.get_readers()), 0)
+ self.assertEqual(len(self.__sgmt_info.get_old_readers()), 0)
+ self.assertEqual(len(self.__sgmt_info.get_events()), 0)
+
+ def __si_to_ready_state(self):
+ # Go to a default starting state
+ self.__sgmt_info = SegmentInfo.create('mapped', 0, RRClass.IN,
+ 'sqlite3',
+ {'mapped_file_dir':
+ self.__mapped_file_dir})
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ def __si_to_updating_state(self):
+ self.__si_to_ready_state()
+ self.__sgmt_info.add_reader(3)
+ self.__sgmt_info.add_event((42,))
+ e = self.__sgmt_info.start_update()
+ self.assertTupleEqual(e, (42,))
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {3})
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+
+ def __si_to_synchronizing_state(self):
+ self.__si_to_updating_state()
+ self.__sgmt_info.complete_update()
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ def __si_to_copying_state(self):
+ self.__si_to_synchronizing_state()
+ self.__sgmt_info.sync_reader(3)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ def test_add_event(self):
+ self.assertEqual(len(self.__sgmt_info.get_events()), 0)
+ self.__sgmt_info.add_event(None)
+ self.assertEqual(len(self.__sgmt_info.get_events()), 1)
+ self.assertListEqual(self.__sgmt_info.get_events(), [None])
+
+ def test_add_reader(self):
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), set())
+ self.__sgmt_info.add_reader(1)
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {1})
+ self.__sgmt_info.add_reader(3)
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {1, 3})
+ self.__sgmt_info.add_reader(2)
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {1, 2, 3})
+
+ # adding the same existing reader must throw
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.add_reader, (1))
+ # but the existing readers must be untouched
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {1, 3, 2})
+
+ # none of this touches the old readers
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), set())
+
+ def test_start_update(self):
+ # in READY state
+ # a) there are no events
+ self.__si_to_ready_state()
+ e = self.__sgmt_info.start_update()
+ self.assertIsNone(e)
+ # if there are no events, there is nothing to update
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ # b) there are events. this is the same as calling
+ # self.__si_to_updating_state(), but let's try to be
+ # descriptive.
+ self.__si_to_ready_state()
+ self.__sgmt_info.add_event((42,))
+ e = self.__sgmt_info.start_update()
+ self.assertTupleEqual(e, (42,))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+
+ # in UPDATING state, it should always raise an exception and not
+ # change state.
+ self.__si_to_updating_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.start_update)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+
+ # in SYNCHRONIZING state, it should always raise an exception
+ # and not change state.
+ self.__si_to_synchronizing_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.start_update)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # in COPYING state, it should always raise an exception and not
+ # change state.
+ self.__si_to_copying_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.start_update)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ def test_complete_update(self):
+ # in READY state
+ self.__si_to_ready_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.complete_update)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ # in UPDATING state this is the same as calling
+ # self.__si_to_synchronizing_state(), but let's try to be
+ # descriptive.
+ #
+ # a) with no events
+ self.__si_to_updating_state()
+ e = self.__sgmt_info.complete_update()
+ self.assertIsNone(e)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # b) with events
+ self.__si_to_updating_state()
+ self.__sgmt_info.add_event((81,))
+ e = self.__sgmt_info.complete_update()
+ self.assertIsNone(e) # old_readers is not empty
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # c) with no readers, complete_update() from UPDATING must go
+ # directly to READY state
+ self.__si_to_ready_state()
+ self.__sgmt_info.add_event((42,))
+ e = self.__sgmt_info.start_update()
+ self.assertTupleEqual(e, (42,))
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+ e = self.__sgmt_info.complete_update()
+ self.assertTupleEqual(e, (42,))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ # in SYNCHRONIZING state
+ self.__si_to_synchronizing_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.complete_update)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # in COPYING state
+ self.__si_to_copying_state()
+ e = self.__sgmt_info.complete_update()
+ self.assertIsNone(e)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ def test_sync_reader(self):
+ # in READY state, it must raise an exception
+ self.__si_to_ready_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.sync_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ # in UPDATING state, it must raise an exception
+ self.__si_to_updating_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.sync_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+
+ # in COPYING state, it must raise an exception
+ self.__si_to_copying_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.sync_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ # in SYNCHRONIZING state:
+ #
+ # a) ID is not in old readers set. The following call sets up ID 3
+ # to be in the old readers set.
+ self.__si_to_synchronizing_state()
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.sync_reader, (1))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # b) ID is in old readers set, but also in readers set.
+ self.__si_to_synchronizing_state()
+ self.__sgmt_info.add_reader(3)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {3})
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.sync_reader, (3))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # c) ID is in old readers set, but not in readers set, and
+ # old_readers becomes empty.
+ self.__si_to_synchronizing_state()
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ e = self.__sgmt_info.sync_reader(3)
+ self.assertTupleEqual(e, (42,))
+ # the ID should be moved from old readers to readers set
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), set())
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {3})
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ # d) ID is in old readers set, but not in readers set, and
+ # old_readers doesn't become empty.
+ self.__si_to_updating_state()
+ self.__sgmt_info.add_reader(4)
+ self.__sgmt_info.complete_update()
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3, 4})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ e = self.__sgmt_info.sync_reader(3)
+ self.assertIsNone(e)
+ # the ID should be moved from old readers to readers set
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {4})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {3})
+ # we should be left in SYNCHRONIZING state
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ def test_remove_reader(self):
+ # in READY state, it must raise an exception
+ self.__si_to_ready_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.remove_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.READY)
+
+ # in UPDATING state, it must raise an exception
+ self.__si_to_updating_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.remove_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.UPDATING)
+
+ # in COPYING state, it must raise an exception
+ self.__si_to_copying_state()
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.remove_reader, (0))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ # in SYNCHRONIZING state:
+ #
+ # a) ID is not in old readers set or readers set.
+ self.__si_to_synchronizing_state()
+ self.__sgmt_info.add_reader(4)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {4})
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ self.assertRaises(SegmentInfoError, self.__sgmt_info.remove_reader, (1))
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # b) ID is in readers set.
+ self.__si_to_synchronizing_state()
+ self.__sgmt_info.add_reader(4)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {4})
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ e = self.__sgmt_info.remove_reader(4)
+ self.assertIsNone(e)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), set())
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ # we only change state if it was removed from old_readers
+ # specifically and it became empty.
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ # c) ID is in old_readers set and it becomes empty.
+ self.__si_to_synchronizing_state()
+ self.__sgmt_info.add_reader(4)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {4})
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ e = self.__sgmt_info.remove_reader(3)
+ self.assertTupleEqual(e, (42,))
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), set())
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {4})
+ self.assertListEqual(self.__sgmt_info.get_events(), [])
+ # we only change state if it was removed from old_readers
+ # specifically and it became empty.
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.COPYING)
+
+ # d) ID is in old_readers set and it doesn't become empty.
+ self.__si_to_updating_state()
+ self.__sgmt_info.add_reader(4)
+ self.__sgmt_info.complete_update()
+ self.__sgmt_info.add_reader(5)
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {3, 4})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {5})
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ e = self.__sgmt_info.remove_reader(3)
+ self.assertIsNone(e)
+ self.assertSetEqual(self.__sgmt_info.get_old_readers(), {4})
+ self.assertSetEqual(self.__sgmt_info.get_readers(), {5})
+ self.assertListEqual(self.__sgmt_info.get_events(), [(42,)])
+ # we only change state if it was removed from old_readers
+ # specifically and it became empty.
+ self.assertEqual(self.__sgmt_info.get_state(), SegmentInfo.SYNCHRONIZING)
+
+ def test_switch_versions(self):
+ self.__sgmt_info.switch_versions()
+ self.__check_sgmt_reset_param(SegmentInfo.WRITER, 1)
+ self.__check_sgmt_reset_param(SegmentInfo.READER, 0)
+
+ self.__sgmt_info.switch_versions()
+ self.__check_sgmt_reset_param(SegmentInfo.WRITER, 0)
+ self.__check_sgmt_reset_param(SegmentInfo.READER, 1)
+
+ def test_init_others(self):
+ # For local type of segment, information isn't needed and won't be
+ # created.
+ self.assertIsNone(SegmentInfo.create('local', 0, RRClass.IN,
+ 'sqlite3', {}))
+
+ # Unknown type of segment will result in an exception.
+ self.assertRaises(SegmentInfoError, SegmentInfo.create, 'unknown', 0,
+ RRClass.IN, 'sqlite3', {})
+
+ def test_missing_methods(self):
+ # Bad subclass of SegmentInfo that doesn't implement mandatory methods.
+ class TestSegmentInfo(SegmentInfo):
+ pass
+
+ self.assertRaises(SegmentInfoError,
+ TestSegmentInfo().get_reset_param,
+ SegmentInfo.WRITER)
+ self.assertRaises(SegmentInfoError, TestSegmentInfo().switch_versions)
+
+class MockClientList:
+ """A mock ConfigurableClientList class.
+
+ Just providing minimal shortcut interfaces needed for DataSrcInfo class.
+
+ """
+ def __init__(self, status_list):
+ self.__status_list = status_list
+
+ def get_status(self):
+ return self.__status_list
+
+class TestDataSrcInfo(unittest.TestCase):
+ def setUp(self):
+ self.__mapped_file_dir = os.environ['TESTDATA_WRITE_PATH']
+ self.__mgr_config = {'mapped_file_dir': self.__mapped_file_dir}
+ self.__sqlite3_dbfile = os.environ['TESTDATA_WRITE_PATH'] + '/' + 'zone.db'
+ self.__clients_map = {
+ # mixture of 'local' and 'mapped' and 'unused' (type =None)
+ # segments
+ RRClass.IN: MockClientList([('datasrc1', 'local', None),
+ ('datasrc2', 'mapped', None),
+ ('datasrc3', None, None)]),
+ RRClass.CH: MockClientList([('datasrc2', 'mapped', None),
+ ('datasrc1', 'local', None)]) }
+
+ def tearDown(self):
+ if os.path.exists(self.__sqlite3_dbfile):
+ os.unlink(self.__sqlite3_dbfile)
+
+ def __check_sgmt_reset_param(self, sgmt_info, writer_file):
+ # Check if the initial state of (mapped) segment info object has
+ # expected values.
+ self.assertIsNone(sgmt_info.get_reset_param(SegmentInfo.READER))
+ param = sgmt_info.get_reset_param(SegmentInfo.WRITER)
+ self.assertEqual(writer_file, param['mapped-file'])
+
+ def test_init(self):
+ """Check basic scenarios of constructing DataSrcInfo."""
+
+ # This checks that all data sources of all RR classes are covered,
+ # "local" segments are ignored, info objects for "mapped" segments
+ # are created and stored in segment_info_map.
+ datasrc_info = DataSrcInfo(42, self.__clients_map, self.__mgr_config)
+ self.assertEqual(42, datasrc_info.gen_id)
+ self.assertEqual(self.__clients_map, datasrc_info.clients_map)
+ self.assertEqual(2, len(datasrc_info.segment_info_map))
+ sgmt_info = datasrc_info.segment_info_map[(RRClass.IN, 'datasrc2')]
+ self.__check_sgmt_reset_param(sgmt_info, self.__mapped_file_dir +
+ '/zone-IN-42-datasrc2-mapped.0')
+ sgmt_info = datasrc_info.segment_info_map[(RRClass.CH, 'datasrc2')]
+ self.__check_sgmt_reset_param(sgmt_info, self.__mapped_file_dir +
+ '/zone-CH-42-datasrc2-mapped.0')
+
+ # A case where clist.get_status() returns an empty list; shouldn't
+ # cause disruption
+ self.__clients_map = { RRClass.IN: MockClientList([])}
+ datasrc_info = DataSrcInfo(42, self.__clients_map, self.__mgr_config)
+ self.assertEqual(42, datasrc_info.gen_id)
+ self.assertEqual(0, len(datasrc_info.segment_info_map))
+
+ # A case where clients_map is empty; shouldn't cause disruption
+ self.__clients_map = {}
+ datasrc_info = DataSrcInfo(42, self.__clients_map, self.__mgr_config)
+ self.assertEqual(42, datasrc_info.gen_id)
+ self.assertEqual(0, len(datasrc_info.segment_info_map))
+
+ # This test uses real "mmaped" segment and doesn't work without shared
+ # memory support.
+ @unittest.skipIf(os.environ['HAVE_SHARED_MEMORY'] != 'yes',
+ 'shared memory support is not available')
+ def test_production(self):
+ """Check the behavior closer to a production environment.
+
+ Instead of using a mock classes, just for confirming we didn't miss
+ something.
+
+ """
+ cfg_data = MockConfigData(
+ {"classes":
+ {"IN": [{"type": "sqlite3", "cache-enable": True,
+ "cache-type": "mapped", "cache-zones": [],
+ "params": {"database_file": self.__sqlite3_dbfile}}]
+ }
+ })
+ cmgr = DataSrcClientsMgr(use_cache=True)
+ cmgr.reconfigure({}, cfg_data)
+
+ genid, clients_map = cmgr.get_clients_map()
+ datasrc_info = DataSrcInfo(genid, clients_map, self.__mgr_config)
+
+ self.assertEqual(1, datasrc_info.gen_id)
+ self.assertEqual(clients_map, datasrc_info.clients_map)
+ self.assertEqual(1, len(datasrc_info.segment_info_map))
+ sgmt_info = datasrc_info.segment_info_map[(RRClass.IN, 'sqlite3')]
+ self.assertIsNone(sgmt_info.get_reset_param(SegmentInfo.READER))
+
+if __name__ == "__main__":
+ isc.log.init("bind10-test")
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/lib/python/isc/memmgr/tests/testdata/Makefile.am b/src/lib/python/isc/memmgr/tests/testdata/Makefile.am
new file mode 100644
index 0000000..22e7ce3
--- /dev/null
+++ b/src/lib/python/isc/memmgr/tests/testdata/Makefile.am
@@ -0,0 +1,2 @@
+EXTRA_DIST = \
+ example.com.zone
diff --git a/src/lib/python/isc/memmgr/tests/testdata/example.com.zone b/src/lib/python/isc/memmgr/tests/testdata/example.com.zone
new file mode 100644
index 0000000..24e22e1
--- /dev/null
+++ b/src/lib/python/isc/memmgr/tests/testdata/example.com.zone
@@ -0,0 +1,8 @@
+example.com. 1000 IN SOA a.dns.example.com. mail.example.com. 1 1 1 1 1
+example.com. 1000 IN NS a.dns.example.com.
+example.com. 1000 IN NS b.dns.example.com.
+example.com. 1000 IN NS c.dns.example.com.
+a.dns.example.com. 1000 IN A 1.1.1.1
+b.dns.example.com. 1000 IN A 3.3.3.3
+b.dns.example.com. 1000 IN AAAA 4:4::4:4
+b.dns.example.com. 1000 IN AAAA 5:5::5:5
diff --git a/src/lib/python/isc/notify/notify_out.py b/src/lib/python/isc/notify/notify_out.py
index a030a51..f4de7b8 100644
--- a/src/lib/python/isc/notify/notify_out.py
+++ b/src/lib/python/isc/notify/notify_out.py
@@ -25,7 +25,7 @@ from isc.datasrc import DataSourceClient
from isc.net import addr
import isc
from isc.log_messages.notify_out_messages import *
-from isc.statistics import Counters
+from isc.statistics.dns import Counters
from isc.util.address_formatter import AddressFormatter
logger = isc.log.Logger("notify_out")
@@ -128,7 +128,7 @@ class NotifyOut:
notify message to its slaves). notify service can be started by
calling dispatcher(), and it can be stopped by calling shutdown()
in another thread. '''
- def __init__(self, datasrc_file, verbose=True):
+ def __init__(self, datasrc_file, counters=None, verbose=True):
self._notify_infos = {} # key is (zone_name, zone_class)
self._waiting_zones = []
self._notifying_zones = []
@@ -143,7 +143,7 @@ class NotifyOut:
# Use nonblock event to eliminate busy loop
# If there are no notifying zones, clear the event bit and wait.
self._nonblock_event = threading.Event()
- self._counters = Counters()
+ self._counters = counters
def _init_notify_out(self, datasrc_file):
'''Get all the zones name and its notify target's address.
@@ -508,12 +508,17 @@ class NotifyOut:
sock = zone_notify_info.create_socket(addrinfo[0])
sock.sendto(render.get_data(), 0, addrinfo)
# count notifying by IPv4 or IPv6 for statistics
- if zone_notify_info.get_socket().family == socket.AF_INET:
- self._counters.inc('zones', zone_notify_info.zone_name,
- 'notifyoutv4')
- elif zone_notify_info.get_socket().family == socket.AF_INET6:
- self._counters.inc('zones', zone_notify_info.zone_name,
- 'notifyoutv6')
+ if self._counters is not None:
+ if zone_notify_info.get_socket().family == socket.AF_INET:
+ self._counters.inc('zones',
+ zone_notify_info.zone_class,
+ zone_notify_info.zone_name,
+ 'notifyoutv4')
+ elif zone_notify_info.get_socket().family == socket.AF_INET6:
+ self._counters.inc('zones',
+ zone_notify_info.zone_class,
+ zone_notify_info.zone_name,
+ 'notifyoutv6')
logger.info(NOTIFY_OUT_SENDING_NOTIFY, AddressFormatter(addrinfo))
except (socket.error, addr.InvalidAddress) as err:
logger.error(NOTIFY_OUT_SOCKET_ERROR, AddressFormatter(addrinfo),
diff --git a/src/lib/python/isc/notify/tests/Makefile.am b/src/lib/python/isc/notify/tests/Makefile.am
index d964880..52f2409 100644
--- a/src/lib/python/isc/notify/tests/Makefile.am
+++ b/src/lib/python/isc/notify/tests/Makefile.am
@@ -5,7 +5,7 @@ EXTRA_DIST += testdata/test.sqlite3 testdata/brokentest.sqlite3
# The rest of the files are actually not necessary, but added for reference
EXTRA_DIST += testdata/example.com testdata/example.net
EXTRA_DIST += testdata/nons.example testdata/nosoa.example
-EXTRA_DIST += testdata/multisoa.example
+EXTRA_DIST += testdata/multisoa.example testdata/test_spec1.spec
# If necessary (rare cases), explicitly specify paths to dynamic libraries
# required by loadable python modules.
diff --git a/src/lib/python/isc/notify/tests/notify_out_test.py b/src/lib/python/isc/notify/tests/notify_out_test.py
index e2b8d27..ec4df0d 100644
--- a/src/lib/python/isc/notify/tests/notify_out_test.py
+++ b/src/lib/python/isc/notify/tests/notify_out_test.py
@@ -22,8 +22,10 @@ import socket
from isc.notify import notify_out, SOCK_DATA
import isc.log
from isc.dns import *
+from isc.statistics.dns import Counters
TESTDATA_SRCDIR = os.getenv("TESTDATASRCDIR")
+SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec1.spec'
def get_notify_msgdata(zone_name, qid=0):
"""A helper function to generate a notify response in wire format.
@@ -128,7 +130,7 @@ class TestZoneNotifyInfo(unittest.TestCase):
class TestNotifyOut(unittest.TestCase):
def setUp(self):
self._db_file = TESTDATA_SRCDIR + '/test.sqlite3'
- self._notify = notify_out.NotifyOut(self._db_file)
+ self._notify = notify_out.NotifyOut(self._db_file, counters=Counters(SPECFILE_LOCATION))
self._notify._notify_infos[('example.com.', 'IN')] = MockZoneNotifyInfo('example.com.', 'IN')
self._notify._notify_infos[('example.com.', 'CH')] = MockZoneNotifyInfo('example.com.', 'CH')
self._notify._notify_infos[('example.net.', 'IN')] = MockZoneNotifyInfo('example.net.', 'IN')
@@ -304,10 +306,10 @@ class TestNotifyOut(unittest.TestCase):
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv4')
+ 'zones', 'IN', 'example.net.', 'notifyoutv4')
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv6')
+ 'zones', 'IN', 'example.net.', 'notifyoutv6')
example_com_info.prepare_notify_out()
ret = self._notify._send_notify_message_udp(example_com_info,
@@ -315,38 +317,38 @@ class TestNotifyOut(unittest.TestCase):
self.assertTrue(ret)
self.assertEqual(socket.AF_INET, example_com_info.sock_family)
self.assertEqual(self._notify._counters.get(
- 'zones', 'example.net.', 'notifyoutv4'), 1)
+ 'zones', 'IN', 'example.net.', 'notifyoutv4'), 1)
self.assertEqual(self._notify._counters.get(
- 'zones', 'example.net.', 'notifyoutv6'), 0)
+ 'zones', 'IN', 'example.net.', 'notifyoutv6'), 0)
def test_send_notify_message_udp_ipv6(self):
example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv4')
+ 'zones', 'IN', 'example.net.', 'notifyoutv4')
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv6')
+ 'zones', 'IN', 'example.net.', 'notifyoutv6')
ret = self._notify._send_notify_message_udp(example_com_info,
('2001:db8::53', 53))
self.assertTrue(ret)
self.assertEqual(socket.AF_INET6, example_com_info.sock_family)
self.assertEqual(self._notify._counters.get(
- 'zones', 'example.net.', 'notifyoutv4'), 0)
+ 'zones', 'IN', 'example.net.', 'notifyoutv4'), 0)
self.assertEqual(self._notify._counters.get(
- 'zones', 'example.net.', 'notifyoutv6'), 1)
+ 'zones', 'IN', 'example.net.', 'notifyoutv6'), 1)
def test_send_notify_message_with_bogus_address(self):
example_com_info = self._notify._notify_infos[('example.net.', 'IN')]
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv4')
+ 'zones', 'IN', 'example.net.', 'notifyoutv4')
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv6')
+ 'zones', 'IN', 'example.net.', 'notifyoutv6')
# As long as the underlying data source validates RDATA this shouldn't
# happen, but right now it's not actually the case. Even if the
@@ -358,10 +360,10 @@ class TestNotifyOut(unittest.TestCase):
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv4')
+ 'zones', 'IN', 'example.net.', 'notifyoutv4')
self.assertRaises(isc.cc.data.DataNotFoundError,
self._notify._counters.get,
- 'zones', 'example.net.', 'notifyoutv4')
+ 'zones', 'IN', 'example.net.', 'notifyoutv4')
def test_zone_notify_handler(self):
sent_addrs = []
diff --git a/src/lib/python/isc/notify/tests/testdata/test_spec1.spec b/src/lib/python/isc/notify/tests/testdata/test_spec1.spec
new file mode 100644
index 0000000..7ac8013
--- /dev/null
+++ b/src/lib/python/isc/notify/tests/testdata/test_spec1.spec
@@ -0,0 +1,57 @@
+{
+ "module_spec": {
+ "module_name": "NotifyOutLike",
+ "module_description": "Test notifier",
+ "config_data": [],
+ "commands": [],
+ "statistics": [
+ {
+ "item_name": "zones",
+ "item_type": "named_set",
+ "item_optional": false,
+ "item_default": {
+ "_SERVER_" : {
+ "notifyoutv4" : 0,
+ "notifyoutv6" : 0
+ }
+ },
+ "item_title": "Zone names",
+ "item_description": "Zone names",
+ "named_set_item_spec": {
+ "item_name": "classname",
+ "item_type": "named_set",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "RR class name",
+ "item_description": "RR class name",
+ "named_set_item_spec": {
+ "item_name": "zonename",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "Zone name",
+ "item_description": "Zone name",
+ "map_item_spec": [
+ {
+ "item_name": "notifyoutv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv4 notifies",
+ "item_description": "Number of IPv4 notifies per zone name sent out"
+ },
+ {
+ "item_name": "notifyoutv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv6 notifies",
+ "item_description": "Number of IPv6 notifies per zone name sent out"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/src/lib/python/isc/server_common/.gitignore b/src/lib/python/isc/server_common/.gitignore
new file mode 100644
index 0000000..63239b7
--- /dev/null
+++ b/src/lib/python/isc/server_common/.gitignore
@@ -0,0 +1 @@
+/bind10_server.py
diff --git a/src/lib/python/isc/server_common/Makefile.am b/src/lib/python/isc/server_common/Makefile.am
index d89df2f..53e46e0 100644
--- a/src/lib/python/isc/server_common/Makefile.am
+++ b/src/lib/python/isc/server_common/Makefile.am
@@ -1,11 +1,13 @@
SUBDIRS = tests
python_PYTHON = __init__.py tsig_keyring.py auth_command.py dns_tcp.py
+python_PYTHON += datasrc_clients_mgr.py bind10_server.py
python_PYTHON += logger.py
pythondir = $(pyexecdir)/isc/server_common
BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py
+
nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/server_common_messages.py
pylogmessagedir = $(pyexecdir)/isc/log_messages/
diff --git a/src/lib/python/isc/server_common/bind10_server.py b/src/lib/python/isc/server_common/bind10_server.py
new file mode 100644
index 0000000..249d4d9
--- /dev/null
+++ b/src/lib/python/isc/server_common/bind10_server.py
@@ -0,0 +1,251 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import errno
+import os
+import select
+import signal
+
+import bind10_config
+import isc.log
+import isc.config
+from isc.server_common.logger import logger
+from isc.log_messages.server_common_messages import *
+
+class BIND10ServerFatal(Exception):
+ """Exception raised when the server program encounters a fatal error."""
+ pass
+
+class BIND10Server:
+ """A mixin class for common BIND 10 server implementations.
+
+ It takes care of common initialization such as setting up a module CC
+ session, and running main event loop. It also handles the "shutdown"
+ command for its normal behavior. If a specific server class wants to
+ handle this command differently or if it does not support the command,
+ it should override the _command_handler method.
+
+ Specific modules can define module-specific class inheriting this class,
+ instantiate it, and call run() with the module name.
+
+ Methods to be implemented in the actual class:
+ _config_handler: config handler method as specified in ModuleCCSession.
+ must be exception free; errors should be signaled by
+ the return value.
+ _mod_command_handler: can be optionally defined to handle
+ module-specific commands. should conform to
+ command handlers as specified in ModuleCCSession.
+ must be exception free; errors should be signaled
+ by the return value.
+ _setup_module: can be optionally defined for module-specific
+ initialization. This is called after the module CC
+ session has started, and can be used for registering
+ interest on remote modules, etc. If it raises an
+ exception, the server will be immediately stopped.
+ Parameter: None, Return: None
+ _shutdown_module: can be optionally defined for module-specific
+ finalization. This is called right before the
+ module CC session is stopped. If it raises an
+ exception, the server will be immediately
+ stopped.
+ Parameter: None, Return: None
+
+ """
+ # Will be set to True when the server should stop and shut down.
+ # Can be read via accessor method 'shutdown', mainly for testing.
+ __shutdown = False
+
+ # ModuleCCSession used in the server. Defined as 'protectd' so tests
+ # can refer to it directly; others should access it via the
+ # 'mod_ccsession' accessor.
+ _mod_cc = None
+
+ # Will be set in run(). Define a tentative value so other methods can
+ # be tested directly.
+ __module_name = ''
+
+ # Basically constant, but allow tests to override it.
+ _select_fn = select.select
+
+ def __init__(self):
+ self._read_callbacks = {}
+ self._write_callbacks = {}
+ self._error_callbacks = {}
+
+ @property
+ def shutdown(self):
+ return self.__shutdown
+
+ @property
+ def mod_ccsession(self):
+ return self._mod_cc
+
+ def _setup_ccsession(self):
+ """Create and start module CC session.
+
+ This is essentially private, but allows tests to override it.
+
+ """
+ self._mod_cc = isc.config.ModuleCCSession(
+ bind10_config.get_specfile_location(self.__module_name),
+ self._config_handler, self._command_handler)
+ self._mod_cc.start()
+
+ def _trigger_shutdown(self):
+ """Initiate a shutdown sequence.
+
+ This method is expected to be called in various ways including
+ in the middle of a signal handler, and is designed to be as simple
+ as possible to minimize side effects. Actual shutdown will take
+ place in a normal control flow.
+
+ This method is defined as 'protected'. User classes can use it
+ to shut down the server.
+
+ """
+ self.__shutdown = True
+
+ def _run_internal(self):
+ """Main event loop.
+
+ This method is essentially private, but allows tests to override it.
+
+ """
+
+ logger.info(PYSERVER_COMMON_SERVER_STARTED, self.__module_name)
+ cc_fileno = self._mod_cc.get_socket().fileno()
+ while not self.__shutdown:
+ try:
+ read_fds = list(self._read_callbacks.keys())
+ read_fds.append(cc_fileno)
+ write_fds = list(self._write_callbacks.keys())
+ error_fds = list(self._error_callbacks.keys())
+
+ (reads, writes, errors) = \
+ self._select_fn(read_fds, write_fds, error_fds)
+ except select.error as ex:
+ # ignore intterruption by signal; regard other select errors
+ # fatal.
+ if ex.args[0] == errno.EINTR:
+ continue
+ else:
+ raise
+
+ for fileno in reads:
+ if fileno in self._read_callbacks:
+ for callback in self._read_callbacks[fileno]:
+ callback()
+
+ for fileno in writes:
+ if fileno in self._write_callbacks:
+ for callback in self._write_callbacks[fileno]:
+ callback()
+
+ for fileno in errors:
+ if fileno in self._error_callbacks:
+ for callback in self._error_callbacks[fileno]:
+ callback()
+
+ if cc_fileno in reads:
+ # this shouldn't raise an exception (if it does, we'll
+ # propagate it)
+ self._mod_cc.check_command(True)
+
+ self._shutdown_module()
+ self._mod_cc.send_stopping()
+
+ def _command_handler(self, cmd, args):
+ logger.debug(logger.DBGLVL_TRACE_BASIC, PYSERVER_COMMON_COMMAND,
+ self.__module_name, cmd)
+ if cmd == 'shutdown':
+ self._trigger_shutdown()
+ answer = isc.config.create_answer(0)
+ else:
+ answer = self._mod_command_handler(cmd, args)
+
+ return answer
+
+ def _mod_command_handler(self, cmd, args):
+ """The default implementation of the module specific command handler"""
+ return isc.config.create_answer(1, "Unknown command: " + str(cmd))
+
+ def _setup_module(self):
+ """The default implementation of the module specific initialization"""
+ pass
+
+ def _shutdown_module(self):
+ """The default implementation of the module specific finalization"""
+ pass
+
+ def watch_fileno(self, fileno, rcallback=None, wcallback=None, \
+ xcallback=None):
+ """Register the fileno for the internal select() call.
+
+ *callback's are callable objects which would be called when
+ read, write, error events occur on the specified fileno.
+ """
+ if rcallback is not None:
+ if fileno in self._read_callbacks:
+ self._read_callbacks[fileno].append(rcallback)
+ else:
+ self._read_callbacks[fileno] = [rcallback]
+
+ if wcallback is not None:
+ if fileno in self._write_callbacks:
+ self._write_callbacks[fileno].append(wcallback)
+ else:
+ self._write_callbacks[fileno] = [wcallback]
+
+ if xcallback is not None:
+ if fileno in self._error_callbacks:
+ self._error_callbacks[fileno].append(xcallback)
+ else:
+ self._error_callbacks[fileno] = [xcallback]
+
+ def run(self, module_name):
+ """Start the server and let it run until it's told to stop.
+
+ Usually this must be the first method of this class that is called
+ from its user.
+
+ Parameter:
+ module_name (str): the Python module name for the actual server
+ implementation. Often identical to the directory name in which
+ the implementation files are placed.
+
+ Returns: values expected to be used as program's exit code.
+ 0: server has run and finished successfully.
+ 1: some error happens
+
+ """
+ try:
+ self.__module_name = module_name
+ shutdown_sighandler = \
+ lambda signal, frame: self._trigger_shutdown()
+ signal.signal(signal.SIGTERM, shutdown_sighandler)
+ signal.signal(signal.SIGINT, shutdown_sighandler)
+ self._setup_ccsession()
+ self._setup_module()
+ self._run_internal()
+ logger.info(PYSERVER_COMMON_SERVER_STOPPED, self.__module_name)
+ return 0
+ except BIND10ServerFatal as ex:
+ logger.error(PYSERVER_COMMON_SERVER_FATAL, self.__module_name,
+ ex)
+ except Exception as ex:
+ logger.error(PYSERVER_COMMON_UNCAUGHT_EXCEPTION, type(ex).__name__,
+ ex)
+
+ return 1
diff --git a/src/lib/python/isc/server_common/datasrc_clients_mgr.py b/src/lib/python/isc/server_common/datasrc_clients_mgr.py
new file mode 100644
index 0000000..4f565df
--- /dev/null
+++ b/src/lib/python/isc/server_common/datasrc_clients_mgr.py
@@ -0,0 +1,173 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import isc.dns
+import isc.datasrc
+import threading
+import json
+
+class ConfigError(Exception):
+ """Exception class raised for data source configuration errors."""
+ pass
+
+class DataSrcClientsMgr:
+ """A container of data source client lists.
+
+ This class represents a set of isc.datasrc.ConfigurableClientList
+ objects (currently per RR class), and provides APIs to configure
+ the lists and access to a specific list in a thread safe manner.
+
+ It is intended to be used by applications that refer to the global
+ 'data_sources' module. The reconfigure() method can be called from
+ a configuration callback for the module of the application. The
+ get_client_list() method is a simple search method to get the configured
+ ConfigurableClientList object for a specified RR class (if any),
+ while still allowing a separate thread to reconfigure the entire lists.
+
+ """
+ def __init__(self, use_cache=False):
+ """Constructor.
+
+ In the initial implementation, most user applications of this class
+ are generally expected to NOT use in-memory cache; the only expected
+ exception is the memory (cache) manager, which, by definition,
+ needs to deal with in-memory data. In future, some more applications
+ such as outbound zone transfer may want to set it to True.
+
+ Parameter:
+ use_cache (bool): If set to True, enable in-memory cache on
+ (re)configuration.
+
+ """
+ self.__use_cache = use_cache
+
+ # Map from RRClass to ConfigurableClientList. Resetting this map
+ # is protected by __map_lock. Note that this lock doesn't protect
+ # "updates" of the map content (currently it's not a problem, but
+ # if and when we support more operations such as reloading
+ # particular zones in in-memory cache, remember that there will have
+ # to be an additional layer of protection).
+ self.__clients_map = {}
+ self.__map_lock = threading.Lock()
+
+ # The generation ID of the configuration corresponding to
+ # current __clinets_map. Until we support the concept of generations
+ # in the configuration framework, we tentatively maintain it within
+ # this class.
+ self.__gen_id = 0
+
+ def get_clients_map(self):
+ """Returns a dict from RR class to ConfigurableClientList with gen ID.
+
+ It corresponds to the generation of data source configuration at the
+ time of the call. It can be safely called while reconfigure() is
+ called from another thread.
+
+ The mapping of the dict should be considered "frozen"; the caller
+ shouldn't modify the mapping (it can use the mapped objects in a
+ way modifying its internal state).
+
+ Note: in a future version we may also need to return the
+ "generation ID" of the corresponding configuration so the caller
+ application can handle migration between generations gradually.
+
+ """
+ with self.__map_lock:
+ return (self.__gen_id, self.__clients_map)
+
+ def get_client_list(self, rrclass):
+ """Return the configured ConfigurableClientList for the RR class.
+
+ If no client list is configured for the specified RR class, it
+ returns None.
+
+ This method should not raise an exception as long as the parameter
+ is of valid type.
+
+ This method can be safely called from a thread even if a different
+ thread is calling reconfigure(). Also, it's safe for the caller
+ to use the returned list even if reconfigure() is called while or
+ after the call to this thread.
+
+ Note that this class does not protect further access to the returned
+ list from multiple threads; it's the caller's responsbility to make
+ such access thread safe. In general, the find() method on the list
+ and the use of ZoneFinder created by a DataSourceClient in the list
+ cannot be done by multiple threads without explicit synchronization.
+ On the other hand, multiple threads can create and use ZoneUpdater,
+ ZoneIterator, or ZoneJournalReader on a DataSourceClient in parallel.
+
+ Parameter:
+ rrclass (isc.dns.RRClass): the RR class of the ConfigurableClientList
+ to be returned.
+ """
+ with self.__map_lock:
+ client_list = self.__clients_map.get(rrclass)
+ return client_list
+
+ def reconfigure(self, new_config, config_data):
+ """(Re)configure the set of client lists.
+
+ This method takes a new set of data source configuration, builds
+ a new set of ConfigurableClientList objects corresponding to the
+ configuration, and replaces the internal set with the newly built
+ one. Its parameter is expected to be the "new configuration"
+ parameter of a configuration update callback for the global
+ "data_sources" module. It should match the configuration data
+ of the module spec (see the datasrc.spec file).
+
+ Any error in reconfiguration is converted to a ConfigError
+ exception and is raised from the method. This method guarantees
+ strong exception safety: unless building a new set for the new
+ configuration is fully completed, the old set is intact.
+
+ This method can be called from a thread while some other thread
+ is calling get_client_list() and using the result (see
+ the description of get_client_list()). In general, however,
+ only one thread can call this method at one time; while data
+ integrity will still be preserved, the ordering of the change
+ will not be guaranteed if multiple threads call this method
+ at the same time.
+
+ Parameter:
+ new_config (dict): configuration data for the data_sources module
+ (actually unused in this method).
+ config_data (isc.config.ConfigData): the latest full config data
+ for the data_sources module. Usually the second parameter of
+ the (remote) configuration update callback for the module.
+
+ """
+ try:
+ new_map = {}
+ # We only refer to config_data, not new_config (diff from the
+ # previous). the latter may be empty for the initial default
+ # configuration while the former works for all cases.
+ for rrclass_cfg, class_cfg in \
+ config_data.get_value('classes')[0].items():
+ rrclass = isc.dns.RRClass(rrclass_cfg)
+ new_client_list = isc.datasrc.ConfigurableClientList(rrclass)
+ new_client_list.configure(json.dumps(class_cfg),
+ self.__use_cache)
+ new_map[rrclass] = new_client_list
+ with self.__map_lock:
+ self.__clients_map = new_map
+
+ # NOTE: when we support the concept of generations this should
+ # be retrieved from the configuration
+ self.__gen_id += 1
+ except Exception as ex:
+ # Catch all types of exceptions as a whole: there won't be much
+ # granularity for exceptions raised from the C++ module anyway.
+ raise ConfigError(ex)
diff --git a/src/lib/python/isc/server_common/server_common_messages.mes b/src/lib/python/isc/server_common/server_common_messages.mes
index f22ce65..54602f9 100644
--- a/src/lib/python/isc/server_common/server_common_messages.mes
+++ b/src/lib/python/isc/server_common/server_common_messages.mes
@@ -21,6 +21,9 @@
# have that at this moment. So when adding a message, make sure that
# the name is not already used in src/lib/config/config_messages.mes
+% PYSERVER_COMMON_COMMAND %1 server has received '%2' command
+The server process received the shown name of command from other module.
+
% PYSERVER_COMMON_DNS_TCP_SEND_DONE completed sending TCP message to %1 (%2 bytes in total)
Debug message. A complete DNS message has been successfully
transmitted over a TCP connection, possibly after multiple send
@@ -44,6 +47,18 @@ The destination address and the total size of the message that has
been transmitted so far (including the 2-byte length field) are shown
in the log message.
+% PYSERVER_COMMON_SERVER_FATAL %1 server has encountered a fatal error: %2
+The BIND 10 server process encountered a fatal error (normally specific to
+the particular program), and is forcing itself to shut down.
+
+% PYSERVER_COMMON_SERVER_STARTED %1 server has started
+The server process has successfully started and is now ready to receive
+commands and configuration updates.
+
+% PYSERVER_COMMON_SERVER_STOPPED %1 server has started
+The server process has successfully stopped and is no longer listening for or
+handling commands. Normally the process will soon exit.
+
% PYSERVER_COMMON_TSIG_KEYRING_DEINIT Deinitializing global TSIG keyring
A debug message noting that the global TSIG keyring is being removed from
memory. Most programs don't do that, they just exit, which is OK.
@@ -57,3 +72,8 @@ to be loaded from configuration.
A debug message. The TSIG keyring is being (re)loaded from configuration.
This happens at startup or when the configuration changes. The old keyring
is removed and new one created with all the keys.
+
+% PYSERVER_COMMON_UNCAUGHT_EXCEPTION uncaught exception of type %1: %2
+The BIND 10 server process encountered an uncaught exception and will now shut
+down. This is indicative of a programming error and should not happen under
+normal circumstances. The exception type and message are printed.
diff --git a/src/lib/python/isc/server_common/tests/.gitignore b/src/lib/python/isc/server_common/tests/.gitignore
new file mode 100644
index 0000000..dfeb2e5
--- /dev/null
+++ b/src/lib/python/isc/server_common/tests/.gitignore
@@ -0,0 +1,2 @@
+/datasrc.spec
+/zone.sqlite3
diff --git a/src/lib/python/isc/server_common/tests/Makefile.am b/src/lib/python/isc/server_common/tests/Makefile.am
index fff57d6..bf6b26c 100644
--- a/src/lib/python/isc/server_common/tests/Makefile.am
+++ b/src/lib/python/isc/server_common/tests/Makefile.am
@@ -1,5 +1,6 @@
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = tsig_keyring_test.py dns_tcp_test.py
+PYTESTS = tsig_keyring_test.py dns_tcp_test.py datasrc_clients_mgr_test.py
+PYTESTS += bind10_server_test.py
EXTRA_DIST = $(PYTESTS)
# If necessary (rare cases), explicitly specify paths to dynamic libraries
@@ -9,6 +10,14 @@ if SET_ENV_LIBRARY_PATH
LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
endif
+# We use our own "default" datasrc.spec, tweaking some installation path,
+# so we can run the tests with something very close to the actual spec and
+# yet independent from installation environment.
+BUILT_SOURCES = datasrc.spec
+datasrc.spec: $(abs_top_builddir)/src/bin/cfgmgr/plugins/datasrc.spec.pre
+ $(SED) -e "s|@@STATIC_ZONE_FILE@@|$(abs_top_builddir)/src/lib/datasrc/static.zone|;s|@@SQLITE3_DATABASE_FILE@@|$(abs_builddir)/zone.sqlite3|" $(abs_top_builddir)/src/bin/cfgmgr/plugins/datasrc.spec.pre >$@
+CLEANFILES = datasrc.spec zone.sqlite3
+
# test using command-line arguments, so use check-local target instead of TESTS
check-local:
if ENABLE_PYTHON_COVERAGE
@@ -21,5 +30,7 @@ endif
$(LIBRARY_PATH_PLACEHOLDER) \
PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
+ B10_FROM_SOURCE=$(abs_top_srcdir) \
+ B10_FROM_BUILD=$(abs_top_builddir) \
$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
done
diff --git a/src/lib/python/isc/server_common/tests/bind10_server_test.py b/src/lib/python/isc/server_common/tests/bind10_server_test.py
new file mode 100755
index 0000000..f93eed6
--- /dev/null
+++ b/src/lib/python/isc/server_common/tests/bind10_server_test.py
@@ -0,0 +1,294 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import errno
+import os
+import signal
+
+import isc.log
+import isc.config
+from isc.server_common.bind10_server import *
+from isc.testutils.ccsession_mock import MockModuleCCSession
+
+TEST_FILENO = 42 # arbitrarily chosen
+
+class TestException(Exception):
+ """A generic exception class specific in this test module."""
+ pass
+
+class MyCCSession(MockModuleCCSession, isc.config.ConfigData):
+ def __init__(self, specfile, config_handler, command_handler):
+ # record parameter for later inspection
+ self.specfile_param = specfile
+ self.config_handler_param = config_handler
+ self.command_handler_param = command_handler
+
+ self.check_command_param = None # used in check_command()
+
+ # Initialize some local attributes of MockModuleCCSession, including
+ # 'stopped'
+ MockModuleCCSession.__init__(self)
+
+ def start(self):
+ pass
+
+ def check_command(self, nonblock):
+ """Mock check_command(). Just record the param for later inspection."""
+ self.check_command_param = nonblock
+
+ def get_socket(self):
+ return self
+
+ def fileno(self):
+ """Pretending get_socket().fileno()
+
+ Returing an arbitrarily chosen constant.
+
+ """
+ return TEST_FILENO
+
+class MockServer(BIND10Server):
+ def __init__(self):
+ BIND10Server.__init__(self)
+ self._select_fn = self.select_wrapper
+
+ def _setup_ccsession(self):
+ orig_cls = isc.config.ModuleCCSession
+ isc.config.ModuleCCSession = MyCCSession
+ try:
+ super()._setup_ccsession()
+ except Exception:
+ raise
+ finally:
+ isc.config.ModuleCCSession = orig_cls
+
+ def _config_handler(self):
+ pass
+
+ def mod_command_handler(self, cmd, args):
+ """A sample _mod_command_handler implementation."""
+ self.command_handler_params = (cmd, args) # for inspection
+ return isc.config.create_answer(0)
+
+ def select_wrapper(self, reads, writes, errors):
+ self._trigger_shutdown() # make sure the loop will stop
+ self.select_params = (reads, writes, errors) # record for inspection
+ return [], [], []
+
+class TestBIND10Server(unittest.TestCase):
+ def setUp(self):
+ self.__server = MockServer()
+ self.__reads = 0
+ self.__writes = 0
+ self.__errors = 0
+
+ def test_init(self):
+ """Check initial conditions"""
+ self.assertFalse(self.__server.shutdown)
+
+ def test_trigger_shutdown(self):
+ self.__server._trigger_shutdown()
+ self.assertTrue(self.__server.shutdown)
+
+ def test_sigterm_handler(self):
+ """Check the signal handler behavior.
+
+ SIGTERM and SIGINT should be caught and should call memmgr's
+ _trigger_shutdown(). This test also indirectly confirms run() calls
+ run_internal().
+
+ """
+ def checker():
+ self.__shutdown_called = True
+
+ self.__server._run_internal = lambda: os.kill(os.getpid(),
+ signal.SIGTERM)
+ self.__server._trigger_shutdown = lambda: checker()
+ self.assertEqual(0, self.__server.run('test'))
+ self.assertTrue(self.__shutdown_called)
+
+ self.__shutdown_called = False
+ self.__server._run_internal = lambda: os.kill(os.getpid(),
+ signal.SIGINT)
+ self.assertEqual(0, self.__server.run('test'))
+ self.assertTrue(self.__shutdown_called)
+
+ def test_exception(self):
+ """Check exceptions are handled, not leaked."""
+ def exception_raiser(ex_cls):
+ raise ex_cls('test')
+
+ # Test all possible exceptions that are explicitly caught
+ for ex in [TestException, BIND10ServerFatal]:
+ self.__server._run_internal = lambda: exception_raiser(ex)
+ self.assertEqual(1, self.__server.run('test'))
+
+ def test_run(self):
+ """Check other behavior of run()"""
+ self.__server._run_internal = lambda: None # prevent looping
+ self.assertEqual(0, self.__server.run('test'))
+ # module CC session should have been setup.
+ # The exact path to the spec file can vary, so we simply check
+ # it works and it's the expected name stripping the path.
+ self.assertEqual(
+ self.__server.mod_ccsession.specfile_param.split('/')[-1],
+ 'test.spec')
+ self.assertEqual(self.__server.mod_ccsession.config_handler_param,
+ self.__server._config_handler)
+ self.assertEqual(self.__server.mod_ccsession.command_handler_param,
+ self.__server._command_handler)
+
+ def test_run_with_setup_module(self):
+ """Check run() with module specific setup method."""
+ self.setup_called = False
+ def check_called():
+ self.setup_called = True
+ self.__server._run_internal = lambda: None
+ self.__server._setup_module = check_called
+ self.assertEqual(0, self.__server.run('test'))
+ self.assertTrue(self.setup_called)
+
+ def test_shutdown_command(self):
+ answer = self.__server._command_handler('shutdown', None)
+ self.assertTrue(self.__server.shutdown)
+ self.assertEqual((0, None), isc.config.parse_answer(answer))
+
+ def test_run_with_shutdown_module(self):
+ """Check run() with module specific shutdown method."""
+ self.shutdown_called = False
+ def check_called():
+ self.shutdown_called = True
+ self.__server.__shutdown = True
+ self.__server._shutdown_module = check_called
+ self.assertEqual(0, self.__server.run('test'))
+ self.assertTrue(self.shutdown_called)
+
+ def test_other_command(self):
+ self.__server._mod_command_handler = self.__server.mod_command_handler
+ answer = self.__server._command_handler('other command', None)
+ # shouldn't be confused with shutdown
+ self.assertFalse(self.__server.shutdown)
+ self.assertEqual((0, None), isc.config.parse_answer(answer))
+ self.assertEqual(('other command', None),
+ self.__server.command_handler_params)
+
+ def test_other_command_nohandler(self):
+ """Similar to test_other_command, but without explicit handler"""
+ # In this case "unknown command" error should be returned.
+ answer = self.__server._command_handler('other command', None)
+ self.assertEqual(1, isc.config.parse_answer(answer)[0])
+
+ def test_run_internal(self):
+ self.__server._setup_ccsession()
+ self.__server._run_internal()
+ self.assertEqual(([TEST_FILENO], [], []), self.__server.select_params)
+
+ def select_wrapper(self, r, w, e, ex=None, ret=None):
+ """Mock select() function used some of the tests below.
+
+ If ex is not None and it's first call to this method, it raises ex
+ assuming it's an exception.
+
+ If ret is not None, it returns the given value; otherwise it returns
+ all empty lists.
+
+ """
+ self.select_params.append((r, w, e))
+ if ex is not None and len(self.select_params) == 1:
+ raise ex
+ else:
+ self.__server._trigger_shutdown()
+ if ret is not None:
+ return ret
+ return [], [], []
+
+ def test_select_for_command(self):
+ """A normal event iteration, handling one command."""
+ self.select_params = []
+ self.__server._select_fn = \
+ lambda r, w, e: self.select_wrapper(r, w, e,
+ ret=([TEST_FILENO], [], []))
+ self.__server._setup_ccsession()
+ self.__server._run_internal()
+ # select should be called only once.
+ self.assertEqual([([TEST_FILENO], [], [])], self.select_params)
+ # check_command should have been called.
+ self.assertTrue(self.__server.mod_ccsession.check_command_param)
+ # module CC session should have been stopped explicitly.
+ self.assertTrue(self.__server.mod_ccsession.stopped)
+
+ def test_select_interrupted(self):
+ """Emulating case select() raises EINTR."""
+ self.select_params = []
+ self.__server._select_fn = \
+ lambda r, w, e: self.select_wrapper(r, w, e,
+ ex=select.error(errno.EINTR))
+ self.__server._setup_ccsession()
+ self.__server._run_internal()
+ # EINTR will be ignored and select() will be called again.
+ self.assertEqual([([TEST_FILENO], [], []), ([TEST_FILENO], [], [])],
+ self.select_params)
+ # check_command() shouldn't have been called (select_wrapper returns
+ # empty lists by default).
+ self.assertIsNone(self.__server.mod_ccsession.check_command_param)
+ self.assertTrue(self.__server.mod_ccsession.stopped)
+
+ def test_select_other_exception(self):
+ """Emulating case select() raises other select error."""
+ self.select_params = []
+ self.__server._select_fn = \
+ lambda r, w, e: self.select_wrapper(r, w, e,
+ ex=select.error(errno.EBADF))
+ self.__server._setup_ccsession()
+ # the exception will be propagated.
+ self.assertRaises(select.error, self.__server._run_internal)
+ self.assertEqual([([TEST_FILENO], [], [])], self.select_params)
+ # in this case module CC session hasn't been stopped explicitly
+ # others will notice it due to connection reset.
+ self.assertFalse(self.__server.mod_ccsession.stopped)
+
+ def my_read_callback(self):
+ self.__reads += 1
+
+ def my_write_callback(self):
+ self.__writes += 1
+
+ def my_error_callback(self):
+ self.__errors += 1
+
+ def test_watch_fileno(self):
+ """Test watching for fileno."""
+ self.select_params = []
+ self.__server._select_fn = \
+ lambda r, w, e: self.select_wrapper(r, w, e,
+ ret=([10, 20, 42, TEST_FILENO], [], [30]))
+ self.__server._setup_ccsession()
+
+ self.__server.watch_fileno(10, rcallback=self.my_read_callback)
+ self.__server.watch_fileno(20, rcallback=self.my_read_callback, \
+ wcallback=self.my_write_callback)
+ self.__server.watch_fileno(30, xcallback=self.my_error_callback)
+
+ self.__server._run_internal()
+ self.assertEqual([([10, 20, TEST_FILENO], [20], [30])], self.select_params)
+ self.assertEqual(2, self.__reads)
+ self.assertEqual(0, self.__writes)
+ self.assertEqual(1, self.__errors)
+
+if __name__== "__main__":
+ isc.log.init("bind10_server_test")
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py b/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py
new file mode 100644
index 0000000..e717d65
--- /dev/null
+++ b/src/lib/python/isc/server_common/tests/datasrc_clients_mgr_test.py
@@ -0,0 +1,142 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import isc.log
+from isc.server_common.datasrc_clients_mgr import *
+from isc.dns import *
+import unittest
+import isc.config
+import os
+
+# A (slightly tweaked) local copy of the default data source spec
+DATASRC_SPECFILE = os.environ["B10_FROM_BUILD"] + \
+ "/src/lib/python/isc/server_common/tests/datasrc.spec"
+DEFAULT_CONFIG = \
+ isc.config.ConfigData(isc.config.module_spec_from_file(DATASRC_SPECFILE)).\
+ get_full_config()
+
+class DataSrcClientsMgrTest(unittest.TestCase):
+ def setUp(self):
+ # We construct the manager with enabling in-memory cache for easier
+ # tests. There should be no risk of inter-thread issues in the tests.
+ self.__mgr = DataSrcClientsMgr(use_cache=True)
+ self.__datasrc_cfg = isc.config.ConfigData(
+ isc.config.module_spec_from_file(DATASRC_SPECFILE))
+
+ def test_init(self):
+ """Check some initial state.
+
+ Initially there's no client list available, but get_client_list
+ doesn't cause disruption.
+ """
+ self.assertIsNone(self.__mgr.get_client_list(RRClass.IN))
+ self.assertIsNone(self.__mgr.get_client_list(RRClass.CH))
+
+ def test_reconfigure(self):
+ """Check configuration behavior.
+
+ First try the default configuration, and replace it with something
+ else.
+ """
+
+ # There should be at least in-memory only data for the static
+ # bind/CH zone. (We don't assume the existence of SQLite3 datasrc,
+ # so it'll still work if and when we make the default DB-independent).
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ clist = self.__mgr.get_client_list(RRClass.CH)
+ self.assertIsNotNone(clist)
+ self.assertTrue(clist.find(Name('bind'), True, False)[2])
+
+ # Reconfigure it with a simple new config: the list for CH will be
+ # gone, and and an empty list for IN will be installed.
+ self.__datasrc_cfg.set_local_config({"classes": {"IN": []}})
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ self.assertIsNone(self.__mgr.get_client_list(RRClass.CH))
+ self.assertIsNotNone(self.__mgr.get_client_list(RRClass.IN))
+
+ def test_reconfigure_error(self):
+ """Check reconfigure failure preserves the old config."""
+ # Configure it with the default
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+ # Then try invalid configuration
+ self.assertRaises(ConfigError, self.__mgr.reconfigure, {}, 42)
+ self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+ # Another type of invalid configuration: exception would come from
+ # the C++ wrapper.
+ self.__datasrc_cfg.set_local_config({"classes": {"IN": 42}})
+ self.assertRaises(ConfigError,
+ self.__mgr.reconfigure, {}, self.__datasrc_cfg)
+ self.assertIsNotNone(self.__mgr.get_client_list(RRClass.CH))
+
+ def check_client_list_content(self, clist):
+ """Some set of checks on given data source client list.
+
+ Used by a couple of tests below.
+ """
+ datasrc_client, finder, exact = clist.find(Name('bind'))
+ self.assertTrue(exact)
+
+ # Reset the client list
+ clist = None
+
+ # Both finder and datasrc client should still work without causing
+ # disruption. We shouldn't have to inspect too much details of the
+ # returned values.
+ result, rrset, _ = finder.find(Name('bind'), RRType.SOA)
+ self.assertEqual(Name('bind'), rrset.get_name())
+ self.assertEqual(RRType.SOA, rrset.get_type())
+ self.assertEqual(RRClass.CH, rrset.get_class())
+ self.assertEqual(RRTTL(0), rrset.get_ttl())
+
+ # iterator should produce some non empty set of RRsets
+ rrsets = datasrc_client.get_iterator(Name('bind'))
+ self.assertNotEqual(0, len(list(rrsets)))
+
+ def test_reconfig_while_using_old(self):
+ """Check datasrc client and finder can work even after list is gone."""
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ clist = self.__mgr.get_client_list(RRClass.CH)
+
+ self.__datasrc_cfg.set_local_config({"classes": {"IN": []}})
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ self.check_client_list_content(clist)
+
+ def test_get_clients_map(self):
+ # This is basically a trivial getter, so it should be sufficient
+ # to check we can call it as we expect.
+
+ # Initially map iss empty, the generation ID is 0.
+ self.assertEqual((0, {}), self.__mgr.get_clients_map())
+
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ genid, clients_map = self.__mgr.get_clients_map()
+ self.assertEqual(1, genid)
+ self.assertEqual(2, len(clients_map)) # should contain 'IN' and 'CH'
+
+ # Check the retrieved map is usable even after further reconfig().
+ self.__datasrc_cfg.set_local_config({"classes": {"IN": []}})
+ self.__mgr.reconfigure({}, self.__datasrc_cfg)
+ self.check_client_list_content(clients_map[RRClass.CH])
+
+ # generation ID should be incremented again
+ self.assertEqual(2, self.__mgr.get_clients_map()[0])
+
+if __name__ == "__main__":
+ isc.log.init("bind10")
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/lib/python/isc/statistics/Makefile.am b/src/lib/python/isc/statistics/Makefile.am
index 9be1312..699004e 100644
--- a/src/lib/python/isc/statistics/Makefile.am
+++ b/src/lib/python/isc/statistics/Makefile.am
@@ -1,6 +1,6 @@
SUBDIRS = . tests
-python_PYTHON = __init__.py counters.py
+python_PYTHON = __init__.py counters.py dns.py
pythondir = $(pyexecdir)/isc/statistics
CLEANDIRS = __pycache__
diff --git a/src/lib/python/isc/statistics/counters.py b/src/lib/python/isc/statistics/counters.py
index 279c14b..dcdd247 100644
--- a/src/lib/python/isc/statistics/counters.py
+++ b/src/lib/python/isc/statistics/counters.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 Internet Systems Consortium.
+# Copyright (C) 2012-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -17,52 +17,34 @@
This module handles the statistics counters for BIND 10 modules. For
using the module `counter.py`, first a counters object should be created
-in each module (like b10-xfrin or b10-xfrout) after importing this
-module. A spec file can be specified as an argument when creating the
-counters object:
+in each module like b10-foo after importing this module. A spec file can
+be specified as an argument when creating the counters object:
from isc.statistics import Counters
self.counters = Counters("/path/to/foo.spec")
The first argument of Counters() can be specified, which is the location
-of the specification file (like src/bin/xfrout/xfrout.spec). If Counters
-is constructed this way, statistics counters can be accessed from each
-module. For example, in case that the item `xfrreqdone` is defined in
-statistics_spec in xfrout.spec, the following methods are
-callable. Since these methods require the string of the zone name in the
-first argument, if we have the following code in b10-xfrout:
+of the specification file. If Counters is constructed this way,
+statistics counters can be accessed from each module. For example, in
+case that the item `counter1` is defined in statistics_spec in foo.spec,
+the following methods are callable.
- self.counters.inc('zones', zone_name, 'xfrreqdone')
+ self.counters.inc('counter1')
-then the counter for xfrreqdone corresponding to zone_name is
-incremented. For getting the current number of this counter, we can use
-the following code:
+Then the counter for `counter1` is incremented. For getting the current
+number of this counter, we can use the following code:
- number = self.counters.get('zones', zone_name, 'xfrreqdone')
+ number = self.counters.get('counter1')
-then the current count is obtained and set in the variable
+Then the current count is obtained and set in the variable
`number`. Such a getter method would be mainly used for unit-testing.
-As other example, for the item `axfr_running`, the decrementer method is
-also callable. This method is used for decrementing a counter. For the
-item `axfr_running`, an argument like zone name is not required:
+The decrementer method is also callable. This method is used for
+decrementing a counter as well as inc().
- self.counters.dec('axfr_running')
+ self.counters.dec('counter2')
-These methods are effective in other modules. For example, in case that
-this module `counter.py` is once imported in a main module such as
-b10-xfrout, then for the item `notifyoutv4`, the `inc()` method can be
-invoked in another module such as notify_out.py, which is firstly
-imported in the main module.
-
- self.counters.inc('zones', zone_name, 'notifyoutv4')
-
-In this example this is for incrementing the counter of the item
-`notifyoutv4`. Thus, such statement can be also written in another
-library like isc.notify.notify_out. If this module `counter.py` isn't
-imported in the main module but imported in such a library module as
-isc.notify.notify_out, in this example, empty methods would be invoked,
-which is directly defined in `counter.py`.
-"""
+Some other methods accessible to a counter are provided by this
+module."""
import threading
import isc.config
@@ -81,19 +63,47 @@ def _add_counter(element, spec, identifier):
return isc.cc.data.find(element, identifier)
except isc.cc.data.DataNotFoundError:
pass
- # check whether spec and identifier are correct
- isc.config.find_spec_part(spec, identifier)
- # examine spec of the top-level item first
- spec_ = isc.config.find_spec_part(spec, identifier.split('/')[0])
- if spec_['item_type'] == 'named_set' and \
- spec_['named_set_item_spec']['item_type'] == 'map':
- map_spec = spec_['named_set_item_spec']['map_item_spec']
- for name in isc.config.spec_name_list(map_spec):
- spec_ = isc.config.find_spec_part(map_spec, name)
- id_str = '%s/%s/%s' % \
- tuple(identifier.split('/')[0:2] + [name])
- isc.cc.data.set(element, id_str, spec_['item_default'])
- else:
+
+ # Note: If there is a named_set type item in the statistics spec
+ # and if there are map type items under it, all of items under the
+ # map type item need to be added. For example, we're assuming that
+ # this method is now adding a counter whose identifier is like
+ # dir1/dir2/dir3/counter1. If both of dir1 and dir2 are named_set
+ # types, and if dir3 is a map type, and if counter1, counter2, and
+ # counter3 are defined as items under dir3 by the statistics spec,
+ # this method would add other two counters:
+ #
+ # dir1/dir2/dir3/counter2
+ # dir1/dir2/dir3/counter3
+ #
+ # Otherwise this method just adds the only counter
+ # dir1/dir2/dir3/counter1.
+
+ # examine spec from the top-level item and know whether
+ # has_named_set, and check whether spec and identifier are correct
+ pidr = ''
+ has_named_set = False
+ for idr in identifier.split('/'):
+ if len(pidr) > 0:
+ idr = pidr + '/' + idr
+ spec_ = isc.config.find_spec_part(spec, idr)
+ if isc.config.spec_part_is_named_set(spec_):
+ has_named_set = True
+ break
+ pidr = idr
+ # add all elements in map type if has_named_set
+ has_map = False
+ if has_named_set:
+ p_idr = identifier.rsplit('/', 1)[0]
+ p_spec = isc.config.find_spec_part(spec, p_idr)
+ if isc.config.spec_part_is_map(p_spec):
+ has_map = True
+ for name in isc.config.spec_name_list(p_spec['map_item_spec']):
+ idr_ = p_idr + '/' + name
+ spc_ = isc.config.find_spec_part(spec, idr_)
+ isc.cc.data.set(element, idr_, spc_['item_default'])
+ # otherwise add a specific element
+ if not has_map:
spec_ = isc.config.find_spec_part(spec, identifier)
isc.cc.data.set(element, identifier, spec_['item_default'])
return isc.cc.data.find(element, identifier)
@@ -141,54 +151,6 @@ def _concat(*args, sep='/'):
"""
return sep.join(args)
-class _Statistics():
- """Statistics data set"""
- # default statistics data
- _data = {}
- # default statistics spec used in case the specfile is omitted when
- # constructing a Counters() object
- _spec = [
- {
- "item_name": "zones",
- "item_type": "named_set",
- "item_optional": False,
- "item_default": {
- "_SERVER_" : {
- "notifyoutv4" : 0,
- "notifyoutv6" : 0
- }
- },
- "item_title": "Zone names",
- "item_description": "Zone names",
- "named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "item_title": "Zone name",
- "item_description": "Zone name",
- "map_item_spec": [
- {
- "item_name": "notifyoutv4",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "IPv4 notifies",
- "item_description": "Number of IPv4 notifies per zone name sent out"
- },
- {
- "item_name": "notifyoutv6",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "IPv6 notifies",
- "item_description": "Number of IPv6 notifies per zone name sent out"
- }
- ]
- }
- }
- ]
-
class Counters():
"""A class for holding and manipulating all statistics counters
for a module. A Counters object may be created by specifying a spec
@@ -201,87 +163,27 @@ class Counters():
stop_timer() and get() are useful for this. Saved counters can be
cleared by the method clear_all(). Manipulating counters and
timers can be temporarily disabled. If disabled, counter values are
- not changed even if methods to update them are invoked. Including
- per-zone counters, a list of counters which can be handled in the
- class are like the following:
-
- zones/example.com./notifyoutv4
- zones/example.com./notifyoutv6
- zones/example.com./xfrrej
- zones/example.com./xfrreqdone
- zones/example.com./soaoutv4
- zones/example.com./soaoutv6
- zones/example.com./axfrreqv4
- zones/example.com./axfrreqv6
- zones/example.com./ixfrreqv4
- zones/example.com./ixfrreqv6
- zones/example.com./xfrsuccess
- zones/example.com./xfrfail
- zones/example.com./last_ixfr_duration
- zones/example.com./last_axfr_duration
- ixfr_running
- axfr_running
- socket/unixdomain/open
- socket/unixdomain/openfail
- socket/unixdomain/close
- socket/unixdomain/bindfail
- socket/unixdomain/acceptfail
- socket/unixdomain/accept
- socket/unixdomain/senderr
- socket/unixdomain/recverr
- socket/ipv4/tcp/open
- socket/ipv4/tcp/openfail
- socket/ipv4/tcp/close
- socket/ipv4/tcp/connfail
- socket/ipv4/tcp/conn
- socket/ipv4/tcp/senderr
- socket/ipv4/tcp/recverr
- socket/ipv6/tcp/open
- socket/ipv6/tcp/openfail
- socket/ipv6/tcp/close
- socket/ipv6/tcp/connfail
- socket/ipv6/tcp/conn
- socket/ipv6/tcp/senderr
- socket/ipv6/tcp/recverr
- """
-
- # '_SERVER_' is a special zone name representing an entire
- # count. It doesn't mean a specific zone, but it means an
- # entire count in the server.
- _entire_server = '_SERVER_'
- # zone names are contained under this dirname in the spec file.
- _perzone_prefix = 'zones'
- # default statistics data set
- _statistics = _Statistics()
+ not changed even if methods to update them are invoked."""
- def __init__(self, spec_file_name=None):
+ def __init__(self, spec_file_name):
"""A constructor for the Counters class. A path to the spec file
- can be specified in spec_file_name. Statistics data based on
- statistics spec can be accumulated if spec_file_name is
- specified. If omitted, a default statistics spec is used. The
- default statistics spec is defined in a hidden class named
- _Statistics().
+ can be specified in spec_file_name, which is required. Statistics data
+ based on statistics spec can be accumulated. If an invalid argument
+ including None is specified, ModuleSpecError might be raised.
"""
self._zones_item_list = []
self._start_time = {}
self._disabled = False
self._rlock = threading.RLock()
- if not spec_file_name: return
- # change the default statistics spec
- self._statistics._spec = \
+ self._statistics_data = {}
+ self._statistics_spec = \
isc.config.module_spec_from_file(spec_file_name).\
get_statistics_spec()
- if self._perzone_prefix in \
- isc.config.spec_name_list(self._statistics._spec):
- self._zones_item_list = isc.config.spec_name_list(
- isc.config.find_spec_part(
- self._statistics._spec, self._perzone_prefix)\
- ['named_set_item_spec']['map_item_spec'])
def clear_all(self):
"""clears all statistics data"""
with self._rlock:
- self._statistics._data = {}
+ self._statistics_data = {}
def disable(self):
"""disables incrementing/decrementing counters"""
@@ -301,8 +203,8 @@ class Counters():
identifier = _concat(*args)
with self._rlock:
if self._disabled: return
- _inc_counter(self._statistics._data,
- self._statistics._spec,
+ _inc_counter(self._statistics_data,
+ self._statistics_spec,
identifier, step)
def inc(self, *args):
@@ -322,7 +224,7 @@ class Counters():
of the specified counter. isc.cc.data.DataNotFoundError is
raised when the counter doesn't have a number yet."""
identifier = _concat(*args)
- return _get_counter(self._statistics._data, identifier)
+ return _get_counter(self._statistics_data, identifier)
def start_timer(self, *args):
"""Starts a timer which is identified by args and keeps it
@@ -353,8 +255,8 @@ class Counters():
# set the end time
_stop_timer(
start_time,
- self._statistics._data,
- self._statistics._spec,
+ self._statistics_data,
+ self._statistics_spec,
identifier)
# A datetime value of once used timer should be deleted
# for a future use.
@@ -371,33 +273,9 @@ class Counters():
del branch_map[leaf]
def get_statistics(self):
- """Calculates an entire server's counts, and returns statistics
- data in a format to send out to the stats module, including each
- counter. If nothing is counted yet, then it returns an empty
- dictionary."""
+ """Returns statistics data in a format to send out to the
+ stats module, including each counter. If nothing is counted
+ yet, then it returns an empty dictionary."""
# entire copy
- statistics_data = self._statistics._data.copy()
- # If there is no 'zones' found in statistics_data,
- # i.e. statistics_data contains no per-zone counter, it just
- # returns statistics_data because calculating total counts
- # across the zone names isn't necessary.
- if self._perzone_prefix not in statistics_data:
- return statistics_data
- zones = statistics_data[self._perzone_prefix]
- # Start calculation for '_SERVER_' counts
- zones_spec = isc.config.find_spec_part(self._statistics._spec,
- self._perzone_prefix)
- zones_attrs = zones_spec['item_default'][self._entire_server]
- zones_data = {}
- for attr in zones_attrs:
- id_str = '%s/%s' % (self._entire_server, attr)
- sum_ = 0
- for name in zones:
- if attr in zones[name]:
- sum_ += zones[name][attr]
- _set_counter(zones_data, zones_spec, id_str, sum_)
- # insert entire-server counts
- statistics_data[self._perzone_prefix] = dict(
- statistics_data[self._perzone_prefix],
- **zones_data)
+ statistics_data = self._statistics_data.copy()
return statistics_data
diff --git a/src/lib/python/isc/statistics/dns.py b/src/lib/python/isc/statistics/dns.py
new file mode 100644
index 0000000..2cd5fb6
--- /dev/null
+++ b/src/lib/python/isc/statistics/dns.py
@@ -0,0 +1,166 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""BIND 10 statistics counters module for DNS
+
+This module handles the statistics counters for BIND 10 modules for a
+DNS-specific purpose. For using the module `counter.py`, first a
+counters object should be created in each module (like b10-xfrin or
+b10-xfrout) after importing this module. A spec file can be specified as
+an argument when creating the counters object:
+
+ from isc.statistics.dns import Counters
+ self.counters = Counters("/path/to/xfrout/xfrout.spec")
+
+The first argument of Counters() can be specified, which is the location
+of the specification file. If Counters is constructed this way,
+statistics counters can be accessed from each module. For example, in
+case that the item `xfrreqdone` is defined in statistics_spec in
+xfrout.spec, the following methods are callable. Since these methods
+require the string of the zone name in the first argument, if we have
+the following code in b10-xfrout:
+
+ self.counters.inc('zones', zone_name, 'xfrreqdone')
+
+then the counter for xfrreqdone corresponding to zone_name is
+incremented. For getting the current number of this counter, we can use
+the following code:
+
+ number = self.counters.get('zones', zone_name, 'xfrreqdone')
+
+then the current count is obtained and set in the variable
+`number`. Such a getter method would be mainly used for unit-testing.
+As other example, for the item `axfr_running`, the decrementer method is
+also callable. This method is used for decrementing a counter. For the
+item `axfr_running`, an argument like zone name is not required:
+
+ self.counters.dec('axfr_running')
+
+These methods are effective in other modules. For example, in case that
+this module `counters.py` is once imported in a main module such as
+b10-xfrout, then for the item `notifyoutv4`, the `inc()` method can be
+invoked in another module such as notify_out.py, which is firstly
+imported in the main module.
+
+ self.counters.inc('zones', zone_name, 'notifyoutv4')
+
+In this example this is for incrementing the counter of the item
+`notifyoutv4`. Thus, such statement can be also written in another
+library like isc.notify.notify_out. If this module `counter.py` isn't
+imported in the main module but imported in such a library module as
+isc.notify.notify_out, in this example, empty methods would be invoked,
+which is directly defined in `counter.py`.
+
+This module basically inherits isc.statistics.counters. Also see
+documentation for isc.statistics.counters for details."""
+
+import isc.config
+from isc.statistics import counters
+
+class Counters(counters.Counters):
+ """A list of counters which can be handled in the class are like
+ the following. Also see documentation for
+ isc.statistics.counters.Counters for details.
+
+ zones/IN/example.com./notifyoutv4
+ zones/IN/example.com./notifyoutv6
+ zones/IN/example.com./xfrrej
+ zones/IN/example.com./xfrreqdone
+ zones/IN/example.com./soaoutv4
+ zones/IN/example.com./soaoutv6
+ zones/IN/example.com./axfrreqv4
+ zones/IN/example.com./axfrreqv6
+ zones/IN/example.com./ixfrreqv4
+ zones/IN/example.com./ixfrreqv6
+ zones/IN/example.com./xfrsuccess
+ zones/IN/example.com./xfrfail
+ zones/IN/example.com./last_ixfr_duration
+ zones/IN/example.com./last_axfr_duration
+ ixfr_running
+ axfr_running
+ socket/unixdomain/open
+ socket/unixdomain/openfail
+ socket/unixdomain/close
+ socket/unixdomain/bindfail
+ socket/unixdomain/acceptfail
+ socket/unixdomain/accept
+ socket/unixdomain/senderr
+ socket/unixdomain/recverr
+ socket/ipv4/tcp/open
+ socket/ipv4/tcp/openfail
+ socket/ipv4/tcp/close
+ socket/ipv4/tcp/connfail
+ socket/ipv4/tcp/conn
+ socket/ipv4/tcp/senderr
+ socket/ipv4/tcp/recverr
+ socket/ipv6/tcp/open
+ socket/ipv6/tcp/openfail
+ socket/ipv6/tcp/close
+ socket/ipv6/tcp/connfail
+ socket/ipv6/tcp/conn
+ socket/ipv6/tcp/senderr
+ socket/ipv6/tcp/recverr
+ """
+ # '_SERVER_' is a special zone name representing an entire
+ # count. It doesn't mean a specific zone, but it means an
+ # entire count in the server.
+ _entire_server = '_SERVER_'
+ # zone names are contained under this dirname in the spec file.
+ _perzone_prefix = 'zones'
+
+ def __init__(self, spec_file_name):
+ """If the item `zones` is defined in the spec file, it obtains a
+ list of counter names under it when initiating. For behaviors
+ other than this, see documentation for
+ isc.statistics.counters.Counters.__init__()"""
+ counters.Counters.__init__(self, spec_file_name)
+ if self._perzone_prefix in \
+ isc.config.spec_name_list(self._statistics_spec):
+ self._zones_item_list = isc.config.spec_name_list(
+ isc.config.find_spec_part(
+ self._statistics_spec,
+ '%s/%s/%s' % (self._perzone_prefix,
+ '_CLASS_', self._entire_server)))
+
+ def get_statistics(self):
+ """Calculates an entire server's counts, and returns statistics
+ data in a format to send out to the stats module, including each
+ counter. If nothing is counted yet, then it returns an empty
+ dictionary."""
+ # entire copy
+ statistics_data = self._statistics_data.copy()
+ # If there is no 'zones' found in statistics_data,
+ # i.e. statistics_data contains no per-zone counter, it just
+ # returns statistics_data because calculating total counts
+ # across the zone names isn't necessary.
+ if self._perzone_prefix not in statistics_data:
+ return statistics_data
+ zones = statistics_data[self._perzone_prefix]
+ # Start calculation for '_SERVER_' counts
+ zones_spec = isc.config.find_spec_part(self._statistics_spec,
+ self._perzone_prefix)
+ zones_data = {}
+ for cls in zones.keys():
+ for zone in zones[cls].keys():
+ for (attr, val) in zones[cls][zone].items():
+ id_str1 = '%s/%s/%s' % (cls, zone, attr)
+ id_str2 = '%s/%s/%s' % (cls, self._entire_server, attr)
+ counters._set_counter(zones_data, zones_spec, id_str1, val)
+ counters._inc_counter(zones_data, zones_spec, id_str2, val)
+ # insert entire-server counts
+ statistics_data[self._perzone_prefix] = dict(
+ statistics_data[self._perzone_prefix],
+ **zones_data)
+ return statistics_data
diff --git a/src/lib/python/isc/statistics/tests/Makefile.am b/src/lib/python/isc/statistics/tests/Makefile.am
index c38e0f5..0c02290 100644
--- a/src/lib/python/isc/statistics/tests/Makefile.am
+++ b/src/lib/python/isc/statistics/tests/Makefile.am
@@ -1,5 +1,5 @@
PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
-PYTESTS = counters_test.py
+PYTESTS = counters_test.py dns_test.py
EXTRA_DIST = $(PYTESTS)
EXTRA_DIST += testdata/test_spec1.spec
EXTRA_DIST += testdata/test_spec2.spec
diff --git a/src/lib/python/isc/statistics/tests/counters_test.py b/src/lib/python/isc/statistics/tests/counters_test.py
index 5567dda..9d61ba3 100644
--- a/src/lib/python/isc/statistics/tests/counters_test.py
+++ b/src/lib/python/isc/statistics/tests/counters_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 Internet Systems Consortium.
+# Copyright (C) 2012-2013 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -13,7 +13,7 @@
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-'''Tests for isc.statistics.counter'''
+'''Tests for isc.statistics.counters'''
import unittest
import threading
@@ -22,7 +22,6 @@ import os
import imp
import isc.config
-TEST_ZONE_NAME_STR = "example.com."
TESTDATA_SRCDIR = os.getenv("TESTDATASRCDIR")
from isc.statistics import counters
@@ -50,12 +49,8 @@ class TestBasicMethods(unittest.TestCase):
TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec1.spec'
def setUp(self):
- imp.reload(counters)
self.counters = counters.Counters(self.TEST_SPECFILE_LOCATION)
- def tearDown(self):
- self.counters.clear_all()
-
def test_clear_counters(self):
self.assertRaises(isc.cc.data.DataNotFoundError,
self.counters.get, 'counter')
@@ -132,15 +127,15 @@ class TestBasicMethods(unittest.TestCase):
start_functor(concurrency, number, self.counters.inc,
counter_name)
counters._stop_timer(start_time,
- self.counters._statistics._data,
- self.counters._statistics._spec,
+ self.counters._statistics_data,
+ self.counters._statistics_spec,
timer_name)
self.assertEqual(
- counters._get_counter(self.counters._statistics._data,
+ counters._get_counter(self.counters._statistics_data,
counter_name),
concurrency * number)
self.assertGreaterEqual(
- counters._get_counter(self.counters._statistics._data,
+ counters._get_counter(self.counters._statistics_data,
timer_name), 0.0)
def test_concat(self):
@@ -159,17 +154,19 @@ class TestBasicMethods(unittest.TestCase):
b = a + ({},)
self.assertRaises(TypeError, counters._concat, *b)
+ def test_none_of_arg_of_counters(self):
+ """Test Counters raises ModuleSpecError when specifying not valid
+ argument"""
+ self.assertRaises(isc.config.module_spec.ModuleSpecError,
+ counters.Counters, None)
+ self.assertRaises(isc.config.module_spec.ModuleSpecError,
+ counters.Counters, '/foo/bar')
+
class BaseTestCounters():
def setUp(self):
- imp.reload(counters)
self._statistics_data = {}
self.counters = counters.Counters(self.TEST_SPECFILE_LOCATION)
- self._entire_server = self.counters._entire_server
- self._perzone_prefix = self.counters._perzone_prefix
-
- def tearDown(self):
- self.counters.clear_all()
def check_get_statistics(self):
"""Checks no differences between the value returned from
@@ -189,106 +186,10 @@ class BaseTestCounters():
else:
self.assertTrue(isc.config.ModuleSpec(
{'module_name': 'Foo',
- 'statistics': self.counters._statistics._spec}
+ 'statistics': self.counters._statistics_spec}
).validate_statistics(
False, self._statistics_data))
- def test_perzone_counters(self):
- # for per-zone counters
- for name in self.counters._zones_item_list:
- args = (self._perzone_prefix, TEST_ZONE_NAME_STR, name)
- if name.find('last_') == 0 and name.endswith('_duration'):
- self.counters.start_timer(*args)
- self.counters.stop_timer(*args)
- self.assertGreaterEqual(self.counters.get(*args), 0.0)
- sec = self.counters.get(*args)
- for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
- isc.cc.data.set(self._statistics_data,
- '%s/%s/%s' % (args[0], zone_str, name), sec)
- # twice exec stopper, then second is not changed
- self.counters.stop_timer(*args)
- self.assertEqual(self.counters.get(*args), sec)
- else:
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- # checks disable/enable
- self.counters.disable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- self.counters.enable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 2)
- for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
- isc.cc.data.set(self._statistics_data,
- '%s/%s/%s' % (args[0], zone_str, name), 2)
- self.check_get_statistics()
-
- def test_xfrrunning_counters(self):
- # for counters of xfer running
- _suffix = 'xfr_running'
- _xfrrunning_names = \
- isc.config.spec_name_list(self.counters._statistics._spec,
- "", True)
- for name in _xfrrunning_names:
- if name.find(_suffix) != 1: continue
- args = name.split('/')
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- self.counters.dec(*args)
- self.assertEqual(self.counters.get(*args), 0)
- # checks disable/enable
- self.counters.disable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 0)
- self.counters.enable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- self.counters.disable()
- self.counters.dec(*args)
- self.assertEqual(self.counters.get(*args), 1)
- self.counters.enable()
- self.counters.dec(*args)
- self.assertEqual(self.counters.get(*args), 0)
- self._statistics_data[name] = 0
- self.check_get_statistics()
-
- def test_socket_counters(self):
- # for ipsocket/unixsocket counters
- _prefix = 'socket/'
- _socket_names = \
- isc.config.spec_name_list(self.counters._statistics._spec,
- "", True)
- for name in _socket_names:
- if name.find(_prefix) != 0: continue
- args = name.split('/')
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- # checks disable/enable
- self.counters.disable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 1)
- self.counters.enable()
- self.counters.inc(*args)
- self.assertEqual(self.counters.get(*args), 2)
- isc.cc.data.set(
- self._statistics_data, '/'.join(args), 2)
- self.check_get_statistics()
-
- def test_perzone_zero_counters(self):
- # setting all counters to zero
- for name in self.counters._zones_item_list:
- args = (self._perzone_prefix, TEST_ZONE_NAME_STR, name)
- if name.find('last_') == 0 and name.endswith('_duration'):
- zero = 0.0
- else:
- zero = 0
- # set zero
- self.counters._incdec(*args, step=zero)
- for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
- isc.cc.data.set(self._statistics_data,
- '%s/%s/%s' % (args[0], zone_str, name), zero)
- self.check_get_statistics()
-
def test_undefined_item(self):
# test DataNotFoundError raising when specifying item defined
# in the specfile
@@ -302,130 +203,27 @@ class BaseTestCounters():
self.assertRaises(isc.cc.data.DataNotFoundError,
self.counters.get, '__undefined__')
-class TestCounters0(unittest.TestCase, BaseTestCounters):
- TEST_SPECFILE_LOCATION = None
- def setUp(self):
- BaseTestCounters.setUp(self)
- def tearDown(self):
- BaseTestCounters.tearDown(self)
-
class TestCounters1(unittest.TestCase, BaseTestCounters):
TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec1.spec'
def setUp(self):
BaseTestCounters.setUp(self)
- def tearDown(self):
- BaseTestCounters.tearDown(self)
-
-class TestCounters2(unittest.TestCase, BaseTestCounters):
- TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec2.spec'
- def setUp(self):
- BaseTestCounters.setUp(self)
- def tearDown(self):
- BaseTestCounters.tearDown(self)
-
-class TestCounters3(unittest.TestCase, BaseTestCounters):
- TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec3.spec'
- @classmethod
- def setUpClass(cls):
- imp.reload(counters)
- def setUp(self):
- BaseTestCounters.setUp(self)
- def tearDown(self):
- BaseTestCounters.tearDown(self)
-
-class BaseDummyModule():
- """A base dummy class"""
- TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec2.spec'
- def __init__(self):
- self.counters = counters.Counters(self.TEST_SPECFILE_LOCATION)
-
- def get_counters(self):
- return self.counters.get_statistics()
-
- def clear_counters(self):
- self.counters.clear_all()
-
-class DummyNotifyOut(BaseDummyModule):
- """A dummy class equivalent to notify.notify_out.NotifyOut"""
- def __init__(self):
- self.counters = counters.Counters()
-
- def inc_counters(self):
- """increments counters"""
- self.counters.inc('zones', TEST_ZONE_NAME_STR, 'notifyoutv4')
- self.counters.inc('zones', TEST_ZONE_NAME_STR, 'notifyoutv6')
-
-class DummyXfroutSession(BaseDummyModule):
- """A dummy class equivalent to XfroutSession in b10-xfrout"""
- def inc_counters(self):
- """increments counters"""
- self.counters.inc('zones', TEST_ZONE_NAME_STR, 'xfrreqdone')
- self.counters.inc('zones', TEST_ZONE_NAME_STR, 'xfrrej')
- self.counters.inc('axfr_running')
- self.counters.inc('ixfr_running')
- self.counters.dec('axfr_running')
- self.counters.dec('ixfr_running')
-
-class DummyUnixSockServer(BaseDummyModule):
- """A dummy class equivalent to UnixSockServer in b10-xfrout"""
- def inc_counters(self):
- """increments counters"""
- self.counters.inc('socket', 'unixdomain', 'open')
- self.counters.inc('socket', 'unixdomain', 'close')
-
-class DummyXfroutServer(BaseDummyModule):
- """A dummy class equivalent to XfroutServer in b10-xfrout"""
- def __init__(self):
- super().__init__()
- self.xfrout_sess = DummyXfroutSession()
- self.unix_socket_server = DummyUnixSockServer()
- self.notifier = DummyNotifyOut()
-
- def inc_counters(self):
- self.xfrout_sess.inc_counters()
- self.unix_socket_server.inc_counters()
- self.notifier.inc_counters()
-
-class TestDummyNotifyOut(unittest.TestCase):
- """Tests counters are incremented in which the spec file is not
- loaded"""
- def setUp(self):
- imp.reload(counters)
- self.notifier = DummyNotifyOut()
- self.notifier.inc_counters()
-
- def tearDown(self):
- self.notifier.clear_counters()
def test_counters(self):
- self.assertEqual(
- {'zones': {'_SERVER_': {'notifyoutv4': 1, 'notifyoutv6': 1},
- TEST_ZONE_NAME_STR: {'notifyoutv4': 1, 'notifyoutv6': 1}}},
- self.notifier.get_counters())
-
-class TestDummyXfroutServer(unittest.TestCase):
- """Tests counters are incremented or decremented in which the same
- spec file is multiply loaded in each child class"""
- def setUp(self):
- imp.reload(counters)
- self.xfrout_server = DummyXfroutServer()
- self.xfrout_server.inc_counters()
-
- def tearDown(self):
- self.xfrout_server.clear_counters()
-
- def test_counters(self):
- self.assertEqual(
- {'axfr_running': 0, 'ixfr_running': 0,
- 'socket': {'unixdomain': {'open': 1, 'close': 1}},
- 'zones': {'_SERVER_': {'notifyoutv4': 1,
- 'notifyoutv6': 1,
- 'xfrrej': 1, 'xfrreqdone': 1},
- TEST_ZONE_NAME_STR: {'notifyoutv4': 1,
- 'notifyoutv6': 1,
- 'xfrrej': 1,
- 'xfrreqdone': 1}}},
- self.xfrout_server.get_counters())
+ spec = isc.config.module_spec_from_file(self.TEST_SPECFILE_LOCATION)
+ self.assertEqual(spec.get_statistics_spec(),
+ self.counters._statistics_spec)
+ for name in isc.config.spec_name_list(self.counters._statistics_spec):
+ self.counters.inc(name)
+ self.assertEqual(self.counters.get(name), 1)
+ # checks disable/enable
+ self.counters.disable()
+ self.counters.inc(name)
+ self.assertEqual(self.counters.get(name), 1)
+ self.counters.enable()
+ self.counters.inc(name)
+ self.assertEqual(self.counters.get(name), 2)
+ self._statistics_data = {'counter':2, 'seconds': 2.0}
+ self.check_get_statistics()
if __name__== "__main__":
unittest.main()
diff --git a/src/lib/python/isc/statistics/tests/dns_test.py b/src/lib/python/isc/statistics/tests/dns_test.py
new file mode 100644
index 0000000..1ebe169
--- /dev/null
+++ b/src/lib/python/isc/statistics/tests/dns_test.py
@@ -0,0 +1,224 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+'''Tests for isc.statistics.dns'''
+
+import unittest
+import os
+import imp
+import isc.config
+import counters_test
+
+TEST_ZONE_NAME_STR = "example.com."
+TEST_ZONE_CLASS_STR = "IN"
+TESTDATA_SRCDIR = os.getenv("TESTDATASRCDIR")
+
+from isc.statistics import dns
+
+class BaseTestCounters(counters_test.BaseTestCounters):
+
+ def setUp(self):
+ self._statistics_data = {}
+ self.counters = dns.Counters(self.TEST_SPECFILE_LOCATION)
+ self._entire_server = self.counters._entire_server
+ self._perzone_prefix = self.counters._perzone_prefix
+
+ def test_perzone_counters(self):
+ # for per-zone counters
+ for name in self.counters._zones_item_list:
+ args = (self._perzone_prefix, TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, name)
+ if name.find('last_') == 0 and name.endswith('_duration'):
+ self.counters.start_timer(*args)
+ self.counters.stop_timer(*args)
+ self.assertGreaterEqual(self.counters.get(*args), 0.0)
+ sec = self.counters.get(*args)
+ for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
+ isc.cc.data.set(self._statistics_data,
+ '%s/%s/%s/%s' % (args[0], args[1],
+ zone_str, name), sec)
+ # twice exec stopper, then second is not changed
+ self.counters.stop_timer(*args)
+ self.assertEqual(self.counters.get(*args), sec)
+ else:
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ # checks disable/enable
+ self.counters.disable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ self.counters.enable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 2)
+ for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
+ isc.cc.data.set(self._statistics_data,
+ '%s/%s/%s/%s' % (args[0], args[1],
+ zone_str, name), 2)
+ self.check_get_statistics()
+
+ def test_xfrrunning_counters(self):
+ # for counters of xfer running
+ _suffix = 'xfr_running'
+ _xfrrunning_names = \
+ isc.config.spec_name_list(self.counters._statistics_spec,
+ "", True)
+ for name in _xfrrunning_names:
+ if name.find(_suffix) != 1: continue
+ args = name.split('/')
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ self.counters.dec(*args)
+ self.assertEqual(self.counters.get(*args), 0)
+ # checks disable/enable
+ self.counters.disable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 0)
+ self.counters.enable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ self.counters.disable()
+ self.counters.dec(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ self.counters.enable()
+ self.counters.dec(*args)
+ self.assertEqual(self.counters.get(*args), 0)
+ self._statistics_data[name] = 0
+ self.check_get_statistics()
+
+ def test_socket_counters(self):
+ # for ipsocket/unixsocket counters
+ _prefix = 'socket/'
+ _socket_names = \
+ isc.config.spec_name_list(self.counters._statistics_spec,
+ "", True)
+ for name in _socket_names:
+ if name.find(_prefix) != 0: continue
+ args = name.split('/')
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ # checks disable/enable
+ self.counters.disable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 1)
+ self.counters.enable()
+ self.counters.inc(*args)
+ self.assertEqual(self.counters.get(*args), 2)
+ isc.cc.data.set(
+ self._statistics_data, '/'.join(args), 2)
+ self.check_get_statistics()
+
+ def test_perzone_zero_counters(self):
+ # setting all counters to zero
+ for name in self.counters._zones_item_list:
+ args = (self._perzone_prefix, TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, name)
+ if name.find('last_') == 0 and name.endswith('_duration'):
+ zero = 0.0
+ else:
+ zero = 0
+ # set zero
+ self.counters._incdec(*args, step=zero)
+ for zone_str in (self._entire_server, TEST_ZONE_NAME_STR):
+ isc.cc.data.set(self._statistics_data,
+ '%s/%s/%s/%s' % (args[0], args[1],
+ zone_str, name), zero)
+ self.check_get_statistics()
+
+
+class TestCounters2(unittest.TestCase, BaseTestCounters):
+ TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec2.spec'
+ def setUp(self):
+ BaseTestCounters.setUp(self)
+
+class TestCounters3(unittest.TestCase, BaseTestCounters):
+ TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec3.spec'
+ @classmethod
+ def setUp(self):
+ BaseTestCounters.setUp(self)
+
+class BaseDummyModule():
+ """A base dummy class"""
+ TEST_SPECFILE_LOCATION = TESTDATA_SRCDIR + os.sep + 'test_spec2.spec'
+ def __init__(self, counters):
+ self.counters = counters
+
+ def get_counters(self):
+ return self.counters.get_statistics()
+
+class DummyNotifyOut(BaseDummyModule):
+ """A dummy class equivalent to notify.notify_out.NotifyOut"""
+ def inc_counters(self):
+ """increments counters"""
+ self.counters.inc('zones', TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, 'notifyoutv4')
+ self.counters.inc('zones', TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, 'notifyoutv6')
+
+class DummyXfroutSession(BaseDummyModule):
+ """A dummy class equivalent to XfroutSession in b10-xfrout"""
+ def inc_counters(self):
+ """increments counters"""
+ self.counters.inc('zones', TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, 'xfrreqdone')
+ self.counters.inc('zones', TEST_ZONE_CLASS_STR,
+ TEST_ZONE_NAME_STR, 'xfrrej')
+ self.counters.inc('axfr_running')
+ self.counters.inc('ixfr_running')
+ self.counters.dec('axfr_running')
+ self.counters.dec('ixfr_running')
+
+class DummyUnixSockServer(BaseDummyModule):
+ """A dummy class equivalent to UnixSockServer in b10-xfrout"""
+ def inc_counters(self):
+ """increments counters"""
+ self.counters.inc('socket', 'unixdomain', 'open')
+ self.counters.inc('socket', 'unixdomain', 'close')
+
+class DummyXfroutServer(BaseDummyModule):
+ """A dummy class equivalent to XfroutServer in b10-xfrout"""
+ def __init__(self):
+ self.counters = dns.Counters(self.TEST_SPECFILE_LOCATION)
+ self.xfrout_sess = DummyXfroutSession(self.counters)
+ self.unix_socket_server = DummyUnixSockServer(self.counters)
+ self.notifier = DummyNotifyOut(self.counters)
+
+ def inc_counters(self):
+ self.xfrout_sess.inc_counters()
+ self.unix_socket_server.inc_counters()
+ self.notifier.inc_counters()
+
+class TestDummyXfroutServer(unittest.TestCase):
+ """Tests counters are incremented or decremented in which the same
+ spec file is multiply loaded in each child class"""
+ def setUp(self):
+ self.xfrout_server = DummyXfroutServer()
+ self.xfrout_server.inc_counters()
+
+ def test_counters(self):
+ self.assertEqual(
+ {'axfr_running': 0, 'ixfr_running': 0,
+ 'socket': {'unixdomain': {'open': 1, 'close': 1}},
+ 'zones': {TEST_ZONE_CLASS_STR: {
+ '_SERVER_': {'notifyoutv4': 1,
+ 'notifyoutv6': 1,
+ 'xfrrej': 1, 'xfrreqdone': 1},
+ TEST_ZONE_NAME_STR: {'notifyoutv4': 1,
+ 'notifyoutv6': 1,
+ 'xfrrej': 1,
+ 'xfrreqdone': 1}}}},
+ self.xfrout_server.get_counters())
+
+if __name__== "__main__":
+ unittest.main()
diff --git a/src/lib/python/isc/statistics/tests/testdata/test_spec2.spec b/src/lib/python/isc/statistics/tests/testdata/test_spec2.spec
index 11b8706..422fc0a 100644
--- a/src/lib/python/isc/statistics/tests/testdata/test_spec2.spec
+++ b/src/lib/python/isc/statistics/tests/testdata/test_spec2.spec
@@ -9,56 +9,66 @@
"item_type": "named_set",
"item_optional": false,
"item_default": {
- "_SERVER_" : {
- "notifyoutv4" : 0,
- "notifyoutv6" : 0,
- "xfrrej" : 0,
- "xfrreqdone" : 0
+ "IN" : {
+ "_SERVER_" : {
+ "notifyoutv4" : 0,
+ "notifyoutv6" : 0,
+ "xfrrej" : 0,
+ "xfrreqdone" : 0
+ }
}
},
"item_title": "Zone names",
"item_description": "Zone names for Xfrout statistics",
"named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
+ "item_name": "classname",
+ "item_type": "named_set",
"item_optional": false,
"item_default": {},
- "item_title": "Zone name",
- "item_description": "Zone name for Xfrout statistics",
- "map_item_spec": [
- {
- "item_name": "notifyoutv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IPv4 notifies",
- "item_description": "Number of IPv4 notifies per zone name sent out from Xfrout"
- },
- {
- "item_name": "notifyoutv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IPv6 notifies",
- "item_description": "Number of IPv6 notifies per zone name sent out from Xfrout"
- },
- {
- "item_name": "xfrrej",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XFR rejected requests",
- "item_description": "Number of XFR requests per zone name rejected by Xfrout"
- },
- {
- "item_name": "xfrreqdone",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Requested zone transfers",
- "item_description": "Number of requested zone transfers completed per zone name"
- }
- ]
+ "item_title": "RR class name",
+ "item_description": "RR class name for Xfrout statistics",
+ "named_set_item_spec": {
+ "item_name": "zonename",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "Zone name",
+ "item_description": "Zone name for Xfrout statistics",
+ "map_item_spec": [
+ {
+ "item_name": "notifyoutv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv4 notifies",
+ "item_description": "Number of IPv4 notifies per zone name sent out from Xfrout"
+ },
+ {
+ "item_name": "notifyoutv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IPv6 notifies",
+ "item_description": "Number of IPv6 notifies per zone name sent out from Xfrout"
+ },
+ {
+ "item_name": "xfrrej",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XFR rejected requests",
+ "item_description": "Number of XFR requests per zone name rejected by Xfrout"
+ },
+ {
+ "item_name": "xfrreqdone",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "Requested zone transfers",
+ "item_description": "Number of requested zone transfers completed per zone name"
+ }
+ ]
+ }
}
},
{
diff --git a/src/lib/python/isc/statistics/tests/testdata/test_spec3.spec b/src/lib/python/isc/statistics/tests/testdata/test_spec3.spec
index 6c06f69..f620cad 100644
--- a/src/lib/python/isc/statistics/tests/testdata/test_spec3.spec
+++ b/src/lib/python/isc/statistics/tests/testdata/test_spec3.spec
@@ -10,110 +10,120 @@
"item_type": "named_set",
"item_optional": false,
"item_default": {
- "_SERVER_" : {
- "soaoutv4": 0,
- "soaoutv6": 0,
- "axfrreqv4": 0,
- "axfrreqv6": 0,
- "ixfrreqv4": 0,
- "ixfrreqv6": 0,
- "xfrsuccess": 0,
- "xfrfail": 0,
- "last_ixfr_duration": 0.0,
- "last_axfr_duration": 0.0
+ "IN" : {
+ "_SERVER_" : {
+ "soaoutv4": 0,
+ "soaoutv6": 0,
+ "axfrreqv4": 0,
+ "axfrreqv6": 0,
+ "ixfrreqv4": 0,
+ "ixfrreqv6": 0,
+ "xfrsuccess": 0,
+ "xfrfail": 0,
+ "last_ixfr_duration": 0.0,
+ "last_axfr_duration": 0.0
+ }
}
},
"item_title": "Zone names",
"item_description": "Zone names for Xfrout statistics",
"named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
+ "item_name": "classname",
+ "item_type": "named_set",
"item_optional": false,
"item_default": {},
- "item_title": "Zone name",
- "item_description": "Zone name for Xfrout statistics",
- "map_item_spec": [
- {
- "item_name": "soaoutv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "SOAOutv4",
- "item_description": "Number of IPv4 SOA queries sent from Xfrin"
- },
- {
- "item_name": "soaoutv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "SOAOutv6",
- "item_description": "Number of IPv6 SOA queries sent from Xfrin"
- },
- {
- "item_name": "axfrreqv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "AXFRReqv4",
- "item_description": "Number of IPv4 AXFR requests sent from Xfrin"
- },
- {
- "item_name": "axfrreqv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "AXFRReqv6",
- "item_description": "Number of IPv6 AXFR requests sent from Xfrin"
- },
- {
- "item_name": "ixfrreqv4",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IXFRReqv4",
- "item_description": "Number of IPv4 IXFR requests sent from Xfrin"
- },
- {
- "item_name": "ixfrreqv6",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "IXFRReqv6",
- "item_description": "Number of IPv6 IXFR requests sent from Xfrin"
- },
- {
- "item_name": "xfrsuccess",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XfrSuccess",
- "item_description": "Number of zone transfer requests succeeded"
- },
- {
- "item_name": "xfrfail",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "XfrFail",
- "item_description": "Number of zone transfer requests failed"
- },
- {
- "item_name": "last_ixfr_duration",
- "item_type": "real",
- "item_optional": false,
- "item_default": 0.0,
- "item_title": "Last IXFR duration",
- "item_description": "Duration of the last IXFR. 0.0 means no successful IXFR done."
- },
- {
- "item_name": "last_axfr_duration",
- "item_type": "real",
- "item_optional": false,
- "item_default": 0.0,
- "item_title": "Last AXFR duration",
- "item_description": "Duration of the last AXFR. 0.0 means no successful AXFR done."
- }
- ]
+ "item_title": "RR class name",
+ "item_description": "RR class name for Xfrout statistics",
+ "named_set_item_spec": {
+ "item_name": "zonename",
+ "item_type": "map",
+ "item_optional": false,
+ "item_default": {},
+ "item_title": "Zone name",
+ "item_description": "Zone name for Xfrout statistics",
+ "map_item_spec": [
+ {
+ "item_name": "soaoutv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "SOAOutv4",
+ "item_description": "Number of IPv4 SOA queries sent from Xfrin"
+ },
+ {
+ "item_name": "soaoutv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "SOAOutv6",
+ "item_description": "Number of IPv6 SOA queries sent from Xfrin"
+ },
+ {
+ "item_name": "axfrreqv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "AXFRReqv4",
+ "item_description": "Number of IPv4 AXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "axfrreqv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "AXFRReqv6",
+ "item_description": "Number of IPv6 AXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "ixfrreqv4",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IXFRReqv4",
+ "item_description": "Number of IPv4 IXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "ixfrreqv6",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "IXFRReqv6",
+ "item_description": "Number of IPv6 IXFR requests sent from Xfrin"
+ },
+ {
+ "item_name": "xfrsuccess",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XfrSuccess",
+ "item_description": "Number of zone transfer requests succeeded"
+ },
+ {
+ "item_name": "xfrfail",
+ "item_type": "integer",
+ "item_optional": false,
+ "item_default": 0,
+ "item_title": "XfrFail",
+ "item_description": "Number of zone transfer requests failed"
+ },
+ {
+ "item_name": "last_ixfr_duration",
+ "item_type": "real",
+ "item_optional": false,
+ "item_default": 0.0,
+ "item_title": "Last IXFR duration",
+ "item_description": "Duration of the last IXFR. 0.0 means no successful IXFR done."
+ },
+ {
+ "item_name": "last_axfr_duration",
+ "item_type": "real",
+ "item_optional": false,
+ "item_default": 0.0,
+ "item_title": "Last AXFR duration",
+ "item_description": "Duration of the last AXFR. 0.0 means no successful AXFR done."
+ }
+ ]
+ }
}
},
{
diff --git a/src/lib/python/isc/util/Makefile.am b/src/lib/python/isc/util/Makefile.am
index eaeedd8..e742f45 100644
--- a/src/lib/python/isc/util/Makefile.am
+++ b/src/lib/python/isc/util/Makefile.am
@@ -1,8 +1,23 @@
SUBDIRS = . cio tests
-python_PYTHON = __init__.py process.py socketserver_mixin.py file.py
+python_PYTHON = __init__.py process.py socketserver_mixin.py file.py \
+ traceback_handler.py
+BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/util_messages.py
+nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/util_messages.py
+pylogmessagedir = $(pyexecdir)/isc/log_messages/
python_PYTHON += address_formatter.py
+EXTRA_DIST = util_messages.mes
+
+CLEANFILES = $(PYTHON_LOGMSGPKG_DIR)/work/util_messages.py
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/util_messages.pyc
+CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/util_messages.pyo
+
+# Define rule to build logging source files from message file
+$(PYTHON_LOGMSGPKG_DIR)/work/util_messages.py: util_messages.mes
+ $(top_builddir)/src/lib/log/compiler/message \
+ -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/util_messages.mes
+
pythondir = $(pyexecdir)/isc/util
CLEANDIRS = __pycache__
diff --git a/src/lib/python/isc/util/tests/Makefile.am b/src/lib/python/isc/util/tests/Makefile.am
index 4df6947..be60c9a 100644
--- a/src/lib/python/isc/util/tests/Makefile.am
+++ b/src/lib/python/isc/util/tests/Makefile.am
@@ -1,6 +1,6 @@
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
PYTESTS = process_test.py socketserver_mixin_test.py file_test.py
-PYTESTS += address_formatter_test.py
+PYTESTS += address_formatter_test.py traceback_handler_test.py
EXTRA_DIST = $(PYTESTS)
# If necessary (rare cases), explicitly specify paths to dynamic libraries
diff --git a/src/lib/python/isc/util/tests/address_formatter_test.py b/src/lib/python/isc/util/tests/address_formatter_test.py
index 295b7c3..d181e70 100644
--- a/src/lib/python/isc/util/tests/address_formatter_test.py
+++ b/src/lib/python/isc/util/tests/address_formatter_test.py
@@ -62,7 +62,5 @@ class AddressFormatterTest(unittest.TestCase):
self.assertRaises(ValueError, str, AddressFormatter(("::1", 123), 1))
self.assertRaises(ValueError, str, AddressFormatter(("::1", 123), 1))
-
-
if __name__ == "__main__":
unittest.main()
diff --git a/src/lib/python/isc/util/tests/traceback_handler_test.py b/src/lib/python/isc/util/tests/traceback_handler_test.py
new file mode 100644
index 0000000..cbd1baa
--- /dev/null
+++ b/src/lib/python/isc/util/tests/traceback_handler_test.py
@@ -0,0 +1,101 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+import unittest
+import os
+import isc.log
+import isc.util.traceback_handler
+
+class TracebackHandlerTest(unittest.TestCase):
+ def setUp(self):
+ """
+ Save some things to be restored later, if we overwrite them
+ for tests.
+ """
+ self.exit = isc.util.traceback_handler.sys.exit
+ self.logger = isc.util.traceback_handler.logger
+ # Sanity check - the functions exist.
+ self.assertTrue(self.exit)
+ self.assertTrue(self.logger)
+
+ def tearDown(self):
+ """
+ Restore mocked things.
+ """
+ isc.util.traceback_handler.sys.exit = self.exit
+ isc.util.traceback_handler.logger = self.logger
+
+ def test_success(self):
+ """
+ Test the handler doesn't influence the result of successful
+ function.
+ """
+ self.called = False
+ def succ():
+ self.called = True
+ return 42
+
+ self.assertEqual(42,
+ isc.util.traceback_handler.traceback_handler(succ))
+ self.assertTrue(self.called)
+
+ def test_success_no_returned_value(self):
+ """
+ Test the handler handles the case where main() returns nothing.
+ """
+ self.called = False
+ def succ():
+ self.called = True
+ return
+
+ self.assertIsNone(isc.util.traceback_handler.traceback_handler(succ))
+ self.assertTrue(self.called)
+
+ def test_exception(self):
+ """
+ Test the exception is caught and logged, but not propagated.
+ """
+ # Mock up bunch of things
+ self.exited = False
+ def exit(status):
+ self.assertEqual(1, status)
+ self.exited = True
+ isc.util.traceback_handler.sys.exit = exit
+ self.logged = False
+ obj = self
+ class Logger:
+ def fatal(self, message, ename, exception, filename):
+ obj.assertTrue(isinstance(exception, Exception))
+ obj.assertEqual('Exception', ename)
+ obj.assertTrue(os.path.isfile(filename))
+ with open(filename) as f:
+ text = f.read()
+ obj.assertTrue(text.startswith('Traceback'))
+ os.remove(filename)
+ obj.logged = True
+ isc.util.traceback_handler.logger = Logger()
+ # The failing function
+ def fail():
+ raise Exception('Anybody there?')
+ # Does not raise, but returns nothing
+ self.assertIsNone(isc.util.traceback_handler.traceback_handler(fail))
+ # It logged and exited (sane values for those are checked in the mocks)
+ self.assertTrue(self.exited)
+ self.assertTrue(self.logged)
+
+if __name__ == "__main__":
+ isc.log.init("bind10")
+ isc.log.resetUnitTestRootLogger()
+ unittest.main()
diff --git a/src/lib/python/isc/util/traceback_handler.py b/src/lib/python/isc/util/traceback_handler.py
new file mode 100644
index 0000000..74ec706
--- /dev/null
+++ b/src/lib/python/isc/util/traceback_handler.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from isc.log_messages.util_messages import *
+import sys
+import tempfile
+import os
+import traceback
+
+logger = isc.log.Logger('util')
+
+def traceback_handler(main):
+ """
+ Handle uncaught exception from the main callable.
+
+ The function runs the callable passed as main (it is called
+ without any provided parameters). If it raises any exception,
+ the exception is logged and the application is terminated.
+ """
+ try:
+ return main()
+ except Exception as e:
+ fd, name = tempfile.mkstemp(text=True)
+ with os.fdopen(fd, 'w') as handle:
+ traceback.print_exc(None, handle)
+ logger.fatal(PYTHON_UNHANDLED_EXCEPTION, type(e).__name__, e, name)
+ sys.exit(1)
diff --git a/src/lib/python/isc/util/util_messages.mes b/src/lib/python/isc/util/util_messages.mes
new file mode 100644
index 0000000..8258e85
--- /dev/null
+++ b/src/lib/python/isc/util/util_messages.mes
@@ -0,0 +1,26 @@
+# Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+# No namespace declaration - these constants go in the global namespace
+# of the isc.util.util_messages python module.
+
+% PYTHON_UNHANDLED_EXCEPTION program terminated with exception %1: %2. More info in %3
+A program encountered an unexpected situation and terminated because it
+didn't know how to handle it. The exact problem is logged in the
+message. This might be caused by a bug in the program, a broken
+installation, or just a very rare condition which wasn't handled in the
+code. A full stack trace is left in the generated file. If you report a
+bug for this exception, please include that file. The file will not be
+deleted automatically and you may want to remove it when you no longer
+need the information there.
diff --git a/src/lib/resolve/.gitignore b/src/lib/resolve/.gitignore
index 292ed1a..87dc694 100644
--- a/src/lib/resolve/.gitignore
+++ b/src/lib/resolve/.gitignore
@@ -1,2 +1,3 @@
/resolve_messages.cc
/resolve_messages.h
+/s-messages
diff --git a/src/lib/resolve/Makefile.am b/src/lib/resolve/Makefile.am
index 096a14d..13e37e1 100644
--- a/src/lib/resolve/Makefile.am
+++ b/src/lib/resolve/Makefile.am
@@ -8,8 +8,11 @@ AM_CPPFLAGS += $(SQLITE_CFLAGS)
AM_CXXFLAGS = $(B10_CXXFLAGS)
# Define rule to build logging source files from message file
-resolve_messages.h resolve_messages.cc: resolve_messages.mes
+resolve_messages.h resolve_messages.cc: s-messages
+
+s-messages: resolve_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/resolve/resolve_messages.mes
+ touch $@
# Tell Automake that the nsasdef.{cc,h} source files are created in the build
# process, so it must create these before doing anything else. Although they
@@ -19,7 +22,7 @@ resolve_messages.h resolve_messages.cc: resolve_messages.mes
# present when they are compiled), the safest option is to create it first.
BUILT_SOURCES = resolve_messages.h resolve_messages.cc
-CLEANFILES = *.gcno *.gcda resolve_messages.cc resolve_messages.h
+CLEANFILES = *.gcno *.gcda resolve_messages.cc resolve_messages.h s-messages
lib_LTLIBRARIES = libb10-resolve.la
libb10_resolve_la_SOURCES = resolve.h resolve.cc
@@ -35,6 +38,7 @@ libb10_resolve_la_LIBADD = $(top_builddir)/src/lib/dns/libb10-dns++.la
libb10_resolve_la_LIBADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
libb10_resolve_la_LIBADD += $(top_builddir)/src/lib/log/libb10-log.la
libb10_resolve_la_LIBADD += $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
+libb10_resolve_la_LIBADD += $(top_builddir)/src/lib/nsas/libb10-nsas.la
# The message file should be in the distribution.
EXTRA_DIST = resolve_messages.mes
@@ -42,9 +46,4 @@ EXTRA_DIST = resolve_messages.mes
# Note: the ordering matters: -Wno-... must follow -Wextra (defined in
# B10_CXXFLAGS)
libb10_resolve_la_CXXFLAGS = $(AM_CXXFLAGS)
-if USE_CLANGPP
-# For clang++, we need to turn off -Werror completely.
-libb10_resolve_la_CXXFLAGS += -Wno-error
-endif
libb10_resolve_la_CPPFLAGS = $(AM_CPPFLAGS)
-
diff --git a/src/lib/resolve/recursive_query.cc b/src/lib/resolve/recursive_query.cc
index 55dc4da..3c54a78 100644
--- a/src/lib/resolve/recursive_query.cc
+++ b/src/lib/resolve/recursive_query.cc
@@ -245,11 +245,6 @@ private:
// normal query state
- // Update the question that will be sent to the server
- void setQuestion(const Question& new_question) {
- question_ = new_question;
- }
-
// TODO: replace by our wrapper
asio::deadline_timer client_timer;
asio::deadline_timer lookup_timer;
diff --git a/src/lib/resolve/tests/recursive_query_unittest.cc b/src/lib/resolve/tests/recursive_query_unittest.cc
index acbbb03..48e5a31 100644
--- a/src/lib/resolve/tests/recursive_query_unittest.cc
+++ b/src/lib/resolve/tests/recursive_query_unittest.cc
@@ -56,7 +56,6 @@
#include <asiolink/io_service.h>
#include <asiolink/io_message.h>
#include <asiolink/io_error.h>
-#include <asiolink/simple_callback.h>
using isc::UnitTestUtil;
using namespace std;
@@ -333,8 +332,7 @@ protected:
// Set up empty DNS Service
// Set up an IO Service queue without any addresses
void setDNSService() {
- dns_service_.reset(new DNSService(io_service_, callback_.get(), NULL,
- NULL));
+ dns_service_.reset(new DNSService(io_service_, callback_.get(), NULL));
}
// Run a simple server test, on either IPv4 or IPv6, and over either
@@ -478,10 +476,12 @@ protected:
};
private:
- class ASIOCallBack : public SimpleCallback {
+ class ASIOCallBack : public DNSLookup {
public:
ASIOCallBack(RecursiveQueryTest* test_obj) : test_obj_(test_obj) {}
- void operator()(const IOMessage& io_message) const {
+ void operator()(const IOMessage& io_message,
+ isc::dns::MessagePtr, isc::dns::MessagePtr,
+ isc::util::OutputBufferPtr, DNSServer*) const {
test_obj_->callBack(io_message);
}
private:
diff --git a/src/lib/resolve/tests/recursive_query_unittest_2.cc b/src/lib/resolve/tests/recursive_query_unittest_2.cc
index 0b38c59..d6019d0 100644
--- a/src/lib/resolve/tests/recursive_query_unittest_2.cc
+++ b/src/lib/resolve/tests/recursive_query_unittest_2.cc
@@ -159,7 +159,7 @@ public:
RecursiveQueryTest2() :
debug_(DEBUG_PRINT),
service_(),
- dns_service_(service_, NULL, NULL, NULL),
+ dns_service_(service_, NULL, NULL),
question_(new Question(Name("www.example.org"), RRClass::IN(), RRType::A())),
last_(NONE),
expected_(NONE),
diff --git a/src/lib/resolve/tests/recursive_query_unittest_3.cc b/src/lib/resolve/tests/recursive_query_unittest_3.cc
index 92ec589..7803b88 100644
--- a/src/lib/resolve/tests/recursive_query_unittest_3.cc
+++ b/src/lib/resolve/tests/recursive_query_unittest_3.cc
@@ -141,7 +141,7 @@ public:
/// \brief Constructor
RecursiveQueryTest3() :
service_(),
- dns_service_(service_, NULL, NULL, NULL),
+ dns_service_(service_, NULL, NULL),
question_(new Question(Name("ednsfallback"),
RRClass::IN(), RRType::A())),
last_(NONE),
@@ -218,7 +218,8 @@ public:
/// \param ec ASIO error code, completion code of asynchronous I/O issued
/// by the "server" to receive data.
/// \param length Amount of data received.
- void udpReceiveHandler(error_code ec = error_code(), size_t length = 0) {
+ void udpReceiveHandler(asio::error_code ec = asio::error_code(),
+ size_t length = 0) {
// Expected state should be one greater than the last state.
EXPECT_EQ(static_cast<int>(expected_), static_cast<int>(last_) + 1);
last_ = expected_;
@@ -281,7 +282,8 @@ public:
///
/// \param ec Completion error code of the send.
/// \param length Actual number of bytes sent.
- void udpSendHandler(error_code ec = error_code(), size_t length = 0) {
+ void udpSendHandler(asio::error_code ec = asio::error_code(),
+ size_t length = 0) {
// Check send was OK
EXPECT_EQ(0, ec.value());
EXPECT_EQ(udp_length_, length);
@@ -301,7 +303,8 @@ public:
///
/// \param socket Socket on which data will be received
/// \param ec Boost error code, value should be zero.
- void tcpAcceptHandler(error_code ec = error_code(), size_t length = 0) {
+ void tcpAcceptHandler(asio::error_code ec = asio::error_code(),
+ size_t length = 0) {
// Expect that the accept completed without a problem.
EXPECT_EQ(0, ec.value());
@@ -323,7 +326,8 @@ public:
/// \param ec ASIO error code, completion code of asynchronous I/O issued
/// by the "server" to receive data.
/// \param length Amount of data received.
- void tcpReceiveHandler(error_code ec = error_code(), size_t length = 0) {
+ void tcpReceiveHandler(asio::error_code ec = asio::error_code(),
+ size_t length = 0) {
// Expect that the receive completed without a problem.
EXPECT_EQ(0, ec.value());
@@ -409,7 +413,7 @@ public:
/// \param ec Boost error code, value should be zero.
/// \param length Number of bytes sent.
void tcpSendHandler(size_t expected_length = 0,
- error_code ec = error_code(),
+ asio::error_code ec = asio::error_code(),
size_t length = 0)
{
EXPECT_EQ(0, ec.value()); // Expect no error
diff --git a/src/lib/server_common/.gitignore b/src/lib/server_common/.gitignore
index e25a98f..1c16f77 100644
--- a/src/lib/server_common/.gitignore
+++ b/src/lib/server_common/.gitignore
@@ -1,2 +1,3 @@
/server_common_messages.cc
/server_common_messages.h
+/s-messages
diff --git a/src/lib/server_common/Makefile.am b/src/lib/server_common/Makefile.am
index cf9059a..9ea55d8 100644
--- a/src/lib/server_common/Makefile.am
+++ b/src/lib/server_common/Makefile.am
@@ -33,9 +33,12 @@ libb10_server_common_la_LIBADD += $(top_builddir)/src/lib/acl/libb10-acl.la
libb10_server_common_la_LIBADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
libb10_server_common_la_LIBADD += $(top_builddir)/src/lib/util/io/libb10-util-io.la
BUILT_SOURCES = server_common_messages.h server_common_messages.cc
-server_common_messages.h server_common_messages.cc: server_common_messages.mes
+server_common_messages.h server_common_messages.cc: s-messages
+
+s-messages: server_common_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/server_common/server_common_messages.mes
+ touch $@
EXTRA_DIST = server_common_messages.mes
-CLEANFILES = *.gcno *.gcda server_common_messages.h server_common_messages.cc
+CLEANFILES = *.gcno *.gcda server_common_messages.h server_common_messages.cc s-messages
diff --git a/src/lib/server_common/portconfig.cc b/src/lib/server_common/portconfig.cc
index 7bc6876..6120c7d 100644
--- a/src/lib/server_common/portconfig.cc
+++ b/src/lib/server_common/portconfig.cc
@@ -49,6 +49,9 @@ parseAddresses(isc::data::ConstElementPtr addresses,
"address and port");
}
try {
+ // We create an IOAddress object to just check that
+ // construction passes. It is immediately destroyed.
+ // cppcheck-suppress unusedScopedObject
IOAddress(addr->stringValue());
if (port->intValue() < 0 ||
port->intValue() > 0xffff) {
diff --git a/src/lib/server_common/tests/client_unittest.cc b/src/lib/server_common/tests/client_unittest.cc
index 14f6fbc..f962c8d 100644
--- a/src/lib/server_common/tests/client_unittest.cc
+++ b/src/lib/server_common/tests/client_unittest.cc
@@ -63,7 +63,8 @@ protected:
TEST_F(ClientTest, constructIPv4) {
EXPECT_EQ(AF_INET, client4->getRequestSourceEndpoint().getFamily());
- EXPECT_EQ(IPPROTO_UDP, client4->getRequestSourceEndpoint().getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_UDP),
+ client4->getRequestSourceEndpoint().getProtocol());
EXPECT_EQ("192.0.2.1",
client4->getRequestSourceEndpoint().getAddress().toText());
EXPECT_EQ(53214, client4->getRequestSourceEndpoint().getPort());
@@ -77,7 +78,8 @@ TEST_F(ClientTest, constructIPv4) {
TEST_F(ClientTest, constructIPv6) {
EXPECT_EQ(AF_INET6, client6->getRequestSourceEndpoint().getFamily());
- EXPECT_EQ(IPPROTO_TCP, client6->getRequestSourceEndpoint().getProtocol());
+ EXPECT_EQ(static_cast<short>(IPPROTO_TCP),
+ client6->getRequestSourceEndpoint().getProtocol());
EXPECT_EQ("2001:db8::1",
client6->getRequestSourceEndpoint().getAddress().toText());
EXPECT_EQ(53216, client6->getRequestSourceEndpoint().getPort());
diff --git a/src/lib/statistics/counter.h b/src/lib/statistics/counter.h
index b0d31e9..eae4a73 100644
--- a/src/lib/statistics/counter.h
+++ b/src/lib/statistics/counter.h
@@ -22,13 +22,15 @@
#include <vector>
+#include <stdint.h>
+
namespace isc {
namespace statistics {
class Counter : boost::noncopyable {
public:
typedef unsigned int Type;
- typedef unsigned int Value;
+ typedef uint64_t Value;
private:
std::vector<Counter::Value> counters_;
@@ -55,7 +57,7 @@ public:
/// \param type %Counter item to increment
///
/// \throw isc::OutOfRange \a type is invalid
- void inc(const Counter::Type type) {
+ void inc(const Counter::Type& type) {
if (type >= counters_.size()) {
isc_throw(isc::OutOfRange, "Counter type is out of range");
}
@@ -68,7 +70,7 @@ public:
/// \param type %Counter item to get the value of
///
/// \throw isc::OutOfRange \a type is invalid
- const Counter::Value& get(const Counter::Type type) const {
+ const Counter::Value& get(const Counter::Type& type) const {
if (type >= counters_.size()) {
isc_throw(isc::OutOfRange, "Counter type is out of range");
}
diff --git a/src/lib/statistics/tests/Makefile.am b/src/lib/statistics/tests/Makefile.am
index 25a3db2..f45a829 100644
--- a/src/lib/statistics/tests/Makefile.am
+++ b/src/lib/statistics/tests/Makefile.am
@@ -40,10 +40,6 @@ run_unittests_CXXFLAGS = $(AM_CXXFLAGS)
if USE_GXX
run_unittests_CXXFLAGS += -Wno-unused-parameter
endif
-if USE_CLANGPP
-# Same for clang++, but we need to turn off -Werror completely.
-run_unittests_CXXFLAGS += -Wno-error
-endif
endif
noinst_PROGRAMS = $(TESTS)
diff --git a/src/lib/statistics/tests/counter_unittest.cc b/src/lib/statistics/tests/counter_unittest.cc
index e0d29ac..b258d9e 100644
--- a/src/lib/statistics/tests/counter_unittest.cc
+++ b/src/lib/statistics/tests/counter_unittest.cc
@@ -73,6 +73,11 @@ TEST_F(CounterTest, incrementCounterItem) {
EXPECT_EQ(counter.get(ITEM1), 2);
EXPECT_EQ(counter.get(ITEM2), 4);
EXPECT_EQ(counter.get(ITEM3), 6);
+
+ for (long long int i = 0; i < 4294967306LL; i++) {
+ counter.inc(ITEM1);
+ }
+ EXPECT_EQ(counter.get(ITEM1), 4294967308LL); // 4294967306 + 2
}
TEST_F(CounterTest, invalidCounterItem) {
diff --git a/src/lib/testutils/Makefile.am b/src/lib/testutils/Makefile.am
index 2281b6d..574cc78 100644
--- a/src/lib/testutils/Makefile.am
+++ b/src/lib/testutils/Makefile.am
@@ -11,7 +11,8 @@ libb10_testutils_la_SOURCES = srv_test.h srv_test.cc
libb10_testutils_la_SOURCES += dnsmessage_test.h dnsmessage_test.cc
libb10_testutils_la_SOURCES += mockups.h
libb10_testutils_la_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
-libb10_testutils_la_LIBADD = $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+libb10_testutils_la_LIBADD = $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
+libb10_testutils_la_LIBADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
endif
EXTRA_DIST = portconfig.h socket_request.h
diff --git a/src/lib/testutils/mockups.h b/src/lib/testutils/mockups.h
index 58b39ee..00c2dcd 100644
--- a/src/lib/testutils/mockups.h
+++ b/src/lib/testutils/mockups.h
@@ -115,6 +115,8 @@ private:
// to addServerXXX methods so the test code subsequently checks the parameters.
class MockDNSService : public isc::asiodns::DNSServiceBase {
public:
+ MockDNSService() : tcp_recv_timeout_(0) {}
+
// A helper tuple of parameters passed to addServerUDPFromFD().
struct UDPFdParams {
int fd;
diff --git a/src/lib/util/Makefile.am b/src/lib/util/Makefile.am
index 32a9341..7d3781e 100644
--- a/src/lib/util/Makefile.am
+++ b/src/lib/util/Makefile.am
@@ -4,7 +4,6 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += -I$(top_srcdir)/src/lib/util -I$(top_builddir)/src/lib/util
AM_CPPFLAGS += -I$(top_srcdir)/src/lib/exceptions -I$(top_builddir)/src/lib/exceptions
AM_CPPFLAGS += $(BOOST_INCLUDES)
-AM_CPPFLAGS += -DLOCKFILE_DIR=\"${localstatedir}/${PACKAGE_NAME}\"
AM_CXXFLAGS = $(B10_CXXFLAGS)
# If we use the shared-memory support, corresponding Boost library may
# cause build failures especially if it's strict about warnings. We've
@@ -25,9 +24,6 @@ libb10_util_la_SOURCES += locks.h lru_list.h
libb10_util_la_SOURCES += strutil.h strutil.cc
libb10_util_la_SOURCES += buffer.h io_utilities.h
libb10_util_la_SOURCES += time_utilities.h time_utilities.cc
-libb10_util_la_SOURCES += interprocess_sync.h
-libb10_util_la_SOURCES += interprocess_sync_file.h interprocess_sync_file.cc
-libb10_util_la_SOURCES += interprocess_sync_null.h interprocess_sync_null.cc
libb10_util_la_SOURCES += memory_segment.h
libb10_util_la_SOURCES += memory_segment_local.h memory_segment_local.cc
if USE_SHARED_MEMORY
@@ -49,4 +45,4 @@ libb10_util_la_LIBADD = $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
CLEANFILES = *.gcno *.gcda
libb10_util_includedir = $(includedir)/$(PACKAGE_NAME)/util
-libb10_util_include_HEADERS = buffer.h
+libb10_util_include_HEADERS = buffer.h io_utilities.h
diff --git a/src/lib/util/buffer.h b/src/lib/util/buffer.h
index 4800e99..4aac11e 100644
--- a/src/lib/util/buffer.h
+++ b/src/lib/util/buffer.h
@@ -342,15 +342,17 @@ public:
/// \brief Assignment operator
OutputBuffer& operator =(const OutputBuffer& other) {
- uint8_t* newbuff(static_cast<uint8_t*>(malloc(other.allocated_)));
- if (newbuff == NULL && other.allocated_ != 0) {
- throw std::bad_alloc();
+ if (this != &other) {
+ uint8_t* newbuff(static_cast<uint8_t*>(malloc(other.allocated_)));
+ if (newbuff == NULL && other.allocated_ != 0) {
+ throw std::bad_alloc();
+ }
+ free(buffer_);
+ buffer_ = newbuff;
+ size_ = other.size_;
+ allocated_ = other.allocated_;
+ std::memcpy(buffer_, other.buffer_, size_);
}
- free(buffer_);
- buffer_ = newbuff;
- size_ = other.size_;
- allocated_ = other.allocated_;
- std::memcpy(buffer_, other.buffer_, size_);
return (*this);
}
diff --git a/src/lib/util/interprocess_sync.h b/src/lib/util/interprocess_sync.h
deleted file mode 100644
index f55f0ac..0000000
--- a/src/lib/util/interprocess_sync.h
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef INTERPROCESS_SYNC_H
-#define INTERPROCESS_SYNC_H
-
-#include <string>
-
-namespace isc {
-namespace util {
-
-class InterprocessSyncLocker; // forward declaration
-
-/// \brief Interprocess Sync Class
-///
-/// This class specifies an interface for mutual exclusion among
-/// co-operating processes. This is an abstract class and a real
-/// implementation such as InterprocessSyncFile should be used
-/// in code. Usage is as follows:
-///
-/// 1. Client instantiates a sync object of an implementation (such as
-/// InterprocessSyncFile).
-/// 2. Client then creates an automatic (stack) object of
-/// InterprocessSyncLocker around the sync object. Such an object
-/// destroys itself and releases any acquired lock when it goes out of extent.
-/// 3. Client calls lock() method on the InterprocessSyncLocker.
-/// 4. Client performs task that needs mutual exclusion.
-/// 5. Client frees lock with unlock(), or simply returns from the basic
-/// block which forms the scope for the InterprocessSyncLocker.
-///
-/// NOTE: All implementations of InterprocessSync should keep the
-/// is_locked_ member variable updated whenever their
-/// lock()/tryLock()/unlock() implementations are called.
-class InterprocessSync {
- // InterprocessSyncLocker is the only code outside this class that
- // should be allowed to call the lock(), tryLock() and unlock()
- // methods.
- friend class InterprocessSyncLocker;
-
-public:
- /// \brief Constructor
- ///
- /// Creates an interprocess synchronization object
- ///
- /// \param task_name Name of the synchronization task. This has to be
- /// identical among the various processes that need to be
- /// synchronized for the same task.
- InterprocessSync(const std::string& task_name) :
- task_name_(task_name), is_locked_(false)
- {}
-
- /// \brief Destructor
- virtual ~InterprocessSync() {}
-
-protected:
- /// \brief Acquire the lock (blocks if something else has acquired a
- /// lock on the same task name)
- ///
- /// \return Returns true if the lock was acquired, false otherwise.
- virtual bool lock() = 0;
-
- /// \brief Try to acquire a lock (doesn't block)
- ///
- /// \return Returns true if the lock was acquired, false otherwise.
- virtual bool tryLock() = 0;
-
- /// \brief Release the lock
- ///
- /// \return Returns true if the lock was released, false otherwise.
- virtual bool unlock() = 0;
-
- const std::string task_name_; ///< The task name
- bool is_locked_; ///< Is the lock taken?
-};
-
-/// \brief Interprocess Sync Locker Class
-///
-/// This class is used for making automatic stack objects to manage
-/// locks that are released automatically when the block is exited
-/// (RAII). It is meant to be used along with InterprocessSync objects. See
-/// the description of InterprocessSync.
-class InterprocessSyncLocker {
-public:
- /// \brief Constructor
- ///
- /// Creates a lock manager around a interprocess synchronization object
- ///
- /// \param sync The sync object which has to be locked/unlocked by
- /// this locker object.
- InterprocessSyncLocker(InterprocessSync& sync) :
- sync_(sync)
- {}
-
- /// \brief Destructor
- ~InterprocessSyncLocker() {
- if (isLocked())
- unlock();
- }
-
- /// \brief Acquire the lock (blocks if something else has acquired a
- /// lock on the same task name)
- ///
- /// \return Returns true if the lock was acquired, false otherwise.
- bool lock() {
- return (sync_.lock());
- }
-
- /// \brief Try to acquire a lock (doesn't block)
- ///
- /// \return Returns true if a new lock could be acquired, false
- /// otherwise.
- bool tryLock() {
- return (sync_.tryLock());
- }
-
- /// \brief Check if the lock is taken
- ///
- /// \return Returns true if a lock is currently acquired, false
- /// otherwise.
- bool isLocked() const {
- return (sync_.is_locked_);
- }
-
- /// \brief Release the lock
- ///
- /// \return Returns true if the lock was released, false otherwise.
- bool unlock() {
- return (sync_.unlock());
- }
-
-protected:
- InterprocessSync& sync_; ///< Ref to underlying sync object
-};
-
-} // namespace util
-} // namespace isc
-
-#endif // INTERPROCESS_SYNC_H
diff --git a/src/lib/util/interprocess_sync_file.cc b/src/lib/util/interprocess_sync_file.cc
deleted file mode 100644
index 25af55c..0000000
--- a/src/lib/util/interprocess_sync_file.cc
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include "interprocess_sync_file.h"
-
-#include <string>
-#include <cerrno>
-#include <cstring>
-
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-#include <fcntl.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-
-namespace isc {
-namespace util {
-
-InterprocessSyncFile::~InterprocessSyncFile() {
- if (fd_ != -1) {
- // This will also release any applied locks.
- close(fd_);
- // The lockfile will continue to exist, and we must not delete
- // it.
- }
-}
-
-bool
-InterprocessSyncFile::do_lock(int cmd, short l_type) {
- // Open lock file only when necessary (i.e., here). This is so that
- // if a default InterprocessSync object is replaced with another
- // implementation, it doesn't attempt any opens.
- if (fd_ == -1) {
- std::string lockfile_path = LOCKFILE_DIR;
-
- const char* const env = getenv("B10_FROM_BUILD");
- if (env != NULL) {
- lockfile_path = env;
- }
-
- const char* const env2 = getenv("B10_FROM_BUILD_LOCALSTATEDIR");
- if (env2 != NULL) {
- lockfile_path = env2;
- }
-
- const char* const env3 = getenv("B10_LOCKFILE_DIR_FROM_BUILD");
- if (env3 != NULL) {
- lockfile_path = env3;
- }
-
- lockfile_path += "/" + task_name_ + "_lockfile";
-
- // Open the lockfile in the constructor so it doesn't do the access
- // checks every time a message is logged.
- const mode_t mode = umask(0111);
- fd_ = open(lockfile_path.c_str(), O_CREAT | O_RDWR, 0660);
- umask(mode);
-
- if (fd_ == -1) {
- isc_throw(InterprocessSyncFileError,
- "Unable to use interprocess sync lockfile ("
- << std::strerror(errno) << "): " << lockfile_path);
- }
- }
-
- struct flock lock;
-
- memset(&lock, 0, sizeof (lock));
- lock.l_type = l_type;
- lock.l_whence = SEEK_SET;
- lock.l_start = 0;
- lock.l_len = 1;
-
- return (fcntl(fd_, cmd, &lock) == 0);
-}
-
-bool
-InterprocessSyncFile::lock() {
- if (is_locked_) {
- return (true);
- }
-
- if (do_lock(F_SETLKW, F_WRLCK)) {
- is_locked_ = true;
- return (true);
- }
-
- return (false);
-}
-
-bool
-InterprocessSyncFile::tryLock() {
- if (is_locked_) {
- return (true);
- }
-
- if (do_lock(F_SETLK, F_WRLCK)) {
- is_locked_ = true;
- return (true);
- }
-
- return (false);
-}
-
-bool
-InterprocessSyncFile::unlock() {
- if (!is_locked_) {
- return (true);
- }
-
- if (do_lock(F_SETLKW, F_UNLCK)) {
- is_locked_ = false;
- return (true);
- }
-
- return (false);
-}
-
-} // namespace util
-} // namespace isc
diff --git a/src/lib/util/interprocess_sync_file.h b/src/lib/util/interprocess_sync_file.h
deleted file mode 100644
index 029bdba..0000000
--- a/src/lib/util/interprocess_sync_file.h
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef INTERPROCESS_SYNC_FILE_H
-#define INTERPROCESS_SYNC_FILE_H
-
-#include <util/interprocess_sync.h>
-#include <exceptions/exceptions.h>
-
-namespace isc {
-namespace util {
-
-/// \brief InterprocessSyncFileError
-///
-/// Exception that is thrown if it's not possible to open the
-/// lock file.
-class InterprocessSyncFileError : public Exception {
-public:
- InterprocessSyncFileError(const char* file, size_t line,
- const char* what) :
- isc::Exception(file, line, what) {}
-};
-
-/// \brief File-based Interprocess Sync Class
-///
-/// This class specifies a concrete implementation for a file-based
-/// interprocess synchronization mechanism. Please see the
-/// InterprocessSync class documentation for usage.
-///
-/// An InterprocessSyncFileError exception may be thrown if there is an
-/// issue opening the lock file.
-///
-/// Lock files are created typically in the local state directory
-/// (var). They are typically named like "<task_name>_lockfile".
-/// This implementation opens lock files lazily (only when
-/// necessary). It also leaves the lock files lying around as multiple
-/// processes may have locks on them.
-class InterprocessSyncFile : public InterprocessSync {
-public:
- /// \brief Constructor
- ///
- /// Creates a file-based interprocess synchronization object
- ///
- /// \param task_name Name of the synchronization task. This has to be
- /// identical among the various processes that need to be
- /// synchronized for the same task.
- InterprocessSyncFile(const std::string& task_name) :
- InterprocessSync(task_name), fd_(-1)
- {}
-
- /// \brief Destructor
- virtual ~InterprocessSyncFile();
-
-protected:
- /// \brief Acquire the lock (blocks if something else has acquired a
- /// lock on the same task name)
- ///
- /// \return Returns true if the lock was acquired, false otherwise.
- bool lock();
-
- /// \brief Try to acquire a lock (doesn't block)
- ///
- /// \return Returns true if the lock was acquired, false otherwise.
- bool tryLock();
-
- /// \brief Release the lock
- ///
- /// \return Returns true if the lock was released, false otherwise.
- bool unlock();
-
-private:
- bool do_lock(int cmd, short l_type);
-
- int fd_; ///< The descriptor for the open file
-};
-
-} // namespace util
-} // namespace isc
-
-#endif // INTERPROCESS_SYNC_FILE_H
diff --git a/src/lib/util/interprocess_sync_null.cc b/src/lib/util/interprocess_sync_null.cc
deleted file mode 100644
index 5355d57..0000000
--- a/src/lib/util/interprocess_sync_null.cc
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include "interprocess_sync_null.h"
-
-namespace isc {
-namespace util {
-
-InterprocessSyncNull::~InterprocessSyncNull() {
-}
-
-bool
-InterprocessSyncNull::lock() {
- is_locked_ = true;
- return (true);
-}
-
-bool
-InterprocessSyncNull::tryLock() {
- is_locked_ = true;
- return (true);
-}
-
-bool
-InterprocessSyncNull::unlock() {
- is_locked_ = false;
- return (true);
-}
-
-} // namespace util
-} // namespace isc
diff --git a/src/lib/util/interprocess_sync_null.h b/src/lib/util/interprocess_sync_null.h
deleted file mode 100644
index 063cbe5..0000000
--- a/src/lib/util/interprocess_sync_null.h
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#ifndef INTERPROCESS_SYNC_NULL_H
-#define INTERPROCESS_SYNC_NULL_H
-
-#include <util/interprocess_sync.h>
-
-namespace isc {
-namespace util {
-
-/// \brief Null Interprocess Sync Class
-///
-/// This class specifies a concrete implementation for a null (no effect)
-/// interprocess synchronization mechanism. Please see the
-/// InterprocessSync class documentation for usage.
-class InterprocessSyncNull : public InterprocessSync {
-public:
- /// \brief Constructor
- ///
- /// Creates a null interprocess synchronization object
- ///
- /// \param task_name Name of the synchronization task. This has to be
- /// identical among the various processes that need to be
- /// synchronized for the same task.
- InterprocessSyncNull(const std::string& task_name) :
- InterprocessSync(task_name)
- {}
-
- /// \brief Destructor
- virtual ~InterprocessSyncNull();
-
-protected:
- /// \brief Acquire the lock (never blocks)
- ///
- /// \return Always returns true
- bool lock();
-
- /// \brief Try to acquire a lock (doesn't block)
- ///
- /// \return Always returns true
- bool tryLock();
-
- /// \brief Release the lock
- ///
- /// \return Always returns true
- bool unlock();
-};
-
-} // namespace util
-} // namespace isc
-
-#endif // INTERPROCESS_SYNC_NULL_H
diff --git a/src/lib/util/memory_segment.h b/src/lib/util/memory_segment.h
index e62c9df..94aa3ec 100644
--- a/src/lib/util/memory_segment.h
+++ b/src/lib/util/memory_segment.h
@@ -17,6 +17,8 @@
#include <exceptions/exceptions.h>
+#include <utility>
+
#include <stdlib.h>
namespace isc {
@@ -116,6 +118,8 @@ public:
/// requested storage.
/// \throw MemorySegmentGrown The memory segment doesn't have sufficient
/// space for the requested size and has grown internally.
+ /// \throw MemorySegmentError An attempt was made to allocate
+ /// storage on a read-only memory segment.
///
/// \param size The size of the memory requested in bytes.
/// \return Returns pointer to the memory allocated.
@@ -167,6 +171,10 @@ public:
/// corresponding address by that name (in such cases the real address
/// may be different between these two processes).
///
+ /// Some names are reserved for internal use by this class. If such
+ /// a name is passed to this method, an \c isc::InvalidParameter
+ /// exception will be thrown. See \c validateName() method for details.
+ ///
/// \c addr must be 0 (NULL) or an address that belongs to this segment.
/// The latter case means it must be the return value of a previous call
/// to \c allocate(). The actual implementation is encouraged to detect
@@ -176,7 +184,8 @@ public:
/// as \c addr even if it wouldn't be considered to "belong to" the
/// segment in its normal sense; it can be used to indicate that memory
/// has not been allocated for the specified name. A subsequent call
- /// to \c getNamedAddress() will return NULL for that name.
+ /// to \c getNamedAddress() will return NamedAddressResult(true, NULL)
+ /// for that name.
///
/// \note Naming an address is intentionally separated from allocation
/// so that, for example, one module of a program can name a memory
@@ -209,7 +218,8 @@ public:
///
/// \throw std::bad_alloc Allocation of a segment space for the given name
/// failed.
- /// \throw InvalidParameter name is NULL.
+ /// \throw InvalidParameter name is NULL, empty ("") or begins with
+ /// an underscore ('_').
/// \throw MemorySegmentError Failure of implementation specific
/// validation.
///
@@ -221,13 +231,13 @@ public:
// This public method implements common validation. The actual
// work specific to the derived segment is delegated to the
// corresponding protected method.
- if (!name) {
- isc_throw(InvalidParameter,
- "NULL name is given to setNamedAddress");
- }
+ validateName(name);
return (setNamedAddressImpl(name, addr));
}
+ /// \brief Type definition for result returned by getNamedAddress()
+ typedef std::pair<bool, void*> NamedAddressResult;
+
/// \brief Return the address in the segment that has the given name.
///
/// This method returns the memory address in the segment corresponding
@@ -235,25 +245,29 @@ public:
/// associated by a prior call to \c setNameAddress(). If no address
/// associated with the given name is found, it returns NULL.
///
+ /// Some names are reserved for internal use by this class. If such
+ /// a name is passed to this method, an \c isc::InvalidParameter
+ /// exception will be thrown. See \c validateName() method for details.
+ ///
/// This method should generally be considered exception free, but there
/// can be a small chance it throws, depending on the internal
/// implementation (e.g., if it converts the name to std::string), so the
/// API doesn't guarantee that property. In general, if this method
/// throws it should be considered a fatal condition.
///
- /// \throw InvalidParameter name is NULL.
+ /// \throw InvalidParameter name is NULL, empty ("") or begins with
+ /// an underscore ('_').
///
/// \param name A C string of which the segment memory address is to be
/// returned. Must not be NULL.
- /// \return The address associated with the name, or NULL if not found.
- void* getNamedAddress(const char* name) {
+ /// \return An std::pair containing a bool (set to true if the name
+ /// was found, or false otherwise) and the address associated with
+ /// the name (which is undefined if the name was not found).
+ NamedAddressResult getNamedAddress(const char* name) const {
// This public method implements common validation. The actual
// work specific to the derived segment is delegated to the
// corresponding protected method.
- if (!name) {
- isc_throw(InvalidParameter,
- "NULL name is given to getNamedAddress");
- }
+ validateName(name);
return (getNamedAddressImpl(name));
}
@@ -264,9 +278,14 @@ public:
/// \c setNamedAddress(). If there is no association for the given name
/// this method returns false; otherwise it returns true.
///
+ /// Some names are reserved for internal use by this class. If such
+ /// a name is passed to this method, an \c isc::InvalidParameter
+ /// exception will be thrown. See \c validateName() method for details.
+ ///
/// See \c getNamedAddress() about exception consideration.
///
- /// \throw InvalidParameter name is NULL.
+ /// \throw InvalidParameter name is NULL, empty ("") or begins with
+ /// an underscore ('_').
/// \throw MemorySegmentError Failure of implementation specific
/// validation.
///
@@ -276,11 +295,29 @@ public:
// This public method implements common validation. The actual
// work specific to the derived segment is delegated to the
// corresponding protected method.
+ validateName(name);
+ return (clearNamedAddressImpl(name));
+ }
+
+private:
+ /// \brief Validate the passed name.
+ ///
+ /// This method validates the passed name (for name/address pairs)
+ /// and throws \c InvalidParameter if the name fails
+ /// validation. Otherwise, it does nothing.
+ ///
+ /// \throw InvalidParameter name is NULL, empty ("") or begins with
+ /// an underscore ('_').
+ static void validateName(const char* name) {
if (!name) {
+ isc_throw(InvalidParameter, "NULL is invalid for a name.");
+ } else if (*name == '\0') {
+ isc_throw(InvalidParameter, "Empty names are invalid.");
+ } else if (*name == '_') {
isc_throw(InvalidParameter,
- "NULL name is given to clearNamedAddress");
+ "Names beginning with '_' are reserved for "
+ "internal use only.");
}
- return (clearNamedAddressImpl(name));
}
protected:
@@ -288,7 +325,7 @@ protected:
virtual bool setNamedAddressImpl(const char* name, void* addr) = 0;
/// \brief Implementation of getNamedAddress beyond common validation.
- virtual void* getNamedAddressImpl(const char* name) = 0;
+ virtual NamedAddressResult getNamedAddressImpl(const char* name) const = 0;
/// \brief Implementation of clearNamedAddress beyond common validation.
virtual bool clearNamedAddressImpl(const char* name) = 0;
diff --git a/src/lib/util/memory_segment_local.cc b/src/lib/util/memory_segment_local.cc
index 81548fd..b81fe5e 100644
--- a/src/lib/util/memory_segment_local.cc
+++ b/src/lib/util/memory_segment_local.cc
@@ -51,13 +51,14 @@ MemorySegmentLocal::allMemoryDeallocated() const {
return (allocated_size_ == 0 && named_addrs_.empty());
}
-void*
-MemorySegmentLocal::getNamedAddressImpl(const char* name) {
- std::map<std::string, void*>::iterator found = named_addrs_.find(name);
+MemorySegment::NamedAddressResult
+MemorySegmentLocal::getNamedAddressImpl(const char* name) const {
+ std::map<std::string, void*>::const_iterator found =
+ named_addrs_.find(name);
if (found != named_addrs_.end()) {
- return (found->second);
+ return (NamedAddressResult(true, found->second));
}
- return (0);
+ return (NamedAddressResult(false, NULL));
}
bool
diff --git a/src/lib/util/memory_segment_local.h b/src/lib/util/memory_segment_local.h
index 1db55a0..de7249e 100644
--- a/src/lib/util/memory_segment_local.h
+++ b/src/lib/util/memory_segment_local.h
@@ -70,7 +70,7 @@ public:
///
/// There's a small chance this method could throw std::bad_alloc.
/// It should be considered a fatal error.
- virtual void* getNamedAddressImpl(const char* name);
+ virtual NamedAddressResult getNamedAddressImpl(const char* name) const;
/// \brief Local segment version of setNamedAddress.
///
diff --git a/src/lib/util/memory_segment_mapped.cc b/src/lib/util/memory_segment_mapped.cc
index e2ac944..8fea5ea 100644
--- a/src/lib/util/memory_segment_mapped.cc
+++ b/src/lib/util/memory_segment_mapped.cc
@@ -28,6 +28,8 @@
#include <string>
#include <new>
+#include <stdint.h>
+
// boost::interprocess namespace is big and can cause unexpected import
// (e.g., it has "read_only"), so it's safer to be specific for shortcuts.
using boost::interprocess::basic_managed_mapped_file;
@@ -44,6 +46,15 @@ using boost::interprocess::offset_ptr;
namespace isc {
namespace util {
+
+namespace { // unnamed namespace
+
+const char* const RESERVED_NAMED_ADDRESS_STORAGE_NAME =
+ "_RESERVED_NAMED_ADDRESS_STORAGE";
+
+} // end of unnamed namespace
+
+
// Definition of class static constant so it can be referenced by address
// or reference.
const size_t MemorySegmentMapped::INITIAL_SIZE;
@@ -98,6 +109,7 @@ struct MemorySegmentMapped::Impl {
// confirm there's no other user and there won't either.
lock_.reset(new boost::interprocess::file_lock(filename.c_str()));
checkWriter();
+ reserveMemory();
}
// Constructor for open-or-write (and read-write) mode
@@ -108,6 +120,7 @@ struct MemorySegmentMapped::Impl {
lock_(new boost::interprocess::file_lock(filename.c_str()))
{
checkWriter();
+ reserveMemory();
}
// Constructor for existing segment, either read-only or read-write
@@ -123,12 +136,45 @@ struct MemorySegmentMapped::Impl {
} else {
checkWriter();
}
+ reserveMemory();
+ }
+
+ void reserveMemory(bool no_grow = false) {
+ if (!read_only_) {
+ // Reserve a named address for use during
+ // setNamedAddress(). Though this will almost always succeed
+ // on the first try during construction, it may require
+ // multiple attempts later during a call from
+ // allMemoryDeallocated() when the segment has been in use
+ // for a while.
+ while (true) {
+ const offset_ptr<void>* reserved_storage =
+ base_sgmt_->find_or_construct<offset_ptr<void> >(
+ RESERVED_NAMED_ADDRESS_STORAGE_NAME, std::nothrow)();
+
+ if (reserved_storage) {
+ break;
+ }
+ assert(!no_grow);
+
+ growSegment();
+ }
+ }
+ }
+
+ void freeReservedMemory() {
+ if (!read_only_) {
+ const bool deleted = base_sgmt_->destroy<offset_ptr<void> >
+ (RESERVED_NAMED_ADDRESS_STORAGE_NAME);
+ assert(deleted);
+ }
}
// Internal helper to grow the underlying mapped segment.
void growSegment() {
// We first need to unmap it before calling grow().
const size_t prev_size = base_sgmt_->get_size();
+ base_sgmt_->flush();
base_sgmt_.reset();
// Double the segment size. In theory, this process could repeat
@@ -139,16 +185,21 @@ struct MemorySegmentMapped::Impl {
const size_t new_size = prev_size * 2;
assert(new_size > prev_size);
- if (!BaseSegment::grow(filename_.c_str(), new_size - prev_size)) {
- throw std::bad_alloc();
- }
+ const bool grown = BaseSegment::grow(filename_.c_str(),
+ new_size - prev_size);
+ // Remap the file, whether or not grow() succeeded. this should
+ // normally succeed(*), but it's not 100% guaranteed. We abort
+ // if it fails (see the method description in the header file).
+ // (*) Although it's not formally documented, the implementation
+ // of grow() seems to provide strong guarantee, i.e, if it fails
+ // the underlying file can be used with the previous size.
try {
- // Remap the grown file; this should succeed, but it's not 100%
- // guaranteed. If it fails we treat it as if we fail to create
- // the new segment.
base_sgmt_.reset(new BaseSegment(open_only, filename_.c_str()));
- } catch (const boost::interprocess::interprocess_exception& ex) {
+ } catch (...) {
+ abort();
+ }
+ if (!grown) {
throw std::bad_alloc();
}
}
@@ -227,6 +278,7 @@ MemorySegmentMapped::MemorySegmentMapped(const std::string& filename,
MemorySegmentMapped::~MemorySegmentMapped() {
if (impl_->base_sgmt_ && !impl_->read_only_) {
+ impl_->freeReservedMemory();
impl_->base_sgmt_->flush(); // note: this is exception free
}
delete impl_;
@@ -276,17 +328,29 @@ MemorySegmentMapped::deallocate(void* ptr, size_t) {
bool
MemorySegmentMapped::allMemoryDeallocated() const {
- return (impl_->base_sgmt_->all_memory_deallocated());
+ // This method is not technically const, but it reserves the
+ // const-ness property. In case of exceptions, we abort here. (See
+ // ticket #2850 for additional commentary.)
+ try {
+ impl_->freeReservedMemory();
+ const bool result = impl_->base_sgmt_->all_memory_deallocated();
+ // reserveMemory() should succeed now as the memory was already
+ // allocated, so we set no_grow to true.
+ impl_->reserveMemory(true);
+ return (result);
+ } catch (...) {
+ abort();
+ }
}
-void*
-MemorySegmentMapped::getNamedAddressImpl(const char* name) {
+MemorySegment::NamedAddressResult
+MemorySegmentMapped::getNamedAddressImpl(const char* name) const {
offset_ptr<void>* storage =
impl_->base_sgmt_->find<offset_ptr<void> >(name).first;
if (storage) {
- return (storage->get());
+ return (NamedAddressResult(true, storage->get()));
}
- return (NULL);
+ return (NamedAddressResult(false, NULL));
}
bool
@@ -299,13 +363,27 @@ MemorySegmentMapped::setNamedAddressImpl(const char* name, void* addr) {
isc_throw(MemorySegmentError, "address is out of segment: " << addr);
}
+ // Temporarily save the passed addr into pre-allocated offset_ptr in
+ // case there are any relocations caused by allocations.
+ offset_ptr<void>* reserved_storage =
+ impl_->base_sgmt_->find<offset_ptr<void> >(
+ RESERVED_NAMED_ADDRESS_STORAGE_NAME).first;
+ assert(reserved_storage);
+ *reserved_storage = addr;
+
bool grown = false;
while (true) {
offset_ptr<void>* storage =
impl_->base_sgmt_->find_or_construct<offset_ptr<void> >(
name, std::nothrow)();
if (storage) {
- *storage = addr;
+ // Move the address from saved offset_ptr into the
+ // newly-allocated storage.
+ reserved_storage =
+ impl_->base_sgmt_->find<offset_ptr<void> >(
+ RESERVED_NAMED_ADDRESS_STORAGE_NAME).first;
+ assert(reserved_storage);
+ *storage = *reserved_storage;
return (grown);
}
@@ -341,14 +419,20 @@ MemorySegmentMapped::shrinkToFit() {
return;
}
- // First, (unmap and) close the underlying file.
+ // First, unmap the underlying file.
+ impl_->base_sgmt_->flush();
impl_->base_sgmt_.reset();
BaseSegment::shrink_to_fit(impl_->filename_.c_str());
try {
// Remap the shrunk file; this should succeed, but it's not 100%
// guaranteed. If it fails we treat it as if we fail to create
- // the new segment.
+ // the new segment. Note that this is different from the case where
+ // reset() after grow() fails. While the same argument can apply
+ // in theory, it should be less likely that other methods will be
+ // called after shrinkToFit() (and the destructor can still be called
+ // safely), so we give the application an opportunity to handle the
+ // case as gracefully as possible.
impl_->base_sgmt_.reset(
new BaseSegment(open_only, impl_->filename_.c_str()));
} catch (const boost::interprocess::interprocess_exception& ex) {
diff --git a/src/lib/util/memory_segment_mapped.h b/src/lib/util/memory_segment_mapped.h
index 7685e30..301b174 100644
--- a/src/lib/util/memory_segment_mapped.h
+++ b/src/lib/util/memory_segment_mapped.h
@@ -150,7 +150,14 @@ public:
/// \brief Allocate/acquire a segment of memory.
///
- /// This version can throw \c MemorySegmentGrown.
+ /// This version can throw \c MemorySegmentGrown. Furthermore, there is
+ /// a very small chance that the object loses its integrity and can't be
+ /// usable in the case where \c MemorySegmentGrown would be thrown.
+ /// In this case, throwing a different exception wouldn't help, because
+ /// an application trying to provide exception safety might then call
+ /// deallocate() or named address APIs on this object, which would simply
+ /// cause a crash. So, while suboptimal, this method just aborts the
+ /// program in this case, indicating there's no hope to shutdown cleanly.
///
/// This method cannot be called if the segment object is created in the
/// read-only mode; in that case MemorySegmentError will be thrown.
@@ -195,7 +202,7 @@ public:
/// \brief Mapped segment version of getNamedAddress.
///
/// This version never throws.
- virtual void* getNamedAddressImpl(const char* name);
+ virtual NamedAddressResult getNamedAddressImpl(const char* name) const;
/// \brief Mapped segment version of clearNamedAddress.
///
diff --git a/src/lib/util/python/.gitignore b/src/lib/util/python/.gitignore
index c54df80..619c69f 100644
--- a/src/lib/util/python/.gitignore
+++ b/src/lib/util/python/.gitignore
@@ -1,2 +1,3 @@
+/doxygen2pydoc.py
/gen_wiredata.py
/mkpywrapper.py
diff --git a/src/lib/util/python/Makefile.am b/src/lib/util/python/Makefile.am
index 1e05688..c7ddf3d 100644
--- a/src/lib/util/python/Makefile.am
+++ b/src/lib/util/python/Makefile.am
@@ -1,3 +1,3 @@
-noinst_SCRIPTS = gen_wiredata.py mkpywrapper.py const2hdr.py \
+noinst_SCRIPTS = doxygen2pydoc.py gen_wiredata.py mkpywrapper.py const2hdr.py \
pythonize_constants.py
EXTRA_DIST = const2hdr.py pythonize_constants.py
diff --git a/src/lib/util/python/doxygen2pydoc.py.in b/src/lib/util/python/doxygen2pydoc.py.in
new file mode 100755
index 0000000..7aa74ec
--- /dev/null
+++ b/src/lib/util/python/doxygen2pydoc.py.in
@@ -0,0 +1,680 @@
+#!@PYTHON@
+
+# Copyright (C) 2011 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+r"""
+A helper to semi-auto generate Python docstring text from C++ Doxygen
+documentation.
+
+This script converts an XML-format doxygen documentation for C++ library
+into a template Python docstring for the corresponding Python version
+of the library. While it's not perfect and you'll still need to edit the
+output by hand, but past experiments showed the script produces a pretty
+good template. It will help provide more compatible documentation for
+both C++ and Python versions of library from a unified source (C++ Doxygen
+documentation) with minimizing error-prone and boring manual conversion.
+
+HOW TO USE IT
+
+1. Generate XML output by doxygen. Use bind10/doc/Doxyfile-xml:
+
+ % cd bind10/doc
+ % doxygen Doxyfile-xml
+ (XML files will be generated under bind10/doc/html/xml)
+
+2. Identify the xml file of the conversion target (C++ class, function, etc)
+
+ This is a bit tricky. You'll probably need to do manual search.
+ For example, to identify the xml file for a C++ class
+ isc::datasrc::memory::ZoneWriter, you might do:
+
+ % cd bind10/doc/html/xml
+ % grep ZoneWriter *.xml | grep 'kind="class"'
+ index.xml: <compound refid="d4/d3c/classisc_1_1datasrc_1_1memory_1_1ZoneWriter" kind="class"><name>isc::datasrc::memory::ZoneWriter</name>
+
+ In this case the file under the d4/d3c directory (with .xml suffix) would
+ be the file you're looking for.
+
+3. Run this script for the xml file:
+
+ % python3 doxygen2pydoc.py <top_srcdir>/doc/html/xml/d4/d3c/classisc_1_1datasrc_1_1memory_1_1ZoneWriter.xml > output.cc
+
+ The template content is dumped to standard out (redirected to file
+ "output.cc" in this example).
+
+ Sometimes the script produces additional output to standard error,
+ like this:
+
+ Replaced camelCased terms:
+ resetMemorySegment => reset_memory_segment
+ getConfiguration => get_configuration
+
+ In BIND 10 naming convention for methods is different for C++ and
+ Python. This script uses some heuristic guess to convert the
+ C++-style method names to likely Python-style ones, and the converted
+ method names are used in the dumped template. In many cases the guessed
+ names are correct, but you should check this list and make adjustments
+ by hand if necessary.
+
+ If there's no standard error output, this type of conversion didn't
+ happen.
+
+4. Edit and copy the template
+
+ The dumped template has the following organization:
+
+ namespace {
+ #ifdef COPY_THIS_TO_MAIN_CC
+ { "cleanup", ZoneWriter_cleanup, METH_NOARGS,
+ ZoneWriter_cleanup_doc },
+ { "install", ZoneWriter_install, METH_NOARGS,
+ ZoneWriter_install_doc },
+ { "load", ZoneWriter_load, METH_VARARGS,
+ ZoneWriter_load_doc },
+ #endif // COPY_THIS_TO_MAIN_CC
+
+ const char* const ZoneWriter_doc = "\
+ ...
+ ";
+
+ const char* const ZoneWriter_install_doc = "\
+ ...
+ ";
+
+ ...
+ }
+
+ The ifdef-ed block is a template for class methods information
+ to be added to the corresponding PyMethodDef structure array
+ (your wrapper C++ source would have something like ZoneWriter_methods
+ of this type). These lines should be copied there. As long as
+ the method names and corresponding wrapper function (such as
+ ZoneWriter_cleanup) are correct you shouldn't have to edit this part
+ (and they would be normally correct, unless the guessed method name
+ conversion was needed).
+
+ The rest of the content is a sequence of constant C-string variables.
+ Usually the first variable corresponds to the class description, and
+ the rest are method descriptions (note that ZoneWriter_install_doc
+ is referenced from the ifdef-ed block). The content of this part
+ would generally make sense, but you'll often need to make some
+ adjsutments by hand. A common examples of such adjustment is to
+ replace "NULL" with "None". Also, it's not uncommon that some part
+ of the description simply doesn't apply to the Python version or
+ that Python specific notes are needed. So go through the description
+ carefully and make necessary changes. A common practice is to add
+ comments for a summary of adjustments like this:
+
+ // Modifications:
+ // NULL->None
+ // - removed notes about derived classes (which doesn't apply for python)
+ const char* const ZoneWriter_doc = "\
+ ...
+ ";
+ This note will help next time you need to auto-generate and edit the
+ template (probably because the original C++ document is updated).
+
+ You can simply copy this part to the main C++ wrapper file, but since
+ it's relatively large a common practice is to maintain it in a separate
+ file that is exclusively included from the main file: if the name of
+ the main file is zonewriter_python.cc, the pydoc strings would be copied
+ in zonewriter_python_inc.cc, and the main file would have this line:
+
+ #include "zonewriter_inc.cc"
+
+ (In case you are C++ language police: it's okay to use the unnamed
+ name space for a file to be included because it's essentially a part
+ of the single .cc file, not expected to be included by others).
+
+ In either case, the ifdef-ed part should be removed.
+
+ADVANCED FEATURES
+
+You can use a special "xmlonly" doxygen command in C++ doxygent document
+in order to include Python code excerpt (while hiding it from the doxygen
+output for the C++ version). This command will be converted to
+a special XML tag in the XML output.
+
+The block enclosed by \xmlonly and \endxmlonly should contain
+a verbatim XML tag named "pythonlisting", in which the python code should
+be placed.
+/// \code
+/// Name name("example.com");
+/// std::cout << name.toText() << std::endl;
+/// \endcode
+///
+/// \xmlonly <pythonlisting>
+/// name = Name("example.com")
+/// print(name.to_text())
+/// </pythonlisting> \endxmlonly
+
+Note that there must be a blank line between \endcode and \xmlonly.
+doxygen2pydoc assume the pythonlisting tag is in a separate <para> node.
+This blank ensures doxygen will produce the XML file that meets the
+assumption.
+
+INTERNAL MEMO (incomplete, and not very unredable yet)
+
+This simplified utility assumes the following structure:
+...
+ <compounddef ...>
+ <compoundname>isc::dns::TSIGError</compoundname>
+ <sectiondef kind="user-defined">
+ constructor, destructor
+ </sectiondef>
+ <sectiondef kind="public-type">
+ ..
+ </sectiondef>
+ <sectiondef kind="public-func">
+ <memberdef kind="function"...>
+ <type>return type (if any)</type>
+ <argsstring>(...) [const]</argsstring>
+ <name>method name</name>
+ <briefdescription>method's brief description</briefdescription>
+ <detaileddescription>
+ <para>...</para>...
+ <para>
+ <parameterlist kind="exception">
+ <parameteritem>
+ <parameternamelist>
+ <parametername>Exception name</parametername>
+ </parameternamelist>
+ <parameterdescription>
+ <para>exception desc</para>
+ </parameterdescription>
+ </parameteritem>
+ </parameterlist>
+ <parameterlist kind="param">
+ <parameteritem>
+ <parameternamelist>
+ <parametername>param name</parametername>
+ </parameternamelist>
+ <parameterdescription>
+ <para>param desc</para>
+ </parameterdescription>
+ </parameteritem>
+ ...
+ </parameterlist>
+ <simplesect kind="return">Return value</simplesect>
+ </para>
+ </detaileddescription>
+ </memberdef>
+ </sectiondef>
+ <sectiondef kind="public-static-attrib|user-defined">
+ <memberdef kind="variable"...>
+ <name>class-specific-constant</name>
+ <initializer>value</initializer>
+ <brief|detaileddescription>paragraph(s)</brief|detaileddescription>
+ </sectiondef>
+ <briefdescription>
+ class's brief description
+ </briefdescription>
+ <detaileddescription>
+ class's detailed description
+ </detaileddescription>
+ </compounddef>
+"""
+
+import re, string, sys, textwrap
+from xml.dom.minidom import parse
+from textwrap import fill, dedent, TextWrapper
+
+camel_replacements = {}
+member_functions = []
+constructors = []
+class_variables = []
+
+RE_CAMELTERM = re.compile('([\s\.]|^)[a-z]+[A-Z]\S*')
+RE_SIMPLECAMEL = re.compile("([a-z])([A-Z])")
+RE_CAMELAFTERUPPER = re.compile("([A-Z])([A-Z])([a-z])")
+
+class Paragraph:
+ TEXT = 0
+ ITEMIZEDLIST = 1
+ CPPLISTING = 2
+ PYLISTING = 3
+ VERBATIM = 4
+
+ def __init__(self, xml_node):
+ if len(xml_node.getElementsByTagName("pythonlisting")) > 0:
+ self.type = self.PYLISTING
+ self.text = re.sub("///", "", get_text(xml_node))
+ elif len(xml_node.getElementsByTagName("verbatim")) > 0:
+ self.type = self.VERBATIM
+ self.text = get_text(xml_node)
+ elif len(xml_node.getElementsByTagName("programlisting")) > 0:
+ # We ignore node containing a "programlisting" tag.
+ # They are C++ example code, and we are not interested in them
+ # in pydoc.
+ self.type = self.CPPLISTING
+ elif len(xml_node.getElementsByTagName("itemizedlist")) > 0:
+ self.type = self.ITEMIZEDLIST
+ self.items = []
+ for item in xml_node.getElementsByTagName("listitem"):
+ self.items.append(get_text(item))
+ else:
+ self.type = self.TEXT
+
+ # A single textual paragraph could have multiple simple sections
+ # if it contains notes.
+
+ self.texts = []
+ subnodes = []
+ for child in xml_node.childNodes:
+ if child.nodeType == child.ELEMENT_NODE and \
+ child.nodeName == 'simplesect' and \
+ child.getAttribute('kind') == 'note':
+ if len(subnodes) > 0:
+ self.texts.append(get_text_fromnodelist(subnodes))
+ subnodes = []
+ subtext = 'Note: '
+ for t in child.childNodes:
+ subtext += get_text(t)
+ self.texts.append(subtext)
+ else:
+ subnodes.append(child)
+ if len(subnodes) > 0:
+ self.texts.append(get_text_fromnodelist(subnodes))
+
+ def dump(self, f, wrapper):
+ if self.type == self.CPPLISTING:
+ return
+ elif self.type == self.ITEMIZEDLIST:
+ for item in self.items:
+ item_wrapper = TextWrapper(\
+ initial_indent=wrapper.initial_indent + "- ",
+ subsequent_indent=wrapper.subsequent_indent + " ")
+ dump_filled_text(f, item_wrapper, item)
+ f.write("\\n\\\n")
+ elif self.type == self.TEXT:
+ for text in self.texts:
+ if text != self.texts[0]:
+ f.write("\\n\\\n")
+ dump_filled_text(f, wrapper, text)
+ f.write("\\n\\\n")
+ else:
+ dump_filled_text(f, None, self.text)
+ f.write("\\n\\\n")
+ f.write("\\n\\\n")
+
+class NamedItem:
+ def __init__(self, name, desc):
+ self.name = name
+ self.desc = desc
+
+ def dump(self, f, wrapper):
+ # we use deeper indent inside the item list.
+ new_initial_indent = wrapper.initial_indent + " " * 2
+ new_subsequent_indent = wrapper.subsequent_indent + " " * (2 + 11)
+ local_wrapper = TextWrapper(initial_indent=new_initial_indent,
+ subsequent_indent=new_subsequent_indent)
+
+ # concatenate name and description with a fixed width (up to 10 chars)
+ # for the name, and wrap the entire text, then dump it to file.
+ dump_filled_text(f, local_wrapper, "%-10s %s" % (self.name, self.desc))
+ f.write("\\n\\\n")
+
+class FunctionDefinition:
+ # function types
+ CONSTRUCTOR = 0
+ COPY_CONSTRUCTOR = 1
+ DESTRUCTOR = 2
+ ASSIGNMENT_OP = 3
+ OTHER = 4
+
+ def __init__(self):
+ self.type = self.OTHER
+ self.name = None
+ self.pyname = None
+ self.args = ""
+ self.ret_type = None
+ self.brief_desc = None
+ self.detailed_desc = []
+ self.exceptions = []
+ self.parameters = []
+ self.returns = None
+ self.have_param = False
+
+ def dump_doc(self, f, wrapper=TextWrapper()):
+ f.write(self.pyname + "(" + self.args + ")")
+ if self.ret_type is not None:
+ f.write(" -> " + self.ret_type)
+ f.write("\\n\\\n\\n\\\n")
+
+ if self.brief_desc is not None:
+ dump_filled_text(f, wrapper, self.brief_desc)
+ f.write("\\n\\\n\\n\\\n")
+
+ for para in self.detailed_desc:
+ para.dump(f, wrapper)
+
+ if len(self.exceptions) > 0:
+ f.write(wrapper.fill("Exceptions:") + "\\n\\\n")
+ for ex_desc in self.exceptions:
+ ex_desc.dump(f, wrapper)
+ f.write("\\n\\\n")
+ if len(self.parameters) > 0:
+ f.write(wrapper.fill("Parameters:") + "\\n\\\n")
+ for param_desc in self.parameters:
+ param_desc.dump(f, wrapper)
+ f.write("\\n\\\n")
+ if self.returns is not None:
+ dump_filled_text(f, wrapper, "Return Value(s): " + self.returns)
+ f.write("\\n\\\n")
+
+ def dump_pymethod_def(self, f, class_name):
+ f.write(' { "' + self.pyname + '", ')
+ f.write(class_name + '_' + self.name + ', ')
+ if len(self.parameters) == 0:
+ f.write('METH_NOARGS,\n')
+ else:
+ f.write('METH_VARARGS,\n')
+ f.write(' ' + class_name + '_' + self.name + '_doc },\n')
+
+class VariableDefinition:
+ def __init__(self, nodelist):
+ self.value = None
+ self.brief_desc = None
+ self.detailed_desc = []
+
+ for node in nodelist:
+ if node.nodeName == "name":
+ self.name = get_text(node)
+ elif node.nodeName == "initializer":
+ self.value = get_text(node)
+ elif node.nodeName == "briefdescription":
+ self.brief_desc = get_text(node)
+ elif node.nodeName == "detaileddescription":
+ for para in node.childNodes:
+ if para.nodeName != "para":
+ # ignore surrounding empty nodes
+ continue
+ self.detailed_desc.append(Paragraph(para))
+
+ def dump_doc(self, f, wrapper=TextWrapper()):
+ name_value = self.name
+ if self.value is not None:
+ name_value += ' = ' + self.value
+ dump_filled_text(f, wrapper, name_value)
+ f.write('\\n\\\n')
+
+ desc_initial_indent = wrapper.initial_indent + " "
+ desc_subsequent_indent = wrapper.subsequent_indent + " "
+ desc_wrapper = TextWrapper(initial_indent=desc_initial_indent,
+ subsequent_indent=desc_subsequent_indent)
+ if self.brief_desc is not None:
+ dump_filled_text(f, desc_wrapper, self.brief_desc)
+ f.write("\\n\\\n\\n\\\n")
+
+ for para in self.detailed_desc:
+ para.dump(f, desc_wrapper)
+
+def dump_filled_text(f, wrapper, text):
+ """Fill given text using wrapper, and dump it to the given file
+ appending an escaped CR at each end of line.
+ """
+ filled_text = wrapper.fill(text) if wrapper is not None else text
+ f.write("".join(re.sub("\n", r"\\n\\\n", filled_text)))
+
+def camel_to_lowerscores(matchobj):
+ oldtext = matchobj.group(0)
+ newtext = re.sub(RE_SIMPLECAMEL, r"\1_\2", oldtext)
+ newtext = re.sub(RE_CAMELAFTERUPPER, r"\1_\2\3", newtext)
+ newtext = newtext.lower()
+ camel_replacements[oldtext] = newtext
+ return newtext.lower()
+
+def cpp_to_python(text):
+ text = text.replace("::", ".")
+ text = text.replace('"', '\\"')
+
+ # convert camelCase to "_"-concatenated format
+ # (e.g. getLength -> get_length)
+ return re.sub(RE_CAMELTERM, camel_to_lowerscores, text)
+
+def convert_type_name(type_name):
+ """Convert C++ type name to python type name using common conventions"""
+ # strip off leading 'const' and trailing '&/*'
+ type_name = re.sub("^const\S*", "", type_name)
+ type_name = re.sub("\S*[&\*]$", "", type_name)
+
+ # We often typedef smart pointers as [Const]TypePtr. Convert them to
+ # just "Type"
+ type_name = re.sub("^Const", "", type_name)
+ type_name = re.sub("Ptr$", "", type_name)
+
+ if type_name == "std::string":
+ return "string"
+ if re.search(r"(int\d+_t|size_t)", type_name):
+ return "integer"
+ return type_name
+
+def get_text(root, do_convert=True):
+ """Recursively extract bare text inside the specified node (root),
+ concatenate all extracted text and return the result.
+ """
+ nodelist = root.childNodes
+ rc = []
+ for node in nodelist:
+ if node.nodeType == node.TEXT_NODE:
+ if do_convert:
+ rc.append(cpp_to_python(node.data))
+ else:
+ rc.append(node.data)
+ elif node.nodeType == node.ELEMENT_NODE:
+ rc.append(get_text(node))
+ # return the result, removing any leading newlines (that often happens for
+ # brief descriptions, which will cause lines not well aligned)
+ return re.sub("^(\n*)", "", ''.join(rc))
+
+def get_text_fromnodelist(nodelist, do_convert=True):
+ """Recursively extract bare text inside the specified node (root),
+ concatenate all extracted text and return the result.
+ """
+ rc = []
+ for node in nodelist:
+ if node.nodeType == node.TEXT_NODE:
+ if do_convert:
+ rc.append(cpp_to_python(node.data))
+ else:
+ rc.append(node.data)
+ elif node.nodeType == node.ELEMENT_NODE:
+ rc.append(get_text(node))
+ # return the result, removing any leading newlines (that often happens for
+ # brief descriptions, which will cause lines not well aligned)
+ return re.sub("^(\n*)", "", ''.join(rc))
+
+def parse_parameters(nodelist):
+ rc = []
+ for node in nodelist:
+ if node.nodeName != "parameteritem":
+ continue
+ # for simplicity, we assume one parametername and one
+ # parameterdescription for each parameter.
+ name = get_text(node.getElementsByTagName("parametername")[0])
+ desc = get_text(node.getElementsByTagName("parameterdescription")[0])
+ rc.append(NamedItem(name, desc))
+ return rc
+
+def parse_function_description(func_def, nodelist):
+ for node in nodelist:
+ # nodelist contains beginning and ending empty text nodes.
+ # ignore them (otherwise they cause disruption below).
+ if node.nodeName != "para":
+ continue
+
+ if node.getElementsByTagName("parameterlist"):
+ # within this node there may be exception list, parameter list,
+ # and description for return value. parse and store them
+ # seprately.
+ for paramlist in node.getElementsByTagName("parameterlist"):
+ if paramlist.getAttribute("kind") == "exception":
+ func_def.exceptions = \
+ parse_parameters(paramlist.childNodes)
+ elif paramlist.getAttribute("kind") == "param":
+ func_def.parameters = \
+ parse_parameters(paramlist.childNodes)
+ if node.getElementsByTagName("simplesect"):
+ simplesect = node.getElementsByTagName("simplesect")[0]
+ if simplesect.getAttribute("kind") == "return":
+ func_def.returns = get_text(simplesect)
+ else:
+ # for normal text, python listing and itemized list, append them
+ # to the list of paragraphs
+ func_def.detailed_desc.append(Paragraph(node))
+
+def parse_function(func_def, class_name, nodelist):
+ for node in nodelist:
+ if node.nodeName == "name":
+ func_def.name = get_text(node, False)
+ func_def.pyname = cpp_to_python(func_def.name)
+ elif node.nodeName == "argsstring":
+ # extract parameter names only, assuming they immediately follow
+ # their type name + space, and are immeidatelly followed by
+ # either "," or ")". If it's a pointer or reference, */& is
+ # prepended to the parameter name without a space:
+ # e.g. (int var1, char *var2, Foo &var3)
+ args = get_text(node, False)
+ # extract parameter names, possibly with */&
+ func_def.args = ', '.join(re.findall(r"\s(\S+)[,)]", args))
+ # then remove any */& symbols
+ func_def.args = re.sub("[\*&]", "", func_def.args)
+ elif node.nodeName == "type" and node.hasChildNodes():
+ func_def.ret_type = convert_type_name(get_text(node, False))
+ elif node.nodeName == "param":
+ func_def.have_param = True
+ elif node.nodeName == "briefdescription":
+ func_def.brief_desc = get_text(node)
+ elif node.nodeName == "detaileddescription":
+ parse_function_description(func_def, node.childNodes)
+ # identify the type of function using the name and arg
+ if func_def.name == class_name and \
+ re.search("^\(const " + class_name + " &[^,]*$", args):
+ # This function is ClassName(const ClassName& param), which is
+ # the copy constructor.
+ func_def.type = func_def.COPY_CONSTRUCTOR
+ elif func_def.name == class_name:
+ # if it's not the copy ctor but the function name == class name,
+ # it's a constructor.
+ func_def.type = func_def.CONSTRUCTOR
+ elif func_def.name == "~" + class_name:
+ func_def.type = func_def.DESTRUCTOR
+ elif func_def.name == "operator=":
+ func_def.type = func_def.ASSIGNMENT_OP
+
+ # register the definition to the approriate list
+ if func_def.type == func_def.CONSTRUCTOR:
+ constructors.append(func_def)
+ elif func_def.type == func_def.OTHER:
+ member_functions.append(func_def)
+
+def parse_functions(class_name, nodelist):
+ for node in nodelist:
+ if node.nodeName == "memberdef" and \
+ node.getAttribute("kind") == "function":
+ func_def = FunctionDefinition()
+ parse_function(func_def, class_name, node.childNodes)
+
+def parse_class_variables(class_name, nodelist):
+ for node in nodelist:
+ if node.nodeName == "memberdef" and \
+ node.getAttribute("kind") == "variable":
+ class_variables.append(VariableDefinition(node.childNodes))
+
+def dump(f, class_name, class_brief_doc, class_detailed_doc):
+ f.write("namespace {\n")
+
+ f.write('#ifdef COPY_THIS_TO_MAIN_CC\n')
+ for func in member_functions:
+ func.dump_pymethod_def(f, class_name)
+ f.write('#endif // COPY_THIS_TO_MAIN_CC\n\n')
+
+ f.write("const char* const " + class_name + '_doc = "\\\n')
+ if class_brief_doc is not None:
+ f.write("".join(re.sub("\n", r"\\n\\\n", fill(class_brief_doc))))
+ f.write("\\n\\\n")
+ f.write("\\n\\\n")
+ if len(class_detailed_doc) > 0:
+ for para in class_detailed_doc:
+ para.dump(f, wrapper=TextWrapper())
+
+ # dump constructors
+ for func in constructors:
+ indent = " " * 4
+ func.dump_doc(f, wrapper=TextWrapper(initial_indent=indent,
+ subsequent_indent=indent))
+
+ # dump class variables
+ if len(class_variables) > 0:
+ f.write("Class constant data:\\n\\\n")
+ for var in class_variables:
+ var.dump_doc(f)
+
+ f.write("\";\n")
+
+ for func in member_functions:
+ f.write("\n")
+ f.write("const char* const " + class_name + "_" + func.name + \
+ "_doc = \"\\\n");
+ func.dump_doc(f)
+ f.write("\";\n")
+
+ f.write("} // unnamed namespace") # close namespace
+
+if __name__ == '__main__':
+ dom = parse(sys.argv[1])
+ class_elements = dom.getElementsByTagName("compounddef")[0].childNodes
+ class_brief_doc = None
+ class_detailed_doc = []
+ for node in class_elements:
+ if node.nodeName == "compoundname":
+ # class name is the last portion of the period-separated fully
+ # qualified class name. (this should exist)
+ class_name = re.split("\.", get_text(node))[-1]
+ if node.nodeName == "briefdescription":
+ # we assume a brief description consists at most one para
+ class_brief_doc = get_text(node)
+ elif node.nodeName == "detaileddescription":
+ # a detaild description consists of one or more paragraphs
+ for para in node.childNodes:
+ if para.nodeName != "para": # ignore surrounding empty nodes
+ continue
+ class_detailed_doc.append(Paragraph(para))
+ elif node.nodeName == "sectiondef" and \
+ node.getAttribute("kind") == "public-func":
+ parse_functions(class_name, node.childNodes)
+ elif node.nodeName == "sectiondef" and \
+ node.getAttribute("kind") == "public-static-attrib":
+ parse_class_variables(class_name, node.childNodes)
+ elif node.nodeName == "sectiondef" and \
+ node.getAttribute("kind") == "user-defined":
+ # there are two possiblities: functions and variables
+ for child in node.childNodes:
+ if child.nodeName != "memberdef":
+ continue
+ if child.getAttribute("kind") == "function":
+ parse_function(FunctionDefinition(), class_name,
+ child.childNodes)
+ elif child.getAttribute("kind") == "variable":
+ class_variables.append(VariableDefinition(child.childNodes))
+
+ dump(sys.stdout, class_name, class_brief_doc, class_detailed_doc)
+
+ if len(camel_replacements) > 0:
+ sys.stderr.write("Replaced camelCased terms:\n")
+ for oldterm in camel_replacements.keys():
+ sys.stderr.write("%s => %s\n" % (oldterm,
+ camel_replacements[oldterm]))
diff --git a/src/lib/util/random/qid_gen.h b/src/lib/util/random/qid_gen.h
index 80f532f..fd7ce36 100644
--- a/src/lib/util/random/qid_gen.h
+++ b/src/lib/util/random/qid_gen.h
@@ -25,6 +25,8 @@
#include <boost/random/uniform_int.hpp>
#include <boost/random/variate_generator.hpp>
+#include <stdint.h>
+
namespace isc {
namespace util {
namespace random {
diff --git a/src/lib/util/random/random_number_generator.h b/src/lib/util/random/random_number_generator.h
index f0c0fb3..c765835 100644
--- a/src/lib/util/random/random_number_generator.h
+++ b/src/lib/util/random/random_number_generator.h
@@ -18,6 +18,7 @@
#include <algorithm>
#include <cmath>
#include <numeric>
+#include <vector>
#include <exceptions/exceptions.h>
diff --git a/src/lib/util/range_utilities.h b/src/lib/util/range_utilities.h
index 4d5b0bb..4907b30 100644
--- a/src/lib/util/range_utilities.h
+++ b/src/lib/util/range_utilities.h
@@ -17,6 +17,7 @@
#include <stdlib.h>
#include <algorithm>
+#include <functional>
// This header contains useful methods for conduction operations on
// a range of container elements. Currently the collection is limited,
diff --git a/src/lib/util/tests/Makefile.am b/src/lib/util/tests/Makefile.am
index 3ee16f9..d8f3d30 100644
--- a/src/lib/util/tests/Makefile.am
+++ b/src/lib/util/tests/Makefile.am
@@ -31,8 +31,6 @@ run_unittests_SOURCES += filename_unittest.cc
run_unittests_SOURCES += hex_unittest.cc
run_unittests_SOURCES += io_utilities_unittest.cc
run_unittests_SOURCES += lru_list_unittest.cc
-run_unittests_SOURCES += interprocess_sync_file_unittest.cc
-run_unittests_SOURCES += interprocess_sync_null_unittest.cc
run_unittests_SOURCES += memory_segment_local_unittest.cc
if USE_SHARED_MEMORY
run_unittests_SOURCES += memory_segment_mapped_unittest.cc
@@ -46,15 +44,13 @@ run_unittests_SOURCES += socketsession_unittest.cc
run_unittests_SOURCES += strutil_unittest.cc
run_unittests_SOURCES += time_utilities_unittest.cc
run_unittests_SOURCES += range_utilities_unittest.cc
-run_unittests_SOURCES += interprocess_util.h interprocess_util.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
run_unittests_LDADD = $(top_builddir)/src/lib/util/libb10-util.la
run_unittests_LDADD += $(top_builddir)/src/lib/util/io/libb10-util-io.la
-run_unittests_LDADD += \
- $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
run_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libb10-exceptions.la
run_unittests_LDADD += $(GTEST_LDADD)
endif
diff --git a/src/lib/util/tests/buffer_unittest.cc b/src/lib/util/tests/buffer_unittest.cc
index 02ca83d..76b884c 100644
--- a/src/lib/util/tests/buffer_unittest.cc
+++ b/src/lib/util/tests/buffer_unittest.cc
@@ -248,6 +248,11 @@ TEST_F(BufferTest, outputBufferAssign) {
});
}
+// Check assign to self doesn't break stuff
+TEST_F(BufferTest, outputBufferAssignSelf) {
+ EXPECT_NO_THROW(obuffer = obuffer);
+}
+
TEST_F(BufferTest, outputBufferZeroSize) {
// Some OSes might return NULL on malloc for 0 size, so check it works
EXPECT_NO_THROW({
diff --git a/src/lib/util/tests/interprocess_sync_file_unittest.cc b/src/lib/util/tests/interprocess_sync_file_unittest.cc
deleted file mode 100644
index 38d9026..0000000
--- a/src/lib/util/tests/interprocess_sync_file_unittest.cc
+++ /dev/null
@@ -1,155 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include <util/interprocess_sync_file.h>
-
-#include <util/unittests/check_valgrind.h>
-#include <util/tests/interprocess_util.h>
-#include <gtest/gtest.h>
-#include <unistd.h>
-
-using namespace std;
-using isc::util::test::parentReadState;
-
-namespace isc {
-namespace util {
-
-namespace {
-TEST(InterprocessSyncFileTest, TestLock) {
- InterprocessSyncFile sync("test");
- InterprocessSyncLocker locker(sync);
-
- EXPECT_FALSE(locker.isLocked());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.isLocked());
-
- if (!isc::util::unittests::runningOnValgrind()) {
-
- int fds[2];
-
- // Here, we check that a lock has been taken by forking and
- // checking from the child that a lock exists. This has to be
- // done from a separate process as we test by trying to lock the
- // range again on the lock file. The lock attempt would pass if
- // done from the same process for the granted range. The lock
- // attempt must fail to pass our check.
-
- EXPECT_EQ(0, pipe(fds));
-
- if (fork() == 0) {
- unsigned char locked = 0;
- // Child writes to pipe
- close(fds[0]);
-
- InterprocessSyncFile sync2("test");
- InterprocessSyncLocker locker2(sync2);
-
- if (!locker2.tryLock()) {
- EXPECT_FALSE(locker2.isLocked());
- locked = 1;
- } else {
- EXPECT_TRUE(locker2.isLocked());
- }
-
- ssize_t bytes_written = write(fds[1], &locked, sizeof(locked));
- EXPECT_EQ(sizeof(locked), bytes_written);
-
- close(fds[1]);
- exit(0);
- } else {
- // Parent reads from pipe
- close(fds[1]);
-
- const unsigned char locked = parentReadState(fds[0]);
-
- close(fds[0]);
-
- EXPECT_EQ(1, locked);
- }
- }
-
- EXPECT_TRUE(locker.unlock());
- EXPECT_FALSE(locker.isLocked());
-
- EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test_lockfile"));
-}
-
-TEST(InterprocessSyncFileTest, TestMultipleFilesDirect) {
- InterprocessSyncFile sync("test1");
- InterprocessSyncLocker locker(sync);
-
- EXPECT_TRUE(locker.lock());
-
- InterprocessSyncFile sync2("test2");
- InterprocessSyncLocker locker2(sync2);
- EXPECT_TRUE(locker2.lock());
- EXPECT_TRUE(locker2.unlock());
-
- EXPECT_TRUE(locker.unlock());
-
- EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test1_lockfile"));
- EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test2_lockfile"));
-}
-
-TEST(InterprocessSyncFileTest, TestMultipleFilesForked) {
- InterprocessSyncFile sync("test1");
- InterprocessSyncLocker locker(sync);
-
- EXPECT_TRUE(locker.lock());
-
- if (!isc::util::unittests::runningOnValgrind()) {
-
- int fds[2];
-
- EXPECT_EQ(0, pipe(fds));
-
- if (fork() == 0) {
- unsigned char locked = 0xff;
- // Child writes to pipe
- close(fds[0]);
-
- InterprocessSyncFile sync2("test2");
- InterprocessSyncLocker locker2(sync2);
-
- if (locker2.tryLock()) {
- locked = 0;
- }
-
- ssize_t bytes_written = write(fds[1], &locked, sizeof(locked));
- EXPECT_EQ(sizeof(locked), bytes_written);
-
- close(fds[1]);
- exit(0);
- } else {
- // Parent reads from pipe
- close(fds[1]);
-
- const unsigned char locked = parentReadState(fds[0]);
-
- close(fds[0]);
-
- EXPECT_EQ(0, locked);
- }
-
- EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test2_lockfile"));
- }
-
- EXPECT_TRUE(locker.unlock());
-
- EXPECT_EQ (0, unlink(TEST_DATA_TOPBUILDDIR "/test1_lockfile"));
-}
-
-} // anonymous namespace
-} // namespace util
-} // namespace isc
diff --git a/src/lib/util/tests/interprocess_sync_null_unittest.cc b/src/lib/util/tests/interprocess_sync_null_unittest.cc
deleted file mode 100644
index 70e2b07..0000000
--- a/src/lib/util/tests/interprocess_sync_null_unittest.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include "util/interprocess_sync_null.h"
-#include <gtest/gtest.h>
-
-using namespace std;
-
-namespace isc {
-namespace util {
-
-TEST(InterprocessSyncNullTest, TestNull) {
- InterprocessSyncNull sync("test1");
- InterprocessSyncLocker locker(sync);
-
- // Check if the is_locked_ flag is set correctly during lock().
- EXPECT_FALSE(locker.isLocked());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.isLocked());
-
- // lock() must always return true (this is called 4 times, just an
- // arbitrary number)
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.lock());
-
- // Check if the is_locked_ flag is set correctly during unlock().
- EXPECT_TRUE(locker.isLocked());
- EXPECT_TRUE(locker.unlock());
- EXPECT_FALSE(locker.isLocked());
-
- // unlock() must always return true (this is called 4 times, just an
- // arbitrary number)
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.unlock());
-
- // Check if the is_locked_ flag is set correctly during tryLock().
- EXPECT_FALSE(locker.isLocked());
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.isLocked());
-
- // tryLock() must always return true (this is called 4 times, just an
- // arbitrary number)
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.tryLock());
-
- // Random order (should all return true)
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.lock());
- EXPECT_TRUE(locker.tryLock());
- EXPECT_TRUE(locker.unlock());
- EXPECT_TRUE(locker.unlock());
-}
-
-} // namespace util
-} // namespace isc
diff --git a/src/lib/util/tests/interprocess_util.cc b/src/lib/util/tests/interprocess_util.cc
deleted file mode 100644
index dfb04b7..0000000
--- a/src/lib/util/tests/interprocess_util.cc
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include <gtest/gtest.h>
-
-#include <sys/select.h>
-#include <cstddef>
-
-namespace isc {
-namespace util {
-namespace test {
-
-unsigned char
-parentReadState(int fd) {
- unsigned char result = 0xff;
-
- fd_set rfds;
- FD_ZERO(&rfds);
- FD_SET(fd, &rfds);
-
- struct timeval tv = {5, 0};
-
- const int nfds = select(fd + 1, &rfds, NULL, NULL, &tv);
- EXPECT_EQ(1, nfds);
-
- if (nfds == 1) {
- // Read status
- const ssize_t bytes_read = read(fd, &result, sizeof(result));
- EXPECT_EQ(sizeof(result), bytes_read);
- }
-
- return (result);
-}
-
-}
-}
-}
diff --git a/src/lib/util/tests/interprocess_util.h b/src/lib/util/tests/interprocess_util.h
deleted file mode 100644
index 286f9cf..0000000
--- a/src/lib/util/tests/interprocess_util.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-namespace isc {
-namespace util {
-namespace test {
-/// \brief A helper utility for a simple synchronization with another process.
-///
-/// It waits for incoming data on a given file descriptor up to 5 seconds
-/// (arbitrary choice), read one byte data, and return it to the caller.
-/// On any failure it returns 0xff (255), so the sender process should use
-/// a different value to pass.
-unsigned char parentReadState(int fd);
-}
-}
-}
-
-// Local Variables:
-// mode: c++
-// End:
diff --git a/src/lib/util/tests/memory_segment_common_unittest.cc b/src/lib/util/tests/memory_segment_common_unittest.cc
index 3810e0a..17719ae 100644
--- a/src/lib/util/tests/memory_segment_common_unittest.cc
+++ b/src/lib/util/tests/memory_segment_common_unittest.cc
@@ -30,9 +30,8 @@ checkSegmentNamedAddress(MemorySegment& segment, bool out_of_segment_ok) {
// NULL name is not allowed.
EXPECT_THROW(segment.getNamedAddress(NULL), InvalidParameter);
- // If the name does not exist, NULL should be returned.
- EXPECT_EQ(static_cast<void*>(NULL),
- segment.getNamedAddress("test address"));
+ // If the name does not exist, false should be returned.
+ EXPECT_FALSE(segment.getNamedAddress("test address").first);
// Now set it
void* ptr32 = segment.allocate(sizeof(uint32_t));
@@ -42,31 +41,46 @@ checkSegmentNamedAddress(MemorySegment& segment, bool out_of_segment_ok) {
// NULL name isn't allowed.
EXPECT_THROW(segment.setNamedAddress(NULL, ptr32), InvalidParameter);
+ EXPECT_THROW(segment.getNamedAddress(NULL), InvalidParameter);
+ EXPECT_THROW(segment.clearNamedAddress(NULL), InvalidParameter);
+
+ // Empty names are not allowed.
+ EXPECT_THROW(segment.setNamedAddress("", ptr32), InvalidParameter);
+ EXPECT_THROW(segment.getNamedAddress(""), InvalidParameter);
+ EXPECT_THROW(segment.clearNamedAddress(""), InvalidParameter);
+
+ // Names beginning with _ are not allowed.
+ EXPECT_THROW(segment.setNamedAddress("_foo", ptr32), InvalidParameter);
+ EXPECT_THROW(segment.getNamedAddress("_foo"), InvalidParameter);
+ EXPECT_THROW(segment.clearNamedAddress("_foo"), InvalidParameter);
// we can now get it; the stored value should be intact.
- EXPECT_EQ(ptr32, segment.getNamedAddress("test address"));
- EXPECT_EQ(test_val, *static_cast<const uint32_t*>(ptr32));
+ MemorySegment::NamedAddressResult result =
+ segment.getNamedAddress("test address");
+ EXPECT_TRUE(result.first);
+ EXPECT_EQ(test_val, *static_cast<const uint32_t*>(result.second));
// Override it.
void* ptr16 = segment.allocate(sizeof(uint16_t));
const uint16_t test_val16 = 4200;
*static_cast<uint16_t*>(ptr16) = test_val16;
EXPECT_FALSE(segment.setNamedAddress("test address", ptr16));
- EXPECT_EQ(ptr16, segment.getNamedAddress("test address"));
- EXPECT_EQ(test_val16, *static_cast<const uint16_t*>(ptr16));
+ result = segment.getNamedAddress("test address");
+ EXPECT_TRUE(result.first);
+ EXPECT_EQ(test_val16, *static_cast<const uint16_t*>(result.second));
// Clear it. Then we won't be able to find it any more.
EXPECT_TRUE(segment.clearNamedAddress("test address"));
- EXPECT_EQ(static_cast<void*>(NULL),
- segment.getNamedAddress("test address"));
+ EXPECT_FALSE(segment.getNamedAddress("test address").first);
// duplicate attempt of clear will result in false as it doesn't exist.
EXPECT_FALSE(segment.clearNamedAddress("test address"));
// Setting NULL is okay.
EXPECT_FALSE(segment.setNamedAddress("null address", NULL));
- EXPECT_EQ(static_cast<void*>(NULL),
- segment.getNamedAddress("null address"));
+ result = segment.getNamedAddress("null address");
+ EXPECT_TRUE(result.first);
+ EXPECT_FALSE(result.second);
// If the underlying implementation performs explicit check against
// out-of-segment address, confirm the behavior.
diff --git a/src/lib/util/tests/memory_segment_mapped_unittest.cc b/src/lib/util/tests/memory_segment_mapped_unittest.cc
index 1d9979d..8ae6fea 100644
--- a/src/lib/util/tests/memory_segment_mapped_unittest.cc
+++ b/src/lib/util/tests/memory_segment_mapped_unittest.cc
@@ -14,7 +14,7 @@
#include <util/tests/memory_segment_common_unittest.h>
#include <util/unittests/check_valgrind.h>
-#include <util/tests/interprocess_util.h>
+#include <util/unittests/interprocess_util.h>
#include <util/memory_segment_mapped.h>
#include <exceptions/exceptions.h>
@@ -42,7 +42,7 @@
using namespace isc::util;
using boost::scoped_ptr;
-using isc::util::test::parentReadState;
+using isc::util::unittests::parentReadState;
namespace {
// Shortcut to keep code shorter
@@ -237,12 +237,24 @@ TEST_F(MemorySegmentMappedTest, allocate) {
}
TEST_F(MemorySegmentMappedTest, badAllocate) {
+ // If the test is run as the root user, the following allocate()
+ // call will result in a successful MemorySegmentGrown exception,
+ // instead of an abort (due to insufficient permissions during
+ // reopen).
+ if (getuid() == 0) {
+ std::cerr << "Skipping test as it's run as the root user" << std::endl;
+ return;
+ }
+
// Make the mapped file non-writable; managed_mapped_file::grow() will
- // fail, resulting in std::bad_alloc
+ // fail, resulting in abort.
const int ret = chmod(mapped_file, 0444);
ASSERT_EQ(0, ret);
- EXPECT_THROW(segment_->allocate(DEFAULT_INITIAL_SIZE * 2), std::bad_alloc);
+ if (!isc::util::unittests::runningOnValgrind()) {
+ EXPECT_DEATH_IF_SUPPORTED(
+ {segment_->allocate(DEFAULT_INITIAL_SIZE * 2);}, "");
+ }
}
// XXX: this test can cause too strong side effect (creating a very large
@@ -287,12 +299,14 @@ void
checkNamedData(const std::string& name, const std::vector<uint8_t>& data,
MemorySegment& sgmt, bool delete_after_check = false)
{
- void* dp = sgmt.getNamedAddress(name.c_str());
- ASSERT_TRUE(dp);
- EXPECT_EQ(0, std::memcmp(dp, &data[0], data.size()));
+ const MemorySegment::NamedAddressResult result =
+ sgmt.getNamedAddress(name.c_str());
+ ASSERT_TRUE(result.first);
+ ASSERT_TRUE(result.second);
+ EXPECT_EQ(0, std::memcmp(result.second, &data[0], data.size()));
if (delete_after_check) {
- sgmt.deallocate(dp, data.size());
+ sgmt.deallocate(result.second, data.size());
sgmt.clearNamedAddress(name.c_str());
}
}
@@ -309,10 +323,10 @@ TEST_F(MemorySegmentMappedTest, namedAddress) {
segment_.reset(); // close it before opening another one
segment_.reset(new MemorySegmentMapped(mapped_file));
- EXPECT_NE(static_cast<void*>(NULL),
- segment_->getNamedAddress("test address"));
- EXPECT_EQ(test_val16, *static_cast<const uint16_t*>(
- segment_->getNamedAddress("test address")));
+ MemorySegment::NamedAddressResult result =
+ segment_->getNamedAddress("test address");
+ ASSERT_TRUE(result.first);
+ EXPECT_EQ(test_val16, *static_cast<const uint16_t*>(result.second));
// try to set an unusually long name. We re-create the file so
// creating the name would cause allocation failure and trigger internal
@@ -323,8 +337,9 @@ TEST_F(MemorySegmentMappedTest, namedAddress) {
const std::string long_name(1025, 'x'); // definitely larger than segment
// setNamedAddress should return true, indicating segment has grown.
EXPECT_TRUE(segment_->setNamedAddress(long_name.c_str(), NULL));
- EXPECT_EQ(static_cast<void*>(NULL),
- segment_->getNamedAddress(long_name.c_str()));
+ result = segment_->getNamedAddress(long_name.c_str());
+ EXPECT_TRUE(result.first);
+ EXPECT_FALSE(result.second);
// Check contents pointed by named addresses survive growing and
// shrinking segment.
@@ -410,10 +425,12 @@ TEST_F(MemorySegmentMappedTest, multiProcess) {
EXPECT_EQ(0, from_parent);
MemorySegmentMapped sgmt(mapped_file);
- void* ptr_child = sgmt.getNamedAddress("test address");
- EXPECT_TRUE(ptr_child);
- if (ptr_child) {
- const uint32_t val = *static_cast<const uint32_t*>(ptr_child);
+ const MemorySegment::NamedAddressResult result =
+ sgmt.getNamedAddress("test address");
+ ASSERT_TRUE(result.first);
+ EXPECT_TRUE(result.second);
+ if (result.second) {
+ const uint32_t val = *static_cast<const uint32_t*>(result.second);
EXPECT_EQ(424242, val);
// tell the parent whether it succeeded. 0 means it did,
// 0xff means it failed.
@@ -425,9 +442,11 @@ TEST_F(MemorySegmentMappedTest, multiProcess) {
// parent: open another read-only segment, then tell the child to open
// its own segment.
segment_.reset(new MemorySegmentMapped(mapped_file));
- ptr = segment_->getNamedAddress("test address");
- ASSERT_TRUE(ptr);
- EXPECT_EQ(424242, *static_cast<const uint32_t*>(ptr));
+ const MemorySegment::NamedAddressResult result =
+ segment_->getNamedAddress("test address");
+ ASSERT_TRUE(result.first);
+ ASSERT_TRUE(result.second);
+ EXPECT_EQ(424242, *static_cast<const uint32_t*>(result.second));
const char some_data = 0;
EXPECT_EQ(1, write(pipe_to_child.getWriteFD(), &some_data,
sizeof(some_data)));
@@ -460,7 +479,14 @@ TEST_F(MemorySegmentMappedTest, shrink) {
EXPECT_EQ(shrinked_size, segment_->getSize());
// Check that the segment is still usable after shrink.
- void* p = segment_->allocate(sizeof(uint32_t));
+ void *p = NULL;
+ while (!p) {
+ try {
+ p = segment_->allocate(sizeof(uint32_t));
+ } catch (const MemorySegmentGrown&) {
+ // Do nothing. Just try again.
+ }
+ }
segment_->deallocate(p, sizeof(uint32_t));
}
@@ -477,9 +503,11 @@ TEST_F(MemorySegmentMappedTest, violateReadOnly) {
if (!isc::util::unittests::runningOnValgrind()) {
EXPECT_DEATH_IF_SUPPORTED({
MemorySegmentMapped segment_ro(mapped_file);
- EXPECT_TRUE(segment_ro.getNamedAddress("test address"));
- *static_cast<uint32_t*>(
- segment_ro.getNamedAddress("test address")) = 0;
+ const MemorySegment::NamedAddressResult result =
+ segment_ro.getNamedAddress("test address");
+ ASSERT_TRUE(result.first);
+ ASSERT_TRUE(result.second);
+ *static_cast<uint32_t*>(result.second) = 0;
}, "");
}
@@ -487,10 +515,12 @@ TEST_F(MemorySegmentMappedTest, violateReadOnly) {
// attempts are prohibited. When detectable it must result in an
// exception.
MemorySegmentMapped segment_ro(mapped_file);
- ptr = segment_ro.getNamedAddress("test address");
- EXPECT_NE(static_cast<void*>(NULL), ptr);
+ const MemorySegment::NamedAddressResult result =
+ segment_ro.getNamedAddress("test address");
+ ASSERT_TRUE(result.first);
+ EXPECT_NE(static_cast<void*>(NULL), result.second);
- EXPECT_THROW(segment_ro.deallocate(ptr, 4), MemorySegmentError);
+ EXPECT_THROW(segment_ro.deallocate(result.second, 4), MemorySegmentError);
EXPECT_THROW(segment_ro.allocate(16), MemorySegmentError);
// allocation that would otherwise require growing the segment; permission
diff --git a/src/lib/util/tests/random_number_generator_unittest.cc b/src/lib/util/tests/random_number_generator_unittest.cc
index 23d5b88..639d354 100644
--- a/src/lib/util/tests/random_number_generator_unittest.cc
+++ b/src/lib/util/tests/random_number_generator_unittest.cc
@@ -14,14 +14,13 @@
#include <config.h>
+#include <util/random/random_number_generator.h>
+
#include <gtest/gtest.h>
#include <boost/shared_ptr.hpp>
-#include <algorithm>
#include <iostream>
-#include <vector>
-#include <util/random/random_number_generator.h>
namespace isc {
namespace util {
@@ -42,9 +41,9 @@ public:
}
virtual ~UniformRandomIntegerGeneratorTest(){}
- int gen() { return gen_(); }
- int max() const { return max_; }
- int min() const { return min_; }
+ int gen() { return (gen_()); }
+ int max() const { return (max_); }
+ int min() const { return (min_); }
private:
UniformRandomIntegerGenerator gen_;
@@ -82,7 +81,7 @@ TEST_F(UniformRandomIntegerGeneratorTest, IntegerRange) {
vector<int>::iterator it = unique(numbers.begin(), numbers.end());
// make sure the numbers are in range [min, max]
- ASSERT_EQ(it - numbers.begin(), max() - min() + 1);
+ ASSERT_EQ(it - numbers.begin(), max() - min() + 1);
}
/// \brief Test Fixture Class for weighted random number generator
@@ -99,7 +98,8 @@ public:
TEST_F(WeightedRandomIntegerGeneratorTest, Constructor) {
vector<double> probabilities;
- // If no probabilities is provided, the smallest integer will always be generated
+ // If no probabilities is provided, the smallest integer will always
+ // be generated
WeightedRandomIntegerGenerator gen(probabilities, 123);
for (int i = 0; i < 100; ++i) {
ASSERT_EQ(gen(), 123);
diff --git a/src/lib/util/tests/run_unittests.cc b/src/lib/util/tests/run_unittests.cc
index 8789a9c..41761ca 100644
--- a/src/lib/util/tests/run_unittests.cc
+++ b/src/lib/util/tests/run_unittests.cc
@@ -20,6 +20,5 @@ int
main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
- setenv("B10_LOCKFILE_DIR_FROM_BUILD", TEST_DATA_TOPBUILDDIR, 1);
return (isc::util::unittests::run_all());
}
diff --git a/src/lib/util/threads/tests/Makefile.am b/src/lib/util/threads/tests/Makefile.am
index a12d221..80c6ece 100644
--- a/src/lib/util/threads/tests/Makefile.am
+++ b/src/lib/util/threads/tests/Makefile.am
@@ -29,8 +29,7 @@ run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) $(PTHREAD_LDFLAGS)
run_unittests_LDADD = $(top_builddir)/src/lib/util/threads/libb10-threads.la
-run_unittests_LDADD += \
- $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
+run_unittests_LDADD += $(top_builddir)/src/lib/util/unittests/libutil_unittests.la
run_unittests_LDADD += $(GTEST_LDADD)
endif
diff --git a/src/lib/util/threads/tests/condvar_unittest.cc b/src/lib/util/threads/tests/condvar_unittest.cc
index 3bb7230..f804391 100644
--- a/src/lib/util/threads/tests/condvar_unittest.cc
+++ b/src/lib/util/threads/tests/condvar_unittest.cc
@@ -142,8 +142,13 @@ signalAndWait(CondVar* condvar, Mutex* mutex) {
condvar->wait(*mutex);
}
-#ifndef HAS_UNDEFINED_PTHREAD_BEHAVIOR
-TEST_F(CondVarTest, destroyWhileWait) {
+TEST_F(CondVarTest,
+#ifdef HAS_UNDEFINED_PTHREAD_BEHAVIOR
+ DISABLED_destroyWhileWait
+#else
+ destroyWhileWait
+#endif
+) {
// We'll destroy a CondVar object while the thread is still waiting
// on it. This will trigger an assertion failure.
if (!isc::util::unittests::runningOnValgrind()) {
@@ -155,7 +160,6 @@ TEST_F(CondVarTest, destroyWhileWait) {
}, "");
}
}
-#endif // !HAS_UNDEFINED_PTHREAD_BEHAVIOR
#ifdef ENABLE_DEBUG
diff --git a/src/lib/util/threads/tests/lock_unittest.cc b/src/lib/util/threads/tests/lock_unittest.cc
index c17999e..e72b92d 100644
--- a/src/lib/util/threads/tests/lock_unittest.cc
+++ b/src/lib/util/threads/tests/lock_unittest.cc
@@ -86,9 +86,14 @@ TEST(MutexTest, lockNonBlocking) {
#endif // ENABLE_DEBUG
-#ifndef HAS_UNDEFINED_PTHREAD_BEHAVIOR
// Destroying a locked mutex is a bad idea as well
-TEST(MutexTest, destroyLocked) {
+TEST(MutexTest,
+#ifdef HAS_UNDEFINED_PTHREAD_BEHAVIOR
+ DISABLED_destroyLocked
+#else
+ destroyLocked
+#endif
+) {
if (!isc::util::unittests::runningOnValgrind()) {
EXPECT_DEATH_IF_SUPPORTED({
Mutex* mutex = new Mutex;
@@ -99,7 +104,6 @@ TEST(MutexTest, destroyLocked) {
}, "");
}
}
-#endif // !HAS_UNDEFINED_PTHREAD_BEHAVIOR
// In this test, we try to check if a mutex really locks. We could try that
// with a deadlock, but that's not practical (the test would not end).
diff --git a/src/lib/util/unittests/Makefile.am b/src/lib/util/unittests/Makefile.am
index 55e0372..657c2aa 100644
--- a/src/lib/util/unittests/Makefile.am
+++ b/src/lib/util/unittests/Makefile.am
@@ -11,6 +11,7 @@ libutil_unittests_la_SOURCES += check_valgrind.h check_valgrind.cc
libutil_unittests_la_SOURCES += run_all.h run_all.cc
libutil_unittests_la_SOURCES += textdata.h
libutil_unittests_la_SOURCES += wiredata.h wiredata.cc
+libutil_unittests_la_SOURCES += interprocess_util.h interprocess_util.cc
endif
# For now, this isn't needed for libutil_unittests
diff --git a/src/lib/util/unittests/interprocess_util.cc b/src/lib/util/unittests/interprocess_util.cc
new file mode 100644
index 0000000..ce858d4
--- /dev/null
+++ b/src/lib/util/unittests/interprocess_util.cc
@@ -0,0 +1,48 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <gtest/gtest.h>
+
+#include <sys/select.h>
+#include <cstddef>
+
+namespace isc {
+namespace util {
+namespace unittests {
+
+unsigned char
+parentReadState(int fd) {
+ unsigned char result = 0xff;
+
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(fd, &rfds);
+
+ struct timeval tv = {5, 0};
+
+ const int nfds = select(fd + 1, &rfds, NULL, NULL, &tv);
+ EXPECT_EQ(1, nfds);
+
+ if (nfds == 1) {
+ // Read status
+ const ssize_t bytes_read = read(fd, &result, sizeof(result));
+ EXPECT_EQ(sizeof(result), bytes_read);
+ }
+
+ return (result);
+}
+
+}
+}
+}
diff --git a/src/lib/util/unittests/interprocess_util.h b/src/lib/util/unittests/interprocess_util.h
new file mode 100644
index 0000000..f25ad3e
--- /dev/null
+++ b/src/lib/util/unittests/interprocess_util.h
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+namespace isc {
+namespace util {
+namespace unittests {
+/// \brief A helper utility for a simple synchronization with another process.
+///
+/// It waits for incoming data on a given file descriptor up to 5 seconds
+/// (arbitrary choice), read one byte data, and return it to the caller.
+/// On any failure it returns 0xff (255), so the sender process should use
+/// a different value to pass.
+unsigned char parentReadState(int fd);
+}
+}
+}
+
+// Local Variables:
+// mode: c++
+// End:
diff --git a/src/lib/util/unittests/mock_socketsession.h b/src/lib/util/unittests/mock_socketsession.h
index 01ca34f..fb155a2 100644
--- a/src/lib/util/unittests/mock_socketsession.h
+++ b/src/lib/util/unittests/mock_socketsession.h
@@ -42,7 +42,12 @@ class MockSocketSessionForwarder :
public:
MockSocketSessionForwarder() :
is_connected_(false), connect_ok_(true), push_ok_(true),
- close_ok_(true)
+ close_ok_(true),
+ // These are not used until set, but we set them anyway here,
+ // partly to silence cppcheck, and partly to be cleaner. Put some
+ // invalid values in.
+ pushed_sock_(-1), pushed_family_(-1), pushed_type_(-1),
+ pushed_protocol_(-1)
{}
virtual void connectToReceiver() {
diff --git a/src/lib/xfr/Makefile.am b/src/lib/xfr/Makefile.am
index 5551a5b..5e90e9b 100644
--- a/src/lib/xfr/Makefile.am
+++ b/src/lib/xfr/Makefile.am
@@ -5,10 +5,8 @@ AM_CPPFLAGS += -I$(top_srcdir)/src/lib/dns -I$(top_builddir)/src/lib/dns
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(B10_CXXFLAGS)
-AM_CXXFLAGS += -Wno-unused-parameter # see src/lib/cc/Makefile.am
-if USE_CLANGPP
-AM_CXXFLAGS += -Wno-error
-endif
+AM_CXXFLAGS += -Wno-unused-parameter
+# see src/lib/cc/Makefile.am
CLEANFILES = *.gcno *.gcda
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 9f1025b..7c6de04 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1 +1 @@
-SUBDIRS = tools
+SUBDIRS = tools lettuce
diff --git a/tests/lettuce/Makefile.am b/tests/lettuce/Makefile.am
new file mode 100644
index 0000000..82f3e3a
--- /dev/null
+++ b/tests/lettuce/Makefile.am
@@ -0,0 +1 @@
+noinst_SCRIPTS = setup_intree_bind10.sh
diff --git a/tests/lettuce/README b/tests/lettuce/README
index a53c6e7..48c4043 100644
--- a/tests/lettuce/README
+++ b/tests/lettuce/README
@@ -6,7 +6,8 @@ these tests are specific for BIND10, but we are keeping in mind that RFC-related
tests could be separated, so that we can test other systems as well.
Prerequisites:
-- BIND 10 must be compiled or installed
+- BIND 10 must be compiled or installed (even when testing in-tree build;
+ see below)
- dig
- lettuce (http://lettuce.it)
@@ -32,7 +33,9 @@ By default it will use the build tree, but you can use an installed version
of bind10 by passing -I as the first argument of run_lettuce.sh
The tool 'dig' must be in the default search path of your environment. If
-you specified -I, so must the main bind10 program, as well as bindctl.
+you specified -I, so must the main BIND 10 programs. And, with or without
+-I, some BIND 10 programs still have to be installed as they are invoked
+from test tools. Those include bindctl and b10-loadzone.
Due to the default way lettuce prints its output, it is advisable to run it
in a terminal that is wide than the default. If you see a lot of lines twice
diff --git a/tests/lettuce/configurations/example.org.inmem.config b/tests/lettuce/configurations/example.org.inmem.config
index 2e9ca41..2eadedb 100644
--- a/tests/lettuce/configurations/example.org.inmem.config
+++ b/tests/lettuce/configurations/example.org.inmem.config
@@ -22,6 +22,12 @@
"params": {
"example.org": "data/example.org"
}
+ },
+ {
+ "type": "broken_libraries_should_be_skipped",
+ "cache-enable": false,
+ "params": {
+ }
}
]
}
diff --git a/tests/lettuce/configurations/ixfr-out/testset1-config.db b/tests/lettuce/configurations/ixfr-out/testset1-config.db
index d5eaf83..38d29a7 100644
--- a/tests/lettuce/configurations/ixfr-out/testset1-config.db
+++ b/tests/lettuce/configurations/ixfr-out/testset1-config.db
@@ -2,7 +2,6 @@
"Xfrin": {
"zones": [
{
- "use_ixfr": true,
"class": "IN",
"name": "example.com.",
"master_addr": "178.18.82.80"
diff --git a/tests/lettuce/configurations/xfrin/retransfer_slave.conf.orig b/tests/lettuce/configurations/xfrin/retransfer_slave.conf.orig
index 2e6b17f..aff8218 100644
--- a/tests/lettuce/configurations/xfrin/retransfer_slave.conf.orig
+++ b/tests/lettuce/configurations/xfrin/retransfer_slave.conf.orig
@@ -8,7 +8,6 @@
} ]
},
"Auth": {
- "database_file": "data/test_nonexistent_db.sqlite3",
"listen_on": [ {
"address": "::1",
"port": 47806
diff --git a/tests/lettuce/configurations/xfrin/retransfer_slave_diffs.conf b/tests/lettuce/configurations/xfrin/retransfer_slave_diffs.conf
index 1bc6324..24bfa2a 100644
--- a/tests/lettuce/configurations/xfrin/retransfer_slave_diffs.conf
+++ b/tests/lettuce/configurations/xfrin/retransfer_slave_diffs.conf
@@ -8,7 +8,6 @@
} ]
},
"Auth": {
- "database_file": "data/xfrin-before-diffs.sqlite3",
"listen_on": [ {
"address": "::1",
"port": 47806
@@ -18,8 +17,7 @@
"zones": [ {
"name": "example",
"master_addr": "::1",
- "master_port": 47807,
- "use_ixfr": true
+ "master_port": 47807
} ]
},
"data_sources": {
diff --git a/tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig b/tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig
index 3040b6c..c4ba1ef 100644
--- a/tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig
+++ b/tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig
@@ -8,7 +8,6 @@
} ]
},
"Auth": {
- "database_file": "data/xfrin-notify.sqlite3",
"listen_on": [ {
"address": "::1",
"port": 47806
@@ -28,7 +27,8 @@
"zones": [ {
"name": "example.org",
"master_addr": "::1",
- "master_port": 47807
+ "master_port": 47807,
+ "request_ixfr": "no"
} ]
},
"Zonemgr": {
diff --git a/tests/lettuce/configurations/xfrin/retransfer_slave_notify_v4.conf b/tests/lettuce/configurations/xfrin/retransfer_slave_notify_v4.conf
index 67ebfd3..b99f3f7 100644
--- a/tests/lettuce/configurations/xfrin/retransfer_slave_notify_v4.conf
+++ b/tests/lettuce/configurations/xfrin/retransfer_slave_notify_v4.conf
@@ -8,7 +8,6 @@
} ]
},
"Auth": {
- "database_file": "data/xfrin-notify.sqlite3",
"listen_on": [ {
"address": "127.0.0.1",
"port": 47806
@@ -28,7 +27,8 @@
"zones": [ {
"name": "example.org",
"master_addr": "127.0.0.1",
- "master_port": 47809
+ "master_port": 47809,
+ "request_ixfr": "no"
} ]
},
"Zonemgr": {
diff --git a/tests/lettuce/configurations/xfrout_master.conf b/tests/lettuce/configurations/xfrout_master.conf
new file mode 100644
index 0000000..755e698
--- /dev/null
+++ b/tests/lettuce/configurations/xfrout_master.conf
@@ -0,0 +1,41 @@
+{
+ "version": 3,
+ "Logging": {
+ "loggers": [ {
+ "debuglevel": 99,
+ "severity": "DEBUG",
+ "name": "*"
+ } ]
+ },
+ "Auth": {
+ "database_file": "data/xfrout.sqlite3",
+ "listen_on": [ {
+ "address": "::1",
+ "port": 47806
+ } ]
+ },
+ "data_sources": {
+ "classes": {
+ "IN": [{
+ "type": "sqlite3",
+ "params": {
+ "database_file": "data/xfrout.sqlite3"
+ }
+ }]
+ }
+ },
+ "Xfrout": {
+ "zone_config": [ {
+ "origin": "example.org"
+ } ]
+ },
+ "Init": {
+ "components": {
+ "b10-auth": { "kind": "needed", "special": "auth" },
+ "b10-xfrout": { "address": "Xfrout", "kind": "dispensable" },
+ "b10-zonemgr": { "address": "Zonemgr", "kind": "dispensable" },
+ "b10-stats": { "address": "Stats", "kind": "dispensable" },
+ "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
+ }
+ }
+}
diff --git a/tests/lettuce/features/.gitignore b/tests/lettuce/features/.gitignore
new file mode 100644
index 0000000..0634dc7
--- /dev/null
+++ b/tests/lettuce/features/.gitignore
@@ -0,0 +1 @@
+/resolver_basic.feature
diff --git a/tests/lettuce/features/auth_badzone.feature b/tests/lettuce/features/auth_badzone.feature
index 5448b6e..8b902b3 100644
--- a/tests/lettuce/features/auth_badzone.feature
+++ b/tests/lettuce/features/auth_badzone.feature
@@ -2,7 +2,7 @@ Feature: Authoritative DNS server with a bad zone
This feature set is for testing the execution of the b10-auth
component when one zone is broken, whereas others are fine. In this
case, b10-auth should not reject the data source, but reject the bad
- zone only and serve the good zones anyway.
+ zone only (with SERVFAIL) and serve the good zones anyway.
Scenario: Bad zone
Given I have bind10 running with configuration auth/auth_badzone.config
@@ -24,7 +24,7 @@ Feature: Authoritative DNS server with a bad zone
And bind10 module Resolver should not be running
A query for www.example.org should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have ancount 1
The last query response should have nscount 2
The last query response should have adcount 2
@@ -44,6 +44,6 @@ Feature: Authoritative DNS server with a bad zone
ns2.example.org. 3600 IN A 192.0.2.4
"""
- A query for www.example.com should have rcode REFUSED
- A query for www.example.net should have rcode REFUSED
- A query for www.example.info should have rcode REFUSED
+ A query for www.example.com should have rcode SERVFAIL
+ A query for www.example.net should have rcode SERVFAIL
+ A query for www.example.info should have rcode SERVFAIL
diff --git a/tests/lettuce/features/ddns_system.feature b/tests/lettuce/features/ddns_system.feature
index 184c8ae..6747b53 100644
--- a/tests/lettuce/features/ddns_system.feature
+++ b/tests/lettuce/features/ddns_system.feature
@@ -118,6 +118,42 @@ Feature: DDNS System
A query for new3.example.org should have rcode NOERROR
The SOA serial for example.org should be 1236
+ Scenario: Zone validation check
+ Given I have bind10 running with configuration ddns/ddns.config
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message AUTH_SERVER_STARTED
+ And wait for bind10 stderr message DDNS_STARTED
+
+ # Sanity check
+ A query for example.org type NS should have rcode NOERROR
+ The answer section of the last query response should be
+ """
+ example.org. 3600 IN NS ns1.example.org.
+ example.org. 3600 IN NS ns2.example.org.
+ example.org. 3600 IN NS ns3.example.org.
+ """
+ The SOA serial for example.org should be 1234
+
+ # Test failed validation. Here, example.org has ns1.example.org
+ # configured as a name server. CNAME records cannot be added for
+ # ns1.example.org.
+ When I use DDNS to add a record ns1.example.org. 3600 IN CNAME ns3.example.org.
+ The DDNS response should be REFUSED
+ A query for ns1.example.org type CNAME should have rcode NXDOMAIN
+ The SOA serial for example.org should be 1234
+
+ # Test passed validation. Here, example.org does not have
+ # ns4.example.org configured as a name server. CNAME records can
+ # be added for ns4.example.org.
+ When I use DDNS to add a record ns4.example.org. 3600 IN CNAME ns3.example.org.
+ The DDNS response should be SUCCESS
+ A query for ns4.example.org type CNAME should have rcode NOERROR
+ The answer section of the last query response should be
+ """
+ ns4.example.org. 3600 IN CNAME ns3.example.org.
+ """
+ The SOA serial for example.org should be 1235
+
#Scenario: DDNS and Xfrout
## Unfortunately, Xfrout can only notify to inzone slaves, and hence only
## to port 53, which we do not want to use for Lettuce tests (for various
diff --git a/tests/lettuce/features/example.feature b/tests/lettuce/features/example.feature
index 86d20d3..48cef20 100644
--- a/tests/lettuce/features/example.feature
+++ b/tests/lettuce/features/example.feature
@@ -120,7 +120,7 @@ Feature: Example feature
The last query response should have adcount 0
# When checking flags, we must pass them exactly as they appear in
# the output of dig.
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
A query for www.example.org type TXT should have rcode NOERROR
The last query response should have ancount 0
@@ -132,6 +132,36 @@ Feature: Example feature
A query for www.example.org to 127.0.0.1:47806 should have rcode NOERROR
A query for www.example.org type A class IN to 127.0.0.1:47806 should have rcode NOERROR
+ Scenario: example.org mixed-case query
+ # This scenario performs a mixed-case query and checks that the
+ # response has the name copied from the question exactly
+ # (without any change in case). For why this is necessary, see
+ # section 5.2 of:
+ # http://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00
+
+ Given I have bind10 running with configuration example.org.config
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message CMDCTL_STARTED
+ And wait for bind10 stderr message AUTH_SERVER_STARTED
+
+ bind10 module Auth should be running
+ And bind10 module Resolver should not be running
+ And bind10 module Xfrout should not be running
+ And bind10 module Zonemgr should not be running
+ And bind10 module Xfrin should not be running
+ And bind10 module Stats should not be running
+ And bind10 module StatsHttpd should not be running
+
+ A query for wWw.eXaMpLe.Org should have rcode NOERROR
+ The last query response should have qdcount 1
+ The last query response should have ancount 1
+ The last query response should have nscount 3
+ The last query response should have adcount 0
+ The question section of the last query response should exactly be
+ """
+ ;wWw.eXaMpLe.Org. IN A
+ """
+
Scenario: changing database
# This scenario contains a lot of 'wait for' steps
# If those are not present, the asynchronous nature of the application
diff --git a/tests/lettuce/features/nsec3_auth.feature b/tests/lettuce/features/nsec3_auth.feature
index 6d3a556..8ead43f 100644
--- a/tests/lettuce/features/nsec3_auth.feature
+++ b/tests/lettuce/features/nsec3_auth.feature
@@ -25,7 +25,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for a.c.x.w.example. should have rcode NXDOMAIN
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 8
@@ -57,7 +57,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for ns1.example. type MX should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 4
@@ -85,7 +85,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for y.w.example. should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 4
@@ -113,7 +113,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for mc.c.example. type MX should have rcode NOERROR
- The last query response should have flags qr rd
+ The last query response should have flags qr
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 6
@@ -148,7 +148,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for a.z.w.example. type MX should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 2
The last query response should have nscount 5
@@ -195,7 +195,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for a.z.w.example. type AAAA should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 8
@@ -227,7 +227,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for example. type DS should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 4
@@ -259,7 +259,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for b.x.w.example. should have rcode NXDOMAIN
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 6
@@ -289,7 +289,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for a.w.example. should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 6
@@ -319,7 +319,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for *.w.example. type MX should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 2
The last query response should have nscount 3
@@ -362,7 +362,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for *.w.example. type A should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 4
@@ -390,7 +390,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example. type NSEC3 should have rcode NXDOMAIN
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 8
@@ -422,7 +422,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for ai.example. type DS should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 4
@@ -450,7 +450,7 @@ Feature: NSEC3 Authoritative service
And bind10 module StatsHttpd should not be running
A dnssec query for c.example. type DS should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 6
diff --git a/tests/lettuce/features/queries.feature b/tests/lettuce/features/queries.feature
index b0a6fac..2db6c3e 100644
--- a/tests/lettuce/features/queries.feature
+++ b/tests/lettuce/features/queries.feature
@@ -53,6 +53,12 @@ Feature: Querying feature
And wait for bind10 stderr message BIND10_STARTED_CC
And wait for bind10 stderr message CMDCTL_STARTED
And wait for bind10 stderr message AUTH_SERVER_STARTED
+
+ # DATASRC_LIBRARY_ERROR must be generated due to
+ # "broken_libraries_should_be_skipped" in
+ # example.org.inmem.config
+ And wait for bind10 stderr message DATASRC_LIBRARY_ERROR
+
And wait for bind10 stderr message STATS_STARTING
bind10 module Auth should be running
@@ -75,7 +81,7 @@ Feature: Querying feature
The statistics counters are 0 in category .Auth.zones._SERVER_
A query for www.example.org should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have ancount 1
The last query response should have nscount 2
The last query response should have adcount 2
@@ -121,7 +127,7 @@ Feature: Querying feature
# Repeat of the above
A query for www.example.org should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have ancount 1
The last query response should have nscount 2
The last query response should have adcount 2
@@ -165,7 +171,7 @@ Feature: Querying feature
| rcode.noerror | 2 |
# And now query something completely different
- A query for nosuchname.example.org should have rcode NXDOMAIN
+ A recursive query for nosuchname.example.org should have rcode NXDOMAIN
The last query response should have flags qr aa rd
The last query response should have ancount 0
The last query response should have nscount 1
@@ -196,6 +202,7 @@ Feature: Querying feature
| responses | 3 |
| qrysuccess | 2 |
| qryauthans | 3 |
+ | qryrecursion | 1 |
| rcode.noerror | 2 |
| rcode.nxdomain | 1 |
@@ -225,7 +232,7 @@ Feature: Querying feature
The statistics counters are 0 in category .Auth.zones._SERVER_
A query for example.org type ANY should have rcode NOERROR
- The last query response should have flags qr aa rd
+ The last query response should have flags qr aa
The last query response should have ancount 4
The last query response should have nscount 0
The last query response should have adcount 3
@@ -284,7 +291,7 @@ Feature: Querying feature
The statistics counters are 0 in category .Auth.zones._SERVER_
A dnssec query for www.sub.example.org type AAAA should have rcode NOERROR
- The last query response should have flags qr rd
+ The last query response should have flags qr
The last query response should have edns_flags do
The last query response should have ancount 0
The last query response should have nscount 1
diff --git a/tests/lettuce/features/resolver_basic.feature b/tests/lettuce/features/resolver_basic.feature
deleted file mode 100644
index 47fc123..0000000
--- a/tests/lettuce/features/resolver_basic.feature
+++ /dev/null
@@ -1,36 +0,0 @@
-Feature: Basic Resolver
- This feature set is just testing the execution of the b10-resolver
- module. It sees whether it starts up, takes configuration, and
- answers queries.
-
- Scenario: Listen for and answer query
- # This scenario starts a server that runs a real resolver.
- # In order not to send out queries into the wild, we only
- # query for something known to be hardcoded at this moment.
- # NOTE: once real priming has been implemented, this test needs
- # to be revised (as it would then leak, which is probably true
- # for any resolver system test)
- When I start bind10 with configuration resolver/resolver_basic.config
- And wait for bind10 stderr message BIND10_STARTED_CC
- And wait for bind10 stderr message CMDCTL_STARTED
- And wait for bind10 stderr message RESOLVER_STARTED
-
- bind10 module Resolver should be running
- And bind10 module Auth should not be running
- And bind10 module Xfrout should not be running
- And bind10 module Zonemgr should not be running
- And bind10 module Xfrin should not be running
- And bind10 module Stats should not be running
- And bind10 module StatsHttpd should not be running
-
- # The ACL is set to reject any queries
- A query for l.root-servers.net. should have rcode REFUSED
-
- # Test whether acl ACCEPT works
- When I set bind10 configuration Resolver/query_acl[0] to {"action": "ACCEPT", "from": "127.0.0.1"}
- # This address is currently hardcoded, so shouldn't cause outside traffic
- A query for l.root-servers.net. should have rcode NOERROR
-
- # Check whether setting the ACL to reject again works
- When I set bind10 configuration Resolver/query_acl[0] to {"action": "REJECT", "from": "127.0.0.1"}
- A query for l.root-servers.net. should have rcode REFUSED
diff --git a/tests/lettuce/features/resolver_basic.feature.disabled b/tests/lettuce/features/resolver_basic.feature.disabled
new file mode 100644
index 0000000..341c14c
--- /dev/null
+++ b/tests/lettuce/features/resolver_basic.feature.disabled
@@ -0,0 +1,36 @@
+Feature: Basic Resolver
+ This feature set is just testing the execution of the b10-resolver
+ module. It sees whether it starts up, takes configuration, and
+ answers queries.
+
+ Scenario: Listen for and answer query
+ # This scenario starts a server that runs a real resolver.
+ # In order not to send out queries into the wild, we only
+ # query for something known to be hardcoded at this moment.
+ # NOTE: once real priming has been implemented, this test needs
+ # to be revised (as it would then leak, which is probably true
+ # for any resolver system test)
+ When I start bind10 with configuration resolver/resolver_basic.config
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message CMDCTL_STARTED
+ And wait for bind10 stderr message RESOLVER_STARTED
+
+ bind10 module Resolver should be running
+ And bind10 module Auth should not be running
+ And bind10 module Xfrout should not be running
+ And bind10 module Zonemgr should not be running
+ And bind10 module Xfrin should not be running
+ And bind10 module Stats should not be running
+ And bind10 module StatsHttpd should not be running
+
+ # The ACL is set to reject any queries
+ A recursive query for l.root-servers.net. should have rcode REFUSED
+
+ # Test whether acl ACCEPT works
+ When I set bind10 configuration Resolver/query_acl[0] to {"action": "ACCEPT", "from": "127.0.0.1"}
+ # This address is currently hardcoded, so shouldn't cause outside traffic
+ A recursive query for l.root-servers.net. should have rcode NOERROR
+
+ # Check whether setting the ACL to reject again works
+ When I set bind10 configuration Resolver/query_acl[0] to {"action": "REJECT", "from": "127.0.0.1"}
+ A recursive query for l.root-servers.net. should have rcode REFUSED
diff --git a/tests/lettuce/features/terrain/loadzone.py b/tests/lettuce/features/terrain/loadzone.py
new file mode 100644
index 0000000..32fc82b
--- /dev/null
+++ b/tests/lettuce/features/terrain/loadzone.py
@@ -0,0 +1,106 @@
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from lettuce import *
+import subprocess
+import tempfile
+
+def run_loadzone(zone, zone_file, db_file):
+ """Run b10-loadzone.
+
+ It currently only works for an SQLite3-based data source, and takes
+ its DB file as a parameter.
+
+ Parameters:
+ zone (str): the zone name
+ zone_file (str): master zone file for the zone; can be None to make an
+ empty zone.
+ db_file (str): SQLite3 DB file to load the zone into
+
+ """
+ sqlite_datasrc_cfg = '{"database_file": "' + db_file + '"}'
+ if zone_file is not None:
+ args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, zone, zone_file]
+ else:
+ args = ['b10-loadzone', '-c', sqlite_datasrc_cfg, '-e', zone]
+ loadzone = subprocess.Popen(args, 1, None, None,
+ subprocess.PIPE, subprocess.PIPE)
+ (stdout, stderr) = loadzone.communicate()
+ result = loadzone.returncode
+ world.last_loadzone_stdout = stdout
+ world.last_loadzone_stderr = stderr
+ assert result == 0, "loadzone exit code: " + str(result) +\
+ "\nstdout:\n" + str(stdout) +\
+ "stderr:\n" + str(stderr)
+
+ at step('load zone (\S+) to DB file (\S+) from (\S+)')
+def load_zone_to_dbfile(step, zone, db_file, zone_file):
+ """Load a zone into a data source from a zone file.
+
+ It currently only works for an SQLite3-based data source. Its
+ DB file name should be specified.
+
+ Step definition:
+ load zone <zone_name> to DB file <db_file> from <zone_file>
+
+ """
+ run_loadzone(zone, zone_file, db_file)
+
+ at step('make empty zone (\S+) in DB file (\S+)')
+def make_empty_zone_to_dbfile(step, zone, db_file):
+ """Make an empty zone into a data source.
+
+ If a non-empty zone already exists in the data source, it will be emptied;
+ otherwise, a new empty zone will be created.
+
+ It currently only works for an SQLite3-based data source. Its
+ DB file name should be specified.
+
+ Step definition:
+ make empty zone <zone_name> to DB file <db_file>
+
+ """
+ run_loadzone(zone, None, db_file)
+
+ at step('load (\d+) records for zone (\S+) to DB file (\S+)')
+def load_zone_rr_to_dbfile(step, num_records, zone, db_file):
+ """Load a zone with a specified number of RRs into a data source.
+
+ It currently only works for an SQLite3-based data source. Its
+ DB file name should be specified.
+
+ It creates the content of the zone dynamically so the total number of
+ RRs of the zone is the specified number, including mandatory SOA and NS.
+
+ Step definition:
+ load zone <zone_name> to DB file <db_file> from <zone_file>
+
+ """
+ num_records = int(num_records)
+ assert num_records >= 2, 'zone must have at least 2 RRs: SOA and NS'
+ num_records -= 2
+ with tempfile.NamedTemporaryFile(mode='w', prefix='zone-file',
+ dir='data/', delete=True) as f:
+ filename = f.name
+ f.write('$TTL 3600\n')
+ f.write('$ORIGIN .\n') # so it'll work whether or not zone ends with .
+ f.write(zone + ' IN SOA . . 0 0 0 0 0\n')
+ f.write(zone + ' IN NS 0.' + zone + '\n')
+ count = 0
+ while count < num_records:
+ f.write(str(count) + '.' + zone + ' IN A 192.0.2.1\n')
+ count += 1
+ f.flush()
+ run_loadzone(zone, f.name, db_file)
diff --git a/tests/lettuce/features/terrain/querying.py b/tests/lettuce/features/terrain/querying.py
index ae348fd..d585e5e 100644
--- a/tests/lettuce/features/terrain/querying.py
+++ b/tests/lettuce/features/terrain/querying.py
@@ -110,6 +110,8 @@ class QueryResult(object):
self.line_handler = self.parse_answer
elif line == ";; OPT PSEUDOSECTION:\n":
self.line_handler = self.parse_opt
+ elif line == ";; QUESTION SECTION:\n":
+ self.line_handler = self.parse_question
elif line == ";; AUTHORITY SECTION:\n":
self.line_handler = self.parse_authority
elif line == ";; ADDITIONAL SECTION:\n":
@@ -200,14 +202,19 @@ class QueryResult(object):
"""
pass
- at step('A (dnssec )?query for ([\S]+) (?:type ([A-Z0-9]+) )?' +
+ at step('A (dnssec )?(recursive )?query for ([\S]+) (?:type ([A-Z0-9]+) )?' +
'(?:class ([A-Z]+) )?(?:to ([^:]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))? )?' +
'should have rcode ([\w.]+)')
-def query(step, dnssec, query_name, qtype, qclass, addr, port, rcode):
+def query(step, dnssec, recursive, query_name, qtype, qclass, addr, port,
+ rcode):
"""
Run a query, check the rcode of the response, and store the query
result in world.last_query_result.
Parameters:
+ dnssec ('dnssec'): DO bit is set in the query.
+ Defaults to unset (no DNSSEC).
+ recursive ('recursive'): RD bit is set in the query.
+ Defaults to unset (no recursion).
query_name ('query for <name>'): The domain name to query.
qtype ('type <type>', optional): The RR type to query. Defaults to A.
qclass ('class <class>', optional): The RR class to query. Defaults to IN.
@@ -234,6 +241,9 @@ def query(step, dnssec, query_name, qtype, qclass, addr, port, rcode):
# additional counts, so unless we need dnssec, explicitly
# disable edns0
additional_arguments.append("+noedns")
+ # dig sets RD bit by default.
+ if recursive is None:
+ additional_arguments.append("+norecurse")
query_result = QueryResult(query_name, qtype, qclass, addr, port,
additional_arguments)
assert query_result.rcode == rcode,\
@@ -282,8 +292,8 @@ def check_last_query(step, item, value):
assert str(value) == str(lq_val),\
"Got: " + str(lq_val) + ", expected: " + str(value)
- at step('([a-zA-Z]+) section of the last query response should be')
-def check_last_query_section(step, section):
+ at step('([a-zA-Z]+) section of the last query response should (exactly )?be')
+def check_last_query_section(step, section, exact):
"""
Check the entire contents of the given section of the response of the last
query.
@@ -322,9 +332,10 @@ def check_last_query_section(step, section):
# replace whitespace of any length by one space
response_string = re.sub("[ \t]+", " ", response_string)
expect = re.sub("[ \t]+", " ", step.multiline)
- # lowercase them
- response_string = response_string.lower()
- expect = expect.lower()
+ # lowercase them unless we need to do an exact match
+ if exact is None:
+ response_string = response_string.lower()
+ expect = expect.lower()
# sort them
response_string_parts = response_string.split("\n")
response_string_parts.sort()
diff --git a/tests/lettuce/features/terrain/terrain.py b/tests/lettuce/features/terrain/terrain.py
index b861442..d0ba4fe 100644
--- a/tests/lettuce/features/terrain/terrain.py
+++ b/tests/lettuce/features/terrain/terrain.py
@@ -83,7 +83,9 @@ copylist = [
["data/xfrin-notify.sqlite3.orig",
"data/xfrin-notify.sqlite3"],
["data/ddns/example.org.sqlite3.orig",
- "data/ddns/example.org.sqlite3"]
+ "data/ddns/example.org.sqlite3"],
+ ["data/empty_db.sqlite3",
+ "data/xfrout.sqlite3"]
]
# This is a list of files that, if present, will be removed before a scenario
@@ -133,7 +135,7 @@ class RunningProcess:
"""
stderr_write = open(self.stderr_filename, "w")
stdout_write = open(self.stdout_filename, "w")
- self.process = subprocess.Popen(args, 1, None, subprocess.PIPE,
+ self.process = subprocess.Popen(args, 0, None, subprocess.PIPE,
stdout_write, stderr_write)
# open them again, this time for reading
self.stderr = open(self.stderr_filename, "r")
diff --git a/tests/lettuce/features/terrain/transfer.py b/tests/lettuce/features/terrain/transfer.py
index eb4ddc0..0983de5 100644
--- a/tests/lettuce/features/terrain/transfer.py
+++ b/tests/lettuce/features/terrain/transfer.py
@@ -18,7 +18,8 @@
# and inspect the results.
#
# Like querying.py, it uses dig to do the transfers, and
-# places its output in a result structure
+# places its output in a result structure. It also uses a custom client
+# implementation for less normal operations.
#
# This is done in a different file with different steps than
# querying, because the format of dig's output is
@@ -58,7 +59,16 @@ class TransferResult(object):
if len(line) > 0 and line[0] != ';':
self.records.append(line)
- at step('An AXFR transfer of ([\w.]+)(?: from ([^:]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
+def parse_addr_port(address, port):
+ if address is None:
+ address = "::1" # default address
+ # convert [IPv6_addr] to IPv6_addr:
+ address = re.sub(r"\[(.+)\]", r"\1", address)
+ if port is None:
+ port = 47806 # default port
+ return (address, port)
+
+ at step('An AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?')
def perform_axfr(step, zone_name, address, port):
"""
Perform an AXFR transfer, and store the result as an instance of
@@ -70,15 +80,60 @@ def perform_axfr(step, zone_name, address, port):
Address defaults to ::1
Port defaults to 47806
"""
- if address is None:
- address = "::1"
- # convert [IPv6_addr] to IPv6_addr:
- address = re.sub(r"\[(.+)\]", r"\1", address)
- if port is None:
- port = 47806
+ (address, port) = parse_addr_port(address, port)
args = [ 'dig', 'AXFR', '@' + str(address), '-p', str(port), zone_name ]
world.transfer_result = TransferResult(args)
+ at step('A customized AXFR transfer of ([\w.]+)(?: from ([\d.]+|\[[0-9a-fA-F:]+\])(?::([0-9]+))?)?(?: with pose of (\d+) seconds?)?')
+def perform_custom_axfr(step, zone_name, address, port, delay):
+ """Checks AXFR transfer, and store the result in the form of internal
+ CustomTransferResult class, which is compatible with TransferResult.
+
+ Step definition:
+ A customized AXFR transfer of <zone_name> [from <address>:<port>] [with pose of <delay> second]
+
+ If optional delay is specified (not None), it waits for the specified
+ seconds after sending the AXFR query before starting receiving
+ responses. This emulates a slower AXFR client.
+
+ """
+
+ class CustomTransferResult:
+ """Store transfer result only on the number of received answer RRs.
+
+ To be compatible with TransferResult it stores the result in the
+ 'records' attribute, which is a list. But its content is
+ meaningless; its only use is to be used with
+ check_transfer_result_count where its length is of concern.
+
+ """
+ def __init__(self):
+ self.records = []
+
+ # Build arguments and run xfr-client.py. On success, it simply dumps
+ # the number of received answer RRs to stdout.
+ (address, port) = parse_addr_port(address, port)
+ args = ['/bin/sh', 'run_python-tool.sh', 'tools/xfr-client.py',
+ '-s', address, '-p', str(port)]
+ if delay is not None:
+ args.extend(['-d', delay])
+ args.append(zone_name)
+ client = subprocess.Popen(args, 1, None, None, subprocess.PIPE,
+ subprocess.PIPE)
+ (stdout, stderr) = client.communicate()
+ result = client.returncode
+ world.last_client_stdout = stdout
+ world.last_client_stderr = stderr
+ assert result == 0, "xfr-client exit code: " + str(result) +\
+ "\nstdout:\n" + str(stdout) +\
+ "stderr:\n" + str(stderr)
+ num_rrs = int(stdout.strip())
+
+ # Make the result object, storing dummy value (None) for the number of
+ # answer RRs in the records list.
+ world.transfer_result = CustomTransferResult()
+ world.transfer_result.records = [None for _ in range(0, num_rrs)]
+
@step('An IXFR transfer of ([\w.]+) (\d+)(?: from ([^:]+)(?::([0-9]+))?)?(?: over (tcp|udp))?')
def perform_ixfr(step, zone_name, serial, address, port, protocol):
"""
diff --git a/tests/lettuce/features/xfrin_bind10.feature b/tests/lettuce/features/xfrin_bind10.feature
index 90a1144..db4bddb 100644
--- a/tests/lettuce/features/xfrin_bind10.feature
+++ b/tests/lettuce/features/xfrin_bind10.feature
@@ -20,10 +20,13 @@ Feature: Xfrin
And wait for bind10 stderr message XFRIN_STARTED
And wait for bind10 stderr message ZONEMGR_STARTED
- # Now we use the first step again to see if the file has been created
+ # Now we use the first step again to see if the file has been created.
+ # The DB currently doesn't know anything about the zone, so we install
+ # an empty zone for xfrin.
The file data/test_nonexistent_db.sqlite3 should exist
-
A query for www.example.org to [::1]:47806 should have rcode REFUSED
+ Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
When I send bind10 the command Xfrin retransfer example.org IN ::1 47807
# The data we receive contain a NS RRset that refers to three names in the
# example.org. zone. All these three are nonexistent in the data, producing
@@ -80,6 +83,9 @@ Feature: Xfrin
And wait for bind10 stderr message CMDCTL_STARTED
And wait for bind10 stderr message XFRIN_STARTED
+ # For xfrin make the data source aware of the zone (with empty data)
+ Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
# Set slave config for 'automatic' xfrin
When I set bind10 configuration Xfrin/zones to [{"master_port": 47806, "name": "example.org", "master_addr": "::1"}]
@@ -135,10 +141,12 @@ Feature: Xfrin
And wait for bind10 stderr message XFRIN_STARTED
And wait for bind10 stderr message ZONEMGR_STARTED
- # Now we use the first step again to see if the file has been created
+ # Now we use the first step again to see if the file has been created,
+ # then install empty zone data
The file data/test_nonexistent_db.sqlite3 should exist
-
A query for www.example.org to [::1]:47806 should have rcode REFUSED
+ Then make empty zone example.org in DB file data/test_nonexistent_db.sqlite3
+
When I send bind10 the command Xfrin retransfer example.org IN ::1 47807
# It should complain once about invalid data, then again that the whole
# zone is invalid and then reject it.
@@ -182,7 +190,8 @@ Feature: Xfrin
example. 3600 IN SOA ns1.example. hostmaster.example. 94 3600 900 7200 300
"""
- When I send bind10 the command Xfrin retransfer example. IN ::1 47807
+ # To invoke IXFR we need to use refresh command
+ When I send bind10 the command Xfrin refresh example. IN ::1 47807
Then wait for new bind10 stderr message XFRIN_GOT_INCREMENTAL_RESP
Then wait for new bind10 stderr message XFRIN_IXFR_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
# This can't be 'wait for new'
diff --git a/tests/lettuce/features/xfrin_notify_handling.feature b/tests/lettuce/features/xfrin_notify_handling.feature
index 375a8a9..2f43dfd 100644
--- a/tests/lettuce/features/xfrin_notify_handling.feature
+++ b/tests/lettuce/features/xfrin_notify_handling.feature
@@ -28,8 +28,8 @@ Feature: Xfrin incoming notify handling
# Test1 for Xfrout statistics
#
check initial statistics not containing example.org for Xfrout with cmdctl port 47804 except for the following items
- | item_name | item_max | item_min |
- | socket.unixdomain.open | 1 | 0 |
+ | item_name | min_value | max_value |
+ | socket.unixdomain.open | 0 | 1 |
# Note: .Xfrout.socket.unixdomain.open can be either expected to
# be 0 or 1 here. The reason is: if b10-xfrout has started up and is
# ready for a request from b10-stats, then b10-stats does request
@@ -41,7 +41,13 @@ Feature: Xfrin incoming notify handling
#
# Test2 for Xfrin statistics
#
- check initial statistics not containing example.org for Xfrin
+ check initial statistics not containing example.org for Xfrin except for the following items
+ | item_name | min_value | max_value |
+ | soa_in_progress | 0 | 1 |
+ | axfr_running | 0 | 1 |
+ # Note: soa_in_progress and axfr_running cannot be always a fixed value. The
+ # reason is same as the case of .Xfrout.socket.unixdomain.open. as described
+ # above.
When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
@@ -75,7 +81,7 @@ Feature: Xfrin incoming notify handling
last bindctl output should not contain "error"
When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
- The statistics counters are 0 in category .Xfrout.zones except for the following items
+ The statistics counters are 0 in category .Xfrout.zones.IN except for the following items
| item_name | item_value |
| _SERVER_.notifyoutv6 | 1 |
| _SERVER_.xfrreqdone | 1 |
@@ -99,17 +105,23 @@ Feature: Xfrin incoming notify handling
wait for new bind10 stderr message XFRIN_RECEIVED_COMMAND
last bindctl output should not contain "error"
- When I query statistics zones of bind10 module Xfrin with cmdctl
- The statistics counters are 0 in category .Xfrin.zones except for the following items
- | item_name | item_value | min_value |
- | _SERVER_.soaoutv6 | 1 | |
- | _SERVER_.axfrreqv6 | 1 | |
- | _SERVER_.xfrsuccess | 1 | |
- | _SERVER_.last_axfr_duration | | 0.0 |
- | example.org..soaoutv6 | 1 | |
- | example.org..axfrreqv6 | 1 | |
- | example.org..xfrsuccess | 1 | |
- | example.org..last_axfr_duration | | 0.0 |
+ When I query statistics of bind10 module Xfrin with cmdctl
+ The statistics counters are 0 in category .Xfrin except for the following items
+ | item_name | item_value | min_value |
+ | zones.IN._SERVER_.soaoutv6 | 1 | |
+ | zones.IN._SERVER_.axfrreqv6 | 1 | |
+ | zones.IN._SERVER_.xfrsuccess | 1 | |
+ | zones.IN._SERVER_.last_axfr_duration | | 0.0 |
+ | zones.IN.example.org..soaoutv6 | 1 | |
+ | zones.IN.example.org..axfrreqv6 | 1 | |
+ | zones.IN.example.org..xfrsuccess | 1 | |
+ | zones.IN.example.org..last_axfr_duration | | 0.0 |
+ | soa_in_progress | 0 | |
+ | axfr_running | 0 | |
+ | socket.ipv6.tcp.open | | 1 |
+ | socket.ipv6.tcp.close | | 1 |
+ | socket.ipv6.tcp.conn | | 1 |
+ | socket.ipv6.tcp.connfail | 0 | |
#
# Test for handling incoming notify only in IPv4
@@ -136,14 +148,18 @@ Feature: Xfrin incoming notify handling
# Test1 for Xfrout statistics
#
check initial statistics not containing example.org for Xfrout with cmdctl port 47804 except for the following items
- | item_name | item_max | item_min |
- | socket.unixdomain.open | 1 | 0 |
+ | item_name | min_value | max_value |
+ | socket.unixdomain.open | 0 | 1 |
# Note: See above about .Xfrout.socket.unixdomain.open.
#
# Test2 for Xfrin statistics
#
- check initial statistics not containing example.org for Xfrin
+ check initial statistics not containing example.org for Xfrin except for the following items
+ | item_name | min_value | max_value |
+ | soa_in_progress | 0 | 1 |
+ | axfr_running | 0 | 1 |
+ # Note: See above about soa_in_progress and axfr_running of Xfrin
When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
@@ -177,7 +193,7 @@ Feature: Xfrin incoming notify handling
last bindctl output should not contain "error"
When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
- The statistics counters are 0 in category .Xfrout.zones except for the following items
+ The statistics counters are 0 in category .Xfrout.zones.IN except for the following items
| item_name | item_value |
| _SERVER_.notifyoutv4 | 1 |
| _SERVER_.xfrreqdone | 1 |
@@ -201,17 +217,23 @@ Feature: Xfrin incoming notify handling
wait for new bind10 stderr message XFRIN_RECEIVED_COMMAND
last bindctl output should not contain "error"
- When I query statistics zones of bind10 module Xfrin with cmdctl
- The statistics counters are 0 in category .Xfrin.zones except for the following items
- | item_name | item_value | min_value |
- | _SERVER_.soaoutv4 | 1 | |
- | _SERVER_.axfrreqv4 | 1 | |
- | _SERVER_.xfrsuccess | 1 | |
- | _SERVER_.last_axfr_duration | | 0.0 |
- | example.org..soaoutv4 | 1 | |
- | example.org..axfrreqv4 | 1 | |
- | example.org..xfrsuccess | 1 | |
- | example.org..last_axfr_duration | | 0.0 |
+ When I query statistics of bind10 module Xfrin with cmdctl
+ The statistics counters are 0 in category .Xfrin except for the following items
+ | item_name | item_value | min_value |
+ | zones.IN._SERVER_.soaoutv4 | 1 | |
+ | zones.IN._SERVER_.axfrreqv4 | 1 | |
+ | zones.IN._SERVER_.xfrsuccess | 1 | |
+ | zones.IN._SERVER_.last_axfr_duration | | 0.0 |
+ | zones.IN.example.org..soaoutv4 | 1 | |
+ | zones.IN.example.org..axfrreqv4 | 1 | |
+ | zones.IN.example.org..xfrsuccess | 1 | |
+ | zones.IN.example.org..last_axfr_duration | | 0.0 |
+ | soa_in_progress | 0 | |
+ | axfr_running | 0 | |
+ | socket.ipv4.tcp.open | | 1 |
+ | socket.ipv4.tcp.close | | 1 |
+ | socket.ipv4.tcp.conn | | 1 |
+ | socket.ipv4.tcp.connfail | 0 | |
#
# Test for Xfr request rejected
@@ -238,14 +260,18 @@ Feature: Xfrin incoming notify handling
# Test1 for Xfrout statistics
#
check initial statistics not containing example.org for Xfrout with cmdctl port 47804 except for the following items
- | item_name | item_max | item_min |
- | socket.unixdomain.open | 1 | 0 |
+ | item_name | min_value | max_value |
+ | socket.unixdomain.open | 0 | 1 |
# Note: See above about .Xfrout.socket.unixdomain.open.
#
# Test2 for Xfrin statistics
#
- check initial statistics not containing example.org for Xfrin
+ check initial statistics not containing example.org for Xfrin except for the following items
+ | item_name | min_value | max_value |
+ | soa_in_progress | 0 | 1 |
+ | axfr_running | 0 | 1 |
+ # Note: See above about soa_in_progress and axfr_running of Xfrin
#
# set transfer_acl rejection
@@ -282,7 +308,7 @@ Feature: Xfrin incoming notify handling
last bindctl output should not contain "error"
When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
- The statistics counters are 0 in category .Xfrout.zones except for the following items
+ The statistics counters are 0 in category .Xfrout.zones.IN except for the following items
| item_name | item_value | min_value | max_value |
| _SERVER_.notifyoutv6 | 1 | | |
| _SERVER_.xfrrej | | 1 | 3 |
@@ -309,15 +335,21 @@ Feature: Xfrin incoming notify handling
wait for new bind10 stderr message XFRIN_RECEIVED_COMMAND
last bindctl output should not contain "error"
- When I query statistics zones of bind10 module Xfrin with cmdctl
- The statistics counters are 0 in category .Xfrin.zones except for the following items
- | item_name | item_value |
- | _SERVER_.soaoutv6 | 1 |
- | _SERVER_.axfrreqv6 | 1 |
- | _SERVER_.xfrfail | 1 |
- | example.org..soaoutv6 | 1 |
- | example.org..axfrreqv6 | 1 |
- | example.org..xfrfail | 1 |
+ When I query statistics of bind10 module Xfrin with cmdctl
+ The statistics counters are 0 in category .Xfrin except for the following items
+ | item_name | item_value | min_value |
+ | zones.IN._SERVER_.soaoutv6 | | 1 |
+ | zones.IN._SERVER_.axfrreqv6 | | 1 |
+ | zones.IN._SERVER_.xfrfail | | 1 |
+ | zones.IN.example.org..soaoutv6 | | 1 |
+ | zones.IN.example.org..axfrreqv6 | | 1 |
+ | zones.IN.example.org..xfrfail | | 1 |
+ | soa_in_progress | | 0 |
+ | axfr_running | | 0 |
+ | socket.ipv6.tcp.open | | 1 |
+ | socket.ipv6.tcp.close | | 1 |
+ | socket.ipv6.tcp.conn | | 1 |
+ | socket.ipv6.tcp.connfail | 0 | |
#
# Test for Xfr request rejected in IPv4
@@ -344,14 +376,18 @@ Feature: Xfrin incoming notify handling
# Test1 for Xfrout statistics
#
check initial statistics not containing example.org for Xfrout with cmdctl port 47804 except for the following items
- | item_name | item_max | item_min |
- | socket.unixdomain.open | 1 | 0 |
+ | item_name | min_value | max_value |
+ | socket.unixdomain.open | 0 | 1 |
# Note: See above about .Xfrout.socket.unixdomain.open.
#
# Test2 for Xfrin statistics
#
- check initial statistics not containing example.org for Xfrin
+ check initial statistics not containing example.org for Xfrin except for the following items
+ | item_name | min_value | max_value |
+ | soa_in_progress | 0 | 1 |
+ | axfr_running | 0 | 1 |
+ # Note: See above about soa_in_progress and axfr_running of Xfrin
#
# set transfer_acl rejection
@@ -388,7 +424,7 @@ Feature: Xfrin incoming notify handling
last bindctl output should not contain "error"
When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
- The statistics counters are 0 in category .Xfrout.zones except for the following items
+ The statistics counters are 0 in category .Xfrout.zones.IN except for the following items
| item_name | item_value | min_value | max_value |
| _SERVER_.notifyoutv4 | 1 | | |
| _SERVER_.xfrrej | | 1 | 3 |
@@ -415,15 +451,21 @@ Feature: Xfrin incoming notify handling
wait for new bind10 stderr message XFRIN_RECEIVED_COMMAND
last bindctl output should not contain "error"
- When I query statistics zones of bind10 module Xfrin with cmdctl
- The statistics counters are 0 in category .Xfrin.zones except for the following items
- | item_name | item_value |
- | _SERVER_.soaoutv4 | 1 |
- | _SERVER_.axfrreqv4 | 1 |
- | _SERVER_.xfrfail | 1 |
- | example.org..soaoutv4 | 1 |
- | example.org..axfrreqv4 | 1 |
- | example.org..xfrfail | 1 |
+ When I query statistics of bind10 module Xfrin with cmdctl
+ The statistics counters are 0 in category .Xfrin except for the following items
+ | item_name | item_value | min_value |
+ | zones.IN._SERVER_.soaoutv4 | | 1 |
+ | zones.IN._SERVER_.axfrreqv4 | | 1 |
+ | zones.IN._SERVER_.xfrfail | | 1 |
+ | zones.IN.example.org..soaoutv4 | | 1 |
+ | zones.IN.example.org..axfrreqv4 | | 1 |
+ | zones.IN.example.org..xfrfail | | 1 |
+ | soa_in_progress | | 0 |
+ | axfr_running | | 0 |
+ | socket.ipv4.tcp.open | | 1 |
+ | socket.ipv4.tcp.close | | 1 |
+ | socket.ipv4.tcp.conn | | 1 |
+ | socket.ipv4.tcp.connfail | 0 | |
#
# Test for unreachable slave
@@ -454,10 +496,10 @@ Feature: Xfrin incoming notify handling
last bindctl output should not contain "error"
When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
- The statistics counters are 0 in category .Xfrout.zones except for the following items
+ The statistics counters are 0 in category .Xfrout.zones.IN except for the following items
| item_name | min_value | max_value |
- | _SERVER_.notifyoutv6 | 1 | 5 |
- | example.org..notifyoutv6 | 1 | 5 |
+ | _SERVER_.notifyoutv6 | 1 | 5 |
+ | example.org..notifyoutv6 | 1 | 5 |
When I query statistics socket of bind10 module Xfrout with cmdctl port 47804
The statistics counters are 0 in category .Xfrout.socket.unixdomain except for the following items
@@ -587,3 +629,46 @@ Feature: Xfrin incoming notify handling
Then wait for master stderr message NOTIFY_OUT_TIMEOUT not NOTIFY_OUT_REPLY_RECEIVED
A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
+
+ #
+ # Test for unreachable master
+ #
+ Scenario: Handle incoming notify (unreachable master)
+
+ And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message CMDCTL_STARTED
+ And wait for bind10 stderr message AUTH_SERVER_STARTED
+ And wait for bind10 stderr message XFRIN_STARTED
+ And wait for bind10 stderr message ZONEMGR_STARTED
+
+ A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
+
+ #
+ # Test1 for Xfrin statistics
+ #
+ check initial statistics not containing example.org for Xfrin
+
+ #
+ # execute reftransfer for Xfrin
+ #
+ When I send bind10 the command Xfrin retransfer example.org IN
+ Then wait for new bind10 stderr message XFRIN_CONNECT_MASTER
+ Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED
+
+ #
+ # Test2 for Xfrin statistics
+ #
+ # check initial statistics
+ #
+
+ # wait until the last stats requesting is finished
+ wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
+ wait for new bind10 stderr message XFRIN_RECEIVED_COMMAND
+
+ When I query statistics socket of bind10 module Xfrin with cmdctl
+ The statistics counters are 0 in category .Xfrin.socket.ipv6.tcp except for the following items
+ | item_name | min_value |
+ | open | 1 |
+ | close | 1 |
+ | connfail | 1 |
diff --git a/tests/lettuce/features/xfrout_bind10.feature b/tests/lettuce/features/xfrout_bind10.feature
new file mode 100644
index 0000000..7f4e4de
--- /dev/null
+++ b/tests/lettuce/features/xfrout_bind10.feature
@@ -0,0 +1,39 @@
+Feature: Xfrout
+ Tests for Xfrout, specific for BIND 10 behaviour.
+
+ Scenario: normal transfer with a moderate number of RRs
+
+ Load 100 records for zone example.org to DB file data/xfrout.sqlite3
+
+ Given I have bind10 running with configuration xfrout_master.conf
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message CMDCTL_STARTED
+ And wait for bind10 stderr message AUTH_SERVER_STARTED
+ And wait for bind10 stderr message XFROUT_STARTED
+ And wait for bind10 stderr message ZONEMGR_STARTED
+
+ # The transferred zone should have the generated 100 RRs plush one
+ # trailing SOA.
+ When I do a customized AXFR transfer of example.org
+ Then transfer result should have 101 rrs
+
+ # Similar to the previous one, but using a much larger zone, and with
+ # a small delay at the client side. It should still succeed.
+ # The specific delay (5 seconds) was chosen for an environment that
+ # revealed a bug which is now fixed to reproduce the issue; shorter delays
+ # didn't trigger the problem. Depending on the OS implementation, machine
+ # speed, etc, the same delay may be too long or too short, but in any case
+ # the test should succeed now.
+ Scenario: transfer a large zone
+
+ Load 50000 records for zone example.org to DB file data/xfrout.sqlite3
+
+ Given I have bind10 running with configuration xfrout_master.conf
+ And wait for bind10 stderr message BIND10_STARTED_CC
+ And wait for bind10 stderr message CMDCTL_STARTED
+ And wait for bind10 stderr message AUTH_SERVER_STARTED
+ And wait for bind10 stderr message XFROUT_STARTED
+ And wait for bind10 stderr message ZONEMGR_STARTED
+
+ When I do a customized AXFR transfer of example.org from [::1]:47806 with pose of 5 seconds
+ Then transfer result should have 50001 rrs
diff --git a/tests/lettuce/run_python-tool.sh b/tests/lettuce/run_python-tool.sh
new file mode 100755
index 0000000..e93068e
--- /dev/null
+++ b/tests/lettuce/run_python-tool.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# This script runs the specified python program, referring to the in-tree
+# BIND 10 Python libraries (in case the program needs them)
+# usage example: run_python-tool.sh tools/xfr-client.py -p 5300 example.org
+
+. ./setup_intree_bind10.sh
+$PYTHON_EXEC $*
diff --git a/tests/lettuce/setup_intree_bind10.sh.in b/tests/lettuce/setup_intree_bind10.sh.in
old mode 100644
new mode 100755
index b8e85d4..600f5c4
--- a/tests/lettuce/setup_intree_bind10.sh.in
+++ b/tests/lettuce/setup_intree_bind10.sh.in
@@ -20,7 +20,7 @@ export PYTHON_EXEC
BIND10_PATH=@abs_top_builddir@/src/bin/bind10
-PATH=@abs_top_builddir@/src/bin/bind10:@abs_top_builddir@/src/bin/bindctl:@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/ddns:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:$PATH
+PATH=@abs_top_builddir@/src/bin/bind10:@abs_top_builddir@/src/bin/bindctl:@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/ddns:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:@abs_top_builddir@/src/bin/loadzone:$PATH
export PATH
PYTHONPATH=@abs_top_builddir@/src/bin:@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs:$PYTHONPATH
@@ -34,6 +34,11 @@ if test $SET_ENV_LIBRARY_PATH = yes; then
export @ENV_LIBRARY_PATH@
fi
+BUILD_EXPERIMENTAL_RESOLVER=@BUILD_EXPERIMENTAL_RESOLVER@
+if test $BUILD_EXPERIMENTAL_RESOLVER = yes; then
+ cp -f @srcdir@/features/resolver_basic.feature.disabled @srcdir@/features/resolver_basic.feature
+fi
+
B10_FROM_SOURCE=@abs_top_srcdir@
export B10_FROM_SOURCE
# TODO: We need to do this feature based (ie. no general from_source)
diff --git a/tests/lettuce/tools/xfr-client.py b/tests/lettuce/tools/xfr-client.py
new file mode 100755
index 0000000..662e1ed
--- /dev/null
+++ b/tests/lettuce/tools/xfr-client.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2013 Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# A simple XFR client program with some customized behavior.
+# This is intended to provide some less common or even invalid client behavior
+# for some specific tests on outbound zone transfer.
+# It currently only supports AXFR, but can be extended to support IXFR
+# as we see the need for it.
+#
+# For command line usage, run this program with -h option.
+
+from isc.dns import *
+import sys
+import socket
+import struct
+import time
+from optparse import OptionParser
+
+parser = OptionParser(usage='usage: %prog [options] zone_name')
+parser.add_option('-d', '--delay', dest='delay', action='store', default=None,
+ help='delay (sec) before receiving responses, ' +
+ 'emulating slow clients')
+parser.add_option('-s', '--server', dest='server_addr', action='store',
+ default='::1',
+ help="master server's address [default: %default]")
+parser.add_option('-p', '--port', dest='server_port', action='store',
+ default=53,
+ help="master server's TCP port [default: %default]")
+(options, args) = parser.parse_args()
+
+if len(args) == 0:
+ parser.error('missing argument')
+
+# Parse arguments and options, and creates client socket.
+zone_name = Name(args[0])
+gai = socket.getaddrinfo(options.server_addr, int(options.server_port), 0,
+ socket.SOCK_STREAM, socket.IPPROTO_TCP,
+ socket.AI_NUMERICHOST|socket.AI_NUMERICSERV)
+server_family, server_socktype, server_proto, server_sockaddr = \
+ (gai[0][0], gai[0][1], gai[0][2], gai[0][4])
+s = socket.socket(server_family, server_socktype, server_proto)
+s.connect(server_sockaddr)
+s.settimeout(60) # safety net in case of hangup situation
+
+# Build XFR query.
+axfr_qry = Message(Message.RENDER)
+axfr_qry.set_rcode(Rcode.NOERROR)
+axfr_qry.set_opcode(Opcode.QUERY)
+axfr_qry.add_question(Question(zone_name, RRClass.IN, RRType.AXFR))
+
+renderer = MessageRenderer()
+axfr_qry.to_wire(renderer)
+qry_data = renderer.get_data()
+
+# Send the query
+hlen_data = struct.pack('H', socket.htons(len(qry_data)))
+s.send(hlen_data)
+s.send(qry_data)
+
+# If specified wait for the given period
+if options.delay is not None:
+ time.sleep(int(options.delay))
+
+def get_request_response(s, size):
+ """A helper function to receive data of specified length from a socket."""
+ recv_size = 0
+ data = b''
+ while recv_size < size:
+ need_recv_size = size - recv_size
+ tmp_data = s.recv(need_recv_size)
+ if len(tmp_data) == 0:
+ return None
+ recv_size += len(tmp_data)
+ data += tmp_data
+ return data
+
+# Receive responses until the connection is terminated, and dump the
+# number of received answer RRs to stdout.
+num_rrs = 0
+while True:
+ hlen_data = get_request_response(s, 2)
+ if hlen_data is None:
+ break
+ resp_data = get_request_response(
+ s, socket.ntohs(struct.unpack('H', hlen_data)[0]))
+ msg = Message(Message.PARSE)
+ msg.from_wire(resp_data, Message.PRESERVE_ORDER)
+ num_rrs += msg.get_rr_count(Message.SECTION_ANSWER)
+print(str(num_rrs))
diff --git a/tests/tools/badpacket/Makefile.am b/tests/tools/badpacket/Makefile.am
index b24cf3c..945d0e3 100644
--- a/tests/tools/badpacket/Makefile.am
+++ b/tests/tools/badpacket/Makefile.am
@@ -21,9 +21,6 @@ badpacket_SOURCES += scan.cc scan.h
badpacket_SOURCES += version.h
badpacket_CXXFLAGS = $(AM_CXXFLAGS)
-if USE_CLANGPP
-badpacket_CXXFLAGS += -Wno-error
-endif
badpacket_LDADD = $(top_builddir)/src/lib/asiodns/libb10-asiodns.la
badpacket_LDADD += $(top_builddir)/src/lib/dns/libb10-dns++.la
diff --git a/tests/tools/perfdhcp/Makefile.am b/tests/tools/perfdhcp/Makefile.am
index c4b82b5..4e2d22b 100644
--- a/tests/tools/perfdhcp/Makefile.am
+++ b/tests/tools/perfdhcp/Makefile.am
@@ -23,7 +23,9 @@ perfdhcp_SOURCES += command_options.cc command_options.h
perfdhcp_SOURCES += localized_option.h
perfdhcp_SOURCES += perf_pkt6.cc perf_pkt6.h
perfdhcp_SOURCES += perf_pkt4.cc perf_pkt4.h
+perfdhcp_SOURCES += packet_storage.h
perfdhcp_SOURCES += pkt_transform.cc pkt_transform.h
+perfdhcp_SOURCES += rate_control.cc rate_control.h
perfdhcp_SOURCES += stats_mgr.h
perfdhcp_SOURCES += test_control.cc test_control.h
libb10_perfdhcp___la_CXXFLAGS = $(AM_CXXFLAGS)
@@ -41,4 +43,4 @@ perfdhcp_LDADD += $(top_builddir)/src/lib/asiolink/libb10-asiolink.la
# ... and the documentation
-EXTRA_DIST = perfdhcp_internals.dox
\ No newline at end of file
+EXTRA_DIST = perfdhcp_internals.dox
diff --git a/tests/tools/perfdhcp/command_options.cc b/tests/tools/perfdhcp/command_options.cc
index c0ae6fa..26255e8 100644
--- a/tests/tools/perfdhcp/command_options.cc
+++ b/tests/tools/perfdhcp/command_options.cc
@@ -12,19 +12,21 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include "command_options.h"
+#include <exceptions/exceptions.h>
+#include <dhcp/iface_mgr.h>
+#include <dhcp/duid.h>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
#include <config.h>
+#include <sstream>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
-#include <boost/lexical_cast.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-#include <exceptions/exceptions.h>
-#include <dhcp/iface_mgr.h>
-#include <dhcp/duid.h>
-#include "command_options.h"
using namespace std;
using namespace isc;
@@ -32,6 +34,63 @@ using namespace isc;
namespace isc {
namespace perfdhcp {
+CommandOptions::LeaseType::LeaseType()
+ : type_(ADDRESS) {
+}
+
+CommandOptions::LeaseType::LeaseType(const Type lease_type)
+ : type_(lease_type) {
+}
+
+bool
+CommandOptions::LeaseType::is(const Type lease_type) const {
+ return (lease_type == type_);
+}
+
+bool
+CommandOptions::LeaseType::includes(const Type lease_type) const {
+ return (is(ADDRESS_AND_PREFIX) || (lease_type == type_));
+}
+
+void
+CommandOptions::LeaseType::set(const Type lease_type) {
+ type_ = lease_type;
+}
+
+void
+CommandOptions::LeaseType::fromCommandLine(const std::string& cmd_line_arg) {
+ if (cmd_line_arg == "address-only") {
+ type_ = ADDRESS;
+
+ } else if (cmd_line_arg == "prefix-only") {
+ type_ = PREFIX;
+
+ } else if (cmd_line_arg == "address-and-prefix") {
+ type_ = ADDRESS_AND_PREFIX;
+
+ } else {
+ isc_throw(isc::InvalidParameter, "value of lease-type: -e<lease-type>,"
+ " must be one of the following: 'address-only' or"
+ " 'prefix-only'");
+ }
+}
+
+std::string
+CommandOptions::LeaseType::toText() const {
+ switch (type_) {
+ case ADDRESS:
+ return ("address-only (IA_NA option added to the client's request)");
+ case PREFIX:
+ return ("prefix-only (IA_PD option added to the client's request)");
+ case ADDRESS_AND_PREFIX:
+ return ("address-and-prefix (Both IA_NA and IA_PD options added to the"
+ " client's request)");
+ default:
+ isc_throw(Unexpected, "internal error: undefined lease type code when"
+ " returning textual representation of the lease type");
+ }
+}
+
CommandOptions&
CommandOptions::instance() {
static CommandOptions options;
@@ -52,7 +111,10 @@ CommandOptions::reset() {
// will need to reset all members many times to perform unit tests
ipversion_ = 0;
exchange_mode_ = DORA_SARR;
+ lease_type_.set(LeaseType::ADDRESS);
rate_ = 0;
+ renew_rate_ = 0;
+ release_rate_ = 0;
report_delay_ = 0;
clients_num_ = 0;
mac_template_.assign(mac, mac + 6);
@@ -150,7 +212,7 @@ CommandOptions::initialize(int argc, char** argv, bool print_cmd_line) {
// In this section we collect argument values from command line
// they will be tuned and validated elsewhere
while((opt = getopt(argc, argv, "hv46r:t:R:b:n:p:d:D:l:P:a:L:"
- "s:iBc1T:X:O:E:S:I:x:w:")) != -1) {
+ "s:iBc1T:X:O:E:S:I:x:w:e:f:F:")) != -1) {
stream << " -" << static_cast<char>(opt);
if (optarg) {
stream << " " << optarg;
@@ -232,11 +294,26 @@ CommandOptions::initialize(int argc, char** argv, bool print_cmd_line) {
}
break;
+ case 'e':
+ initLeaseType();
+ break;
+
case 'E':
elp_offset_ = nonNegativeInteger("value of time-offset: -E<value>"
" must not be a negative integer");
break;
+ case 'f':
+ renew_rate_ = positiveInteger("value of the renew rate: -f<renew-rate>"
+ " must be a positive integer");
+ break;
+
+ case 'F':
+ release_rate_ = positiveInteger("value of the release rate:"
+ " -F<release-rate> must be a"
+ " positive integer");
+ break;
+
case 'h':
usage();
return (true);
@@ -438,7 +515,7 @@ CommandOptions::initialize(int argc, char** argv, bool print_cmd_line) {
// If DUID is not specified from command line we need to
// generate one.
- if (duid_template_.size() == 0) {
+ if (duid_template_.empty()) {
generateDuidTemplate();
}
return (false);
@@ -506,9 +583,9 @@ CommandOptions::decodeMac(const std::string& base) {
mac_template_.clear();
// Get pieces of MAC address separated with : (or even ::)
while (std::getline(s1, token, ':')) {
- unsigned int ui = 0;
// Convert token to byte value using std::istringstream
if (token.length() > 0) {
+ unsigned int ui = 0;
try {
// Do actual conversion
ui = convertHexString(token);
@@ -618,50 +695,74 @@ CommandOptions::validate() const {
"-B is not compatible with IPv6 (-6)");
check((getIpVersion() != 6) && (isRapidCommit() != 0),
"-6 (IPv6) must be set to use -c");
+ check((getIpVersion() != 6) && (getRenewRate() !=0),
+ "-f<renew-rate> may be used with -6 (IPv6) only");
+ check((getIpVersion() != 6) && (getReleaseRate() != 0),
+ "-F<release-rate> may be used with -6 (IPv6) only");
check((getExchangeMode() == DO_SA) && (getNumRequests().size() > 1),
"second -n<num-request> is not compatible with -i");
+ check((getIpVersion() == 4) && !getLeaseType().is(LeaseType::ADDRESS),
+ "-6 option must be used if lease type other than '-e address-only'"
+ " is specified");
+ check(!getTemplateFiles().empty() &&
+ !getLeaseType().is(LeaseType::ADDRESS),
+ "template files may be only used with '-e address-only'");
check((getExchangeMode() == DO_SA) && (getDropTime()[1] != 1.),
"second -d<drop-time> is not compatible with -i");
check((getExchangeMode() == DO_SA) &&
((getMaxDrop().size() > 1) || (getMaxDropPercentage().size() > 1)),
- "second -D<max-drop> is not compatible with -i\n");
+ "second -D<max-drop> is not compatible with -i");
check((getExchangeMode() == DO_SA) && (isUseFirst()),
- "-1 is not compatible with -i\n");
+ "-1 is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getTemplateFiles().size() > 1),
- "second -T<template-file> is not compatible with -i\n");
+ "second -T<template-file> is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getTransactionIdOffset().size() > 1),
- "second -X<xid-offset> is not compatible with -i\n");
+ "second -X<xid-offset> is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getRandomOffset().size() > 1),
- "second -O<random-offset is not compatible with -i\n");
+ "second -O<random-offset is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getElapsedTimeOffset() >= 0),
- "-E<time-offset> is not compatible with -i\n");
+ "-E<time-offset> is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getServerIdOffset() >= 0),
- "-S<srvid-offset> is not compatible with -i\n");
+ "-S<srvid-offset> is not compatible with -i");
check((getExchangeMode() == DO_SA) && (getRequestedIpOffset() >= 0),
- "-I<ip-offset> is not compatible with -i\n");
+ "-I<ip-offset> is not compatible with -i");
+ check((getExchangeMode() == DO_SA) && (getRenewRate() != 0),
+ "-f<renew-rate> is not compatible with -i");
+ check((getExchangeMode() == DO_SA) && (getReleaseRate() != 0),
+ "-F<release-rate> is not compatible with -i");
check((getExchangeMode() != DO_SA) && (isRapidCommit() != 0),
- "-i must be set to use -c\n");
+ "-i must be set to use -c");
check((getRate() == 0) && (getReportDelay() != 0),
- "-r<rate> must be set to use -t<report>\n");
+ "-r<rate> must be set to use -t<report>");
check((getRate() == 0) && (getNumRequests().size() > 0),
- "-r<rate> must be set to use -n<num-request>\n");
+ "-r<rate> must be set to use -n<num-request>");
check((getRate() == 0) && (getPeriod() != 0),
- "-r<rate> must be set to use -p<test-period>\n");
+ "-r<rate> must be set to use -p<test-period>");
check((getRate() == 0) &&
((getMaxDrop().size() > 0) || getMaxDropPercentage().size() > 0),
- "-r<rate> must be set to use -D<max-drop>\n");
+ "-r<rate> must be set to use -D<max-drop>");
+ check((getRate() != 0) && (getRenewRate() + getReleaseRate() > getRate()),
+ "The sum of Renew rate (-f<renew-rate>) and Release rate"
+ " (-F<release-rate>) must not be greater than the exchange"
+ " rate specified as -r<rate>");
+ check((getRate() == 0) && (getRenewRate() != 0),
+ "Renew rate specified as -f<renew-rate> must not be specified"
+ " when -r<rate> parameter is not specified");
+ check((getRate() == 0) && (getReleaseRate() != 0),
+ "Release rate specified as -F<release-rate> must not be specified"
+ " when -r<rate> parameter is not specified");
check((getTemplateFiles().size() < getTransactionIdOffset().size()),
- "-T<template-file> must be set to use -X<xid-offset>\n");
+ "-T<template-file> must be set to use -X<xid-offset>");
check((getTemplateFiles().size() < getRandomOffset().size()),
- "-T<template-file> must be set to use -O<random-offset>\n");
+ "-T<template-file> must be set to use -O<random-offset>");
check((getTemplateFiles().size() < 2) && (getElapsedTimeOffset() >= 0),
- "second/request -T<template-file> must be set to use -E<time-offset>\n");
+ "second/request -T<template-file> must be set to use -E<time-offset>");
check((getTemplateFiles().size() < 2) && (getServerIdOffset() >= 0),
"second/request -T<template-file> must be set to "
- "use -S<srvid-offset>\n");
+ "use -S<srvid-offset>");
check((getTemplateFiles().size() < 2) && (getRequestedIpOffset() >= 0),
"second/request -T<template-file> must be set to "
- "use -I<ip-offset>\n");
+ "use -I<ip-offset>");
}
@@ -669,6 +770,8 @@ void
CommandOptions::check(bool condition, const std::string& errmsg) const {
// The same could have been done with macro or just if statement but
// we prefer functions to macros here
+ std::ostringstream stream;
+ stream << errmsg << "\n";
if (condition) {
isc_throw(isc::InvalidParameter, errmsg);
}
@@ -706,6 +809,12 @@ CommandOptions::nonEmptyString(const std::string& errmsg) const {
}
void
+CommandOptions::initLeaseType() {
+ std::string lease_type_arg = optarg;
+ lease_type_.fromCommandLine(lease_type_arg);
+}
+
+void
CommandOptions::printCommandLine() const {
std::cout << "IPv" << static_cast<int>(ipversion_) << std::endl;
if (exchange_mode_ == DO_SA) {
@@ -715,9 +824,16 @@ CommandOptions::printCommandLine() const {
std::cout << "SOLICIT-ADVERETISE only" << std::endl;
}
}
+ std::cout << "lease-type=" << getLeaseType().toText() << std::endl;
if (rate_ != 0) {
std::cout << "rate[1/s]=" << rate_ << std::endl;
}
+ if (getRenewRate() != 0) {
+ std::cout << "renew-rate[1/s]=" << getRenewRate() << std::endl;
+ }
+ if (getReleaseRate() != 0) {
+ std::cout << "release-rate[1/s]=" << getReleaseRate() << std::endl;
+ }
if (report_delay_ != 0) {
std::cout << "report[s]=" << report_delay_ << std::endl;
}
@@ -800,13 +916,15 @@ CommandOptions::printCommandLine() const {
void
CommandOptions::usage() const {
std::cout <<
- "perfdhcp [-hv] [-4|-6] [-r<rate>] [-t<report>] [-R<range>] [-b<base>]\n"
- " [-n<num-request>] [-p<test-period>] [-d<drop-time>] [-D<max-drop>]\n"
- " [-l<local-addr|interface>] [-P<preload>] [-a<aggressivity>]\n"
- " [-L<local-port>] [-s<seed>] [-i] [-B] [-c] [-1]\n"
- " [-T<template-file>] [-X<xid-offset>] [-O<random-offset]\n"
- " [-E<time-offset>] [-S<srvid-offset>] [-I<ip-offset>]\n"
- " [-x<diagnostic-selector>] [-w<wrapped>] [server]\n"
+ "perfdhcp [-hv] [-4|-6] [-e<lease-type>] [-r<rate>] [-f<renew-rate>]\n"
+ " [-F<release-rate>] [-t<report>] [-R<range>] [-b<base>]\n"
+ " [-n<num-request>] [-p<test-period>] [-d<drop-time>]\n"
+ " [-D<max-drop>] [-l<local-addr|interface>] [-P<preload>]\n"
+ " [-a<aggressivity>] [-L<local-port>] [-s<seed>] [-i] [-B]\n"
+ " [-c] [-1] [-T<template-file>] [-X<xid-offset>]\n"
+ " [-O<random-offset] [-E<time-offset>] [-S<srvid-offset>]\n"
+ " [-I<ip-offset>] [-x<diagnostic-selector>] [-w<wrapped>]\n"
+ " [server]\n"
"\n"
"The [server] argument is the name/address of the DHCP server to\n"
"contact. For DHCPv4 operation, exchanges are initiated by\n"
@@ -838,6 +956,14 @@ CommandOptions::usage() const {
"-d<drop-time>: Specify the time after which a requeqst is treated as\n"
" having been lost. The value is given in seconds and may contain a\n"
" fractional component. The default is 1 second.\n"
+ "-e<lease-type>: A type of lease being requested from the server. It\n"
+ " may be one of the following: address-only, prefix-only or\n"
+ " address-and-prefix. The address-only indicates that the regular\n"
+ " address (v4 or v6) will be requested. The prefix-only indicates\n"
+ " that the IPv6 prefix will be requested. The address-and-prefix\n"
+ " indicates that both IPv6 address and prefix will be requested.\n"
+ " The '-e prefix-only' and -'e address-and-prefix' must not be\n"
+ " used with -4.\n"
"-E<time-offset>: Offset of the (DHCPv4) secs field / (DHCPv6)\n"
" elapsed-time option in the (second/request) template.\n"
" The value 0 disables it.\n"
@@ -888,6 +1014,16 @@ CommandOptions::usage() const {
"\n"
"DHCPv6 only options:\n"
"-c: Add a rapid commit option (exchanges will be SA).\n"
+ "-f<renew-rate>: Rate at which IPv6 Renew requests are sent to\n"
+ " a server. This value is only valid when used in conjunction with\n"
+ " the exchange rate (given by -r<rate>). Furthermore the sum of\n"
+ " this value and the release-rate (given by -F<rate) must be equal\n"
+ " to or less than the exchange rate.\n"
+ "-F<release-rate>: Rate at which IPv6 Release requests are sent to\n"
+ " a server. This value is only valid when used in conjunction with\n"
+ " the exchange rate (given by -r<rate>). Furthermore the sum of\n"
+ " this value and the renew-rate (given by -f<rate) must be equal\n"
+ " to or less than the exchange rate.\n"
"\n"
"The remaining options are used only in conjunction with -r:\n"
"\n"
diff --git a/tests/tools/perfdhcp/command_options.h b/tests/tools/perfdhcp/command_options.h
index 246fea3..7395eae 100644
--- a/tests/tools/perfdhcp/command_options.h
+++ b/tests/tools/perfdhcp/command_options.h
@@ -15,11 +15,12 @@
#ifndef COMMAND_OPTIONS_H
#define COMMAND_OPTIONS_H
+#include <boost/noncopyable.hpp>
+
+#include <stdint.h>
#include <string>
#include <vector>
-#include <boost/noncopyable.hpp>
-
namespace isc {
namespace perfdhcp {
@@ -30,6 +31,79 @@ namespace perfdhcp {
///
class CommandOptions : public boost::noncopyable {
public:
+
+ /// \brief A class encapsulating the type of lease being requested from the
+ /// server.
+ ///
+ /// This class comprises convenience functions to convert the lease type
+ /// to the textual format and to match the appropriate lease type with the
+ /// value of the -e<lease-type> parameter specified from the command line.
+ class LeaseType {
+ public:
+
+ /// The lease type code.
+ enum Type {
+ ADDRESS,
+ PREFIX,
+ ADDRESS_AND_PREFIX
+ };
+
+ LeaseType();
+
+ /// \brief Constructor from lease type code.
+ ///
+ /// \param lease_type A lease type code.
+ LeaseType(const Type lease_type);
+
+ /// \brief Checks if lease type has the specified code.
+ ///
+ /// \param lease_type A lease type code to be checked.
+ ///
+ /// \return true if lease type is matched with the specified code.
+ bool is(const Type lease_type) const;
+
+ /// \brief Checks if lease type implies request for the address,
+ /// prefix (or both) as specified by the function argument.
+ ///
+ /// This is a convenience function to check that, for the lease type
+ /// specified from the command line, the address or prefix
+ /// (IA_NA or IA_PD) option should be sent to the server.
+ /// For example, if user specified '-e address-and-prefix' in the
+ /// command line this function will return true for both ADDRESS
+ /// and PREFIX, because both address and prefix is requested from
+ /// the server.
+ ///
+ /// \param lease_type A lease type.
+ ///
+ /// \return true if the lease type implies creation of the address,
+ /// prefix or both as specified by the argument.
+ bool includes(const Type lease_type) const;
+
+ /// \brief Sets the lease type code.
+ ///
+ /// \param lease_type A lease type code.
+ void set(const Type lease_type);
+
+ /// \brief Sets the lease type from the command line argument.
+ ///
+ /// \param cmd_line_arg An argument specified in the command line
+ /// as -e<lease-type>:
+ /// - address-only
+ /// - prefix-only
+ ///
+ /// \throw isc::InvalidParameter if the specified argument is invalid.
+ void fromCommandLine(const std::string& cmd_line_arg);
+
+ /// \brief Return textual representation of the lease type.
+ ///
+ /// \return A textual representation of the lease type.
+ std::string toText() const;
+
+ private:
+ Type type_; ///< A lease type code.
+
+ };
+
/// 2-way (cmd line param -i) or 4-way exchanges
enum ExchangeMode {
DO_SA,
@@ -71,11 +145,26 @@ public:
/// \return packet exchange mode.
ExchangeMode getExchangeMode() const { return exchange_mode_; }
+ /// \ brief Returns the type of lease being requested.
+ ///
+ /// \return type of lease being requested by perfdhcp.
+ LeaseType getLeaseType() const { return (lease_type_); }
+
/// \brief Returns echange rate.
///
/// \return exchange rate per second.
int getRate() const { return rate_; }
+ /// \brief Returns a rate at which DHCPv6 Renew messages are sent.
+ ///
+ /// \return A rate at which IPv6 Renew messages are sent.
+ int getRenewRate() const { return (renew_rate_); }
+
+ /// \brief Returns a rate at which DHCPv6 Release messages are sent.
+ ///
+ /// \return A rate at which DHCPv6 Release messages are sent.
+ int getReleaseRate() const { return (release_rate_); }
+
/// \brief Returns delay between two performance reports.
///
/// \return delay between two consecutive performance reports.
@@ -300,6 +389,11 @@ private:
/// \throw InvalidParameter if string is empty.
std::string nonEmptyString(const std::string& errmsg) const;
+ /// \brief Decodes the lease type requested by perfdhcp from optarg.
+ ///
+ /// \throw InvalidParameter if lease type value specified is invalid.
+ void initLeaseType();
+
/// \brief Set number of clients.
///
/// Interprets the getopt() "opt" global variable as the number of clients
@@ -373,8 +467,14 @@ private:
uint8_t ipversion_;
/// Packet exchange mode (e.g. DORA/SARR)
ExchangeMode exchange_mode_;
+ /// Lease Type to be obtained: address only, IPv6 prefix only.
+ LeaseType lease_type_;
/// Rate in exchange per second
int rate_;
+ /// A rate at which DHCPv6 Renew messages are sent.
+ int renew_rate_;
+ /// A rate at which DHCPv6 Release messages are sent.
+ int release_rate_;
/// Delay between generation of two consecutive
/// performance reports
int report_delay_;
@@ -396,7 +496,7 @@ private:
/// Indicates number of -d<value> parameters specified by user.
/// If this value goes above 2, command line parsing fails.
uint8_t drop_time_set_;
- /// Time to elapse before request is lost. The fisrt value of
+ /// Time to elapse before request is lost. The first value of
/// two-element vector refers to DO/SA exchanges,
/// second value refers to RA/RR. Default values are { 1, 1 }
std::vector<double> drop_time_;
@@ -433,12 +533,12 @@ private:
/// Indicates that we take server id from first received packet.
bool use_first_;
/// Packet template file names. These files store template packets
- /// that are used for initiating echanges. Template packets
+ /// that are used for initiating exchanges. Template packets
/// read from files are later tuned with variable data.
std::vector<std::string> template_file_;
/// Offset of transaction id in template files. First vector
/// element points to offset for DISCOVER/SOLICIT messages,
- /// second element points to trasaction id offset for
+ /// second element points to transaction id offset for
/// REQUEST messages
std::vector<int> xid_offset_;
/// Random value offset in templates. Random value offset
diff --git a/tests/tools/perfdhcp/packet_storage.h b/tests/tools/perfdhcp/packet_storage.h
new file mode 100644
index 0000000..2adb070
--- /dev/null
+++ b/tests/tools/perfdhcp/packet_storage.h
@@ -0,0 +1,161 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef PACKET_STORAGE_H
+#define PACKET_STORAGE_H
+
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <list>
+#include <stdint.h>
+
+namespace isc {
+namespace perfdhcp {
+
+/// \brief Represents a list of packets with a sequential and random access to
+/// list elements.
+///
+/// The main purpose of this class is to support sending Renew and Release
+/// messages from perfdhcp. The Renew and Release messages are sent for existing
+/// leases only. Therefore, the typical use case for this class is that it holds
+/// a list of Reply messages sent by the server in response to Request messages.
+/// The Request messages hold addresses and/or IPv6 prefixes acquired so they
+/// can be used to identify existing leases. When perfdhcp needs to send Renew
+/// or Release message, it will access one of the elements on this list and
+/// will create the Renew or Release message based on its content. Once the
+/// element (packet) is returned it is also deleted from the list, so as it is
+/// not used again. This class provide either sequential access to the packets
+/// or random access. The random access algorithm is much slower but at least
+/// it allows to simulate more real scenario when the renewing or releasing
+/// client is random.
+///
+/// \tparam Pkt4 or Pkt6 class, which represents DHCPv4 or DHCPv6 message
+/// respectively.
+///
+/// \note Although the class is intended to hold Pkt4 and Pkt6 objects, the
+/// current implementation is generic enough to holds any object wrapped in the
+/// boost::shared_ptr.
+template<typename T>
+class PacketStorage : public boost::noncopyable {
+public:
+ /// A type which represents the pointer to a packet.
+ typedef boost::shared_ptr<T> PacketPtr;
+
+private:
+ /// An internal container actually holding packets.
+ typedef typename std::list<PacketPtr> PacketContainer;
+ /// An iterator to the element in the internal container.
+ typedef typename PacketContainer::iterator PacketContainerIterator;
+
+public:
+
+ /// \brief Constructor.
+ PacketStorage() { }
+
+ /// \brief Appends the new packet object to the collection.
+ ///
+ /// \param packet A pointer to an object representing a packet.
+ void append(const PacketPtr& packet) {
+ storage_.push_back(packet);
+ if (storage_.size() == 1) {
+ current_pointer_ = storage_.begin();
+ }
+ }
+
+ /// \brief Removes packets from the storage.
+ ///
+ /// It is possible to specify a number of packets to be removed
+ /// from a storage. Packets are removed from the beginning of the
+ /// storage. If specified number is greater than the size of the
+ /// storage, all packets are removed.
+ ///
+ /// @param num A number of packets to be removed. If omitted,
+ /// all packets will be removed.
+ void clear(const uint64_t num = 0) {
+ if (num != 0) {
+ PacketContainerIterator last = storage_.begin();
+ std::advance(last, num > size() ? size() : num);
+ current_pointer_ = storage_.erase(storage_.begin(), last);
+ } else {
+ storage_.clear();
+ current_pointer_ = storage_.begin();
+ }
+ }
+
+ /// \brief Checks if the storage has no packets.
+ ///
+ /// \return true if storage is empty, false otherwise.
+ bool empty() const {
+ return (storage_.empty());
+ }
+
+ /// \brief Returns next packet from the storage.
+ ///
+ /// This function returns packets sequentially (in the same order
+ /// in which they have been appended). The returned packet is
+ /// instantly removed from the storage.
+ ///
+ /// \return next packet from the storage.
+ PacketPtr getNext() {
+ if (storage_.empty()) {
+ return (PacketPtr());
+ } else if (current_pointer_ == storage_.end()) {
+ current_pointer_ = storage_.begin();
+ }
+ PacketPtr packet = *current_pointer_;
+ current_pointer_ = storage_.erase(current_pointer_);
+ return (packet);
+ }
+
+ /// \brief Returns random packet from the storage.
+ ///
+ /// This function picks random packet from the storage and returns
+ /// it. It is way slower than the @c getNext function because it has to
+ /// iterate over all existing entries from the beginning of the storage
+ /// to the random packet's position. Therefore, care should be taken
+ /// when using this function to access elements when storage is large.
+ ///
+ /// \return random packet from the storage.
+ PacketPtr getRandom() {
+ if (empty()) {
+ return (PacketPtr());
+ }
+ current_pointer_ = storage_.begin();
+ if (size() > 1) {
+ std::advance(current_pointer_, rand() % (size() - 1));
+ }
+ PacketPtr packet = *current_pointer_;
+ current_pointer_ = storage_.erase(current_pointer_);
+ return (packet);
+ }
+
+ /// \brief Returns number of packets in the storage.
+ ///
+ /// \return number of packets in the storage.
+ uint64_t size() const {
+ return (storage_.size());
+ }
+
+private:
+
+ std::list<PacketPtr> storage_; ///< Holds all appended packets.
+ PacketContainerIterator current_pointer_; ///< Holds the iterator to the
+ ///< next element returned.
+
+};
+
+} // namespace perfdhcp
+} // namespace isc
+
+#endif // PACKET_STORAGE_H
diff --git a/tests/tools/perfdhcp/perf_pkt4.cc b/tests/tools/perfdhcp/perf_pkt4.cc
index 8b7e974..05d1c6c 100644
--- a/tests/tools/perfdhcp/perf_pkt4.cc
+++ b/tests/tools/perfdhcp/perf_pkt4.cc
@@ -40,7 +40,7 @@ PerfPkt4::rawPack() {
options_,
getTransidOffset(),
getTransid(),
- bufferOut_));
+ buffer_out_));
}
bool
diff --git a/tests/tools/perfdhcp/perf_pkt6.cc b/tests/tools/perfdhcp/perf_pkt6.cc
index 56fe9df..0ede077 100644
--- a/tests/tools/perfdhcp/perf_pkt6.cc
+++ b/tests/tools/perfdhcp/perf_pkt6.cc
@@ -43,7 +43,7 @@ PerfPkt6::rawPack() {
options_,
getTransidOffset(),
getTransid(),
- bufferOut_));
+ buffer_out_));
}
bool
diff --git a/tests/tools/perfdhcp/pkt_transform.cc b/tests/tools/perfdhcp/pkt_transform.cc
index 15f15f1..0267d33 100644
--- a/tests/tools/perfdhcp/pkt_transform.cc
+++ b/tests/tools/perfdhcp/pkt_transform.cc
@@ -32,7 +32,7 @@ namespace perfdhcp {
bool
PktTransform::pack(const Option::Universe universe,
const OptionBuffer& in_buffer,
- const Option::OptionCollection& options,
+ const OptionCollection& options,
const size_t transid_offset,
const uint32_t transid,
util::OutputBuffer& out_buffer) {
@@ -75,7 +75,7 @@ PktTransform::pack(const Option::Universe universe,
bool
PktTransform::unpack(const Option::Universe universe,
const OptionBuffer& in_buffer,
- const Option::OptionCollection& options,
+ const OptionCollection& options,
const size_t transid_offset,
uint32_t& transid) {
@@ -113,13 +113,13 @@ PktTransform::unpack(const Option::Universe universe,
void
PktTransform::packOptions(const OptionBuffer& in_buffer,
- const Option::OptionCollection& options,
+ const OptionCollection& options,
util::OutputBuffer& out_buffer) {
try {
// If there are any options on the list, we will use provided
// options offsets to override them in the output buffer
// with new contents.
- for (Option::OptionCollection::const_iterator it = options.begin();
+ for (OptionCollection::const_iterator it = options.begin();
it != options.end(); ++it) {
// Get options with their position (offset).
boost::shared_ptr<LocalizedOption> option =
@@ -157,8 +157,8 @@ PktTransform::packOptions(const OptionBuffer& in_buffer,
void
PktTransform::unpackOptions(const OptionBuffer& in_buffer,
- const Option::OptionCollection& options) {
- for (Option::OptionCollection::const_iterator it = options.begin();
+ const OptionCollection& options) {
+ for (OptionCollection::const_iterator it = options.begin();
it != options.end(); ++it) {
boost::shared_ptr<LocalizedOption> option =
diff --git a/tests/tools/perfdhcp/pkt_transform.h b/tests/tools/perfdhcp/pkt_transform.h
index 51c1c0b..c8b7602 100644
--- a/tests/tools/perfdhcp/pkt_transform.h
+++ b/tests/tools/perfdhcp/pkt_transform.h
@@ -66,7 +66,7 @@ public:
/// \return false, if pack operation failed.
static bool pack(const dhcp::Option::Universe universe,
const dhcp::OptionBuffer& in_buffer,
- const dhcp::Option::OptionCollection& options,
+ const dhcp::OptionCollection& options,
const size_t transid_offset,
const uint32_t transid,
util::OutputBuffer& out_buffer);
@@ -88,7 +88,7 @@ public:
/// \return false, if unpack operation failed.
static bool unpack(const dhcp::Option::Universe universe,
const dhcp::OptionBuffer& in_buffer,
- const dhcp::Option::OptionCollection& options,
+ const dhcp::OptionCollection& options,
const size_t transid_offset,
uint32_t& transid);
@@ -135,7 +135,7 @@ private:
///
/// \throw isc::Unexpected if options update failed.
static void packOptions(const dhcp::OptionBuffer& in_buffer,
- const dhcp::Option::OptionCollection& options,
+ const dhcp::OptionCollection& options,
util::OutputBuffer& out_buffer);
/// \brief Reads contents of specified options from buffer.
@@ -159,7 +159,7 @@ private:
///
/// \throw isc::Unexpected if options unpack failed.
static void unpackOptions(const dhcp::OptionBuffer& in_buffer,
- const dhcp::Option::OptionCollection& options);
+ const dhcp::OptionCollection& options);
};
diff --git a/tests/tools/perfdhcp/rate_control.cc b/tests/tools/perfdhcp/rate_control.cc
new file mode 100644
index 0000000..1c2a600
--- /dev/null
+++ b/tests/tools/perfdhcp/rate_control.cc
@@ -0,0 +1,158 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include "rate_control.h"
+
+namespace isc {
+namespace perfdhcp {
+
+using namespace boost::posix_time;
+
+RateControl::RateControl()
+ : send_due_(currentTime()), last_sent_(currentTime()),
+ aggressivity_(1), rate_(0), late_sent_(false) {
+}
+
+RateControl::RateControl(const int rate, const int aggressivity)
+ : send_due_(currentTime()), last_sent_(currentTime()),
+ aggressivity_(aggressivity), rate_(rate), late_sent_(false) {
+ if (aggressivity_ < 1) {
+ isc_throw(isc::BadValue, "invalid value of aggressivity "
+ << aggressivity << ", expected value is greater than 0");
+ }
+ if (rate_ < 0) {
+ isc_throw(isc::BadValue, "invalid value of rate " << rate
+ << ", expected non-negative value");
+ }
+}
+
+uint64_t
+RateControl::getOutboundMessageCount() {
+
+ // We need calculate the due time for sending next set of messages.
+ updateSendDue();
+
+ // Get current time. If we are behind due time, we have to calculate
+ // how many messages to send to catch up with the rate.
+ ptime now = currentTime();
+ if (now >= send_due_) {
+ // Reset number of exchanges.
+ uint64_t due_exchanges = 0;
+ // If rate is specified from the command line we have to
+ // synchornize with it.
+ if (getRate() != 0) {
+ time_period period(send_due_, now);
+ time_duration duration = period.length();
+ // due_factor indicates the number of seconds that
+ // sending next chunk of packets will take.
+ double due_factor = duration.fractional_seconds() /
+ time_duration::ticks_per_second();
+ due_factor += duration.total_seconds();
+ // Multiplying due_factor by expected rate gives the number
+ // of exchanges to be initiated.
+ due_exchanges = static_cast<uint64_t>(due_factor * getRate());
+ // We want to make sure that at least one packet goes out.
+ if (due_exchanges == 0) {
+ due_exchanges = 1;
+ }
+ // We should not exceed aggressivity as it could have been
+ // restricted from command line.
+ if (due_exchanges > getAggressivity()) {
+ due_exchanges = getAggressivity();
+ }
+ } else {
+ // Rate is not specified so we rely on aggressivity
+ // which is the number of packets to be sent in
+ // one chunk.
+ due_exchanges = getAggressivity();
+ }
+ return (due_exchanges);
+ }
+ return (0);
+}
+
+boost::posix_time::ptime
+RateControl::currentTime() {
+ return (microsec_clock::universal_time());
+}
+
+void
+RateControl::updateSendDue() {
+ // There is no sense to update due time if the current due time is in the
+ // future. The due time is calculated as a duration between the moment
+ // when the last message of the given type was sent and the time when
+ // next one is supposed to be sent based on a given rate. The former value
+ // will not change until we send the next message, which we don't do
+ // until we reach the due time.
+ if (send_due_ > currentTime()) {
+ return;
+ }
+ // This is initialized in the class constructor, so if it is not initialized
+ // it is a programmatic error.
+ if (last_sent_.is_not_a_date_time()) {
+ isc_throw(isc::Unexpected, "timestamp of the last sent packet not"
+ " initialized");
+ }
+ // If rate was not specified we will wait just one clock tick to
+ // send next packet. This simulates best effort conditions.
+ long duration = 1;
+ if (getRate() != 0) {
+ // We use number of ticks instead of nanoseconds because
+ // nanosecond resolution may not be available on some
+ // machines. Number of ticks guarantees the highest possible
+ // timer resolution.
+ duration = time_duration::ticks_per_second() / getRate();
+ }
+ // Calculate due time to initiate next chunk of exchanges.
+ send_due_ = last_sent_ + time_duration(0, 0, 0, duration);
+ if (send_due_ > currentTime()) {
+ late_sent_ = true;
+ } else {
+ late_sent_ = false;
+ }
+}
+
+void
+RateControl::setAggressivity(const int aggressivity) {
+ if (aggressivity < 1) {
+ isc_throw(isc::BadValue, "invalid value of aggressivity "
+ << aggressivity << ", expected value is greater than 0");
+ }
+ aggressivity_ = aggressivity;
+}
+
+void
+RateControl::setRate(const int rate) {
+ if (rate < 0) {
+ isc_throw(isc::BadValue, "invalid value of rate " << rate
+ << ", expected non-negative value");
+ }
+ rate_ = rate;
+}
+
+void
+RateControl::setRelativeDue(const int offset) {
+ send_due_ = offset > 0 ?
+ currentTime() + seconds(abs(offset)) :
+ currentTime() - seconds(abs(offset));
+}
+
+void
+RateControl::updateSendTime() {
+ last_sent_ = currentTime();
+}
+
+} // namespace perfdhcp
+} // namespace isc
diff --git a/tests/tools/perfdhcp/rate_control.h b/tests/tools/perfdhcp/rate_control.h
new file mode 100644
index 0000000..cd49c2c
--- /dev/null
+++ b/tests/tools/perfdhcp/rate_control.h
@@ -0,0 +1,180 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef RATE_CONTROL_H
+#define RATE_CONTROL_H
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace isc {
+namespace perfdhcp {
+
+/// \brief A message sending rate control class for perfdhcp.
+///
+/// This class provides the means to control the rate at which messages
+/// of the specific type are sent by perfdhcp. Each message type,
+/// for which the desired rate can be specified, has a corresponding
+/// \c RateControl object. So, the perfdhcp is using up to three objects
+/// of this type at the same time, to control the rate of the following
+/// messages being sent:
+/// - Discover(DHCPv4) or Solicit (DHCPv6)
+/// - Renew (DHCPv6) or Request (DHCPv4) to renew leases.
+/// - Release
+///
+/// The purpose of the RateControl class is to track the due time for
+/// sending next message (or bunch of messages) to keep outbound rate
+/// of particular messages at the desired level. The due time is calculated
+/// using the desired rate value and the timestamp when the last message of
+/// the particular type has been sent. That puts the responsibility on the
+/// \c TestControl class to invoke the \c RateControl::updateSendDue, every
+/// time the message is sent.
+///
+/// The \c RateControl object returns the number of messages to be sent at
+/// the time. The number returned is 0, if perfdhcp shouldn't send any messages
+/// yet, or 1 (sometimes more) if the send due time has been reached.
+class RateControl {
+public:
+
+ /// \brief Default constructor.
+ RateControl();
+
+ /// \brief Constructor which sets desired rate and aggressivity.
+ ///
+ /// \param rate A desired rate.
+ /// \param aggressivity A desired aggressivity.
+ RateControl(const int rate, const int aggressivity);
+
+ /// \brief Returns the value of aggressivity.
+ int getAggressivity() const {
+ return (aggressivity_);
+ }
+
+ /// \brief Returns current due time to send next message.
+ boost::posix_time::ptime getDue() const {
+ return (send_due_);
+ }
+
+ /// \brief Returns number of messages to be sent "now".
+ ///
+ /// This function calculates how many messages of the given type should
+ /// be sent immediately when the call to the function returns, to catch
+ /// up with the desired message rate.
+ ///
+ /// The value returned depends on the due time calculated with the
+ /// \c RateControl::updateSendDue function and the current time. If
+ /// the due time has been hit, the non-zero number of messages is returned.
+ /// If the due time hasn't been hit, the number returned is 0.
+ ///
+ /// If the rate is non-zero, the number of messages to be sent is calculated
+ /// as follows:
+ /// \code
+ /// num = duration * rate
+ /// \endcode
+ /// where <b>duration</b> is a time period between the due time to send
+ /// next set of messages and current time. The duration is expressed in
+ /// seconds with the fractional part having 6 or 9 digits (depending on
+ /// the timer resolution). If the calculated value is equal to 0, it is
+ /// rounded to 1, so as at least one message is sent.
+ ///
+ /// The value of aggressivity limits the maximal number of messages to
+ /// be sent one after another. If the number of messages calculated with
+ /// the equation above exceeds the aggressivity, this function will return
+ /// the value equal to aggressivity.
+ ///
+ /// If the rate is not specified (equal to 0), the value calculated by
+ /// this function is equal to aggressivity.
+ ///
+ /// \return A number of messages to be sent immediately.
+ uint64_t getOutboundMessageCount();
+
+ /// \brief Returns the rate.
+ int getRate() const {
+ return (rate_);
+ }
+
+ /// \brief Returns the value of the late send flag.
+ ///
+ /// The flag returned by this function indicates whether the new due time
+ /// calculated by the \c RateControl::updateSendDue is in the past.
+ /// This value is used by the \c TestControl object to increment the counter
+ /// of the late sent messages in the \c StatsMgr.
+ bool isLateSent() const {
+ return (late_sent_);
+ }
+
+ /// \brief Sets the value of aggressivity.
+ ///
+ /// \param aggressivity A new value of aggressivity. This value must be
+ /// a positive integer.
+ /// \throw isc::BadValue if new value is not a positive integer.
+ void setAggressivity(const int aggressivity);
+
+ /// \brief Sets the new rate.
+ ///
+ /// \param rate A new value of rate. This value must not be negative.
+ /// \throw isc::BadValue if new rate is negative.
+ void setRate(const int rate);
+
+ /// \brief Sets the value of the due time.
+ ///
+ /// This function is intended for unit testing. It manipulates the value of
+ /// the due time. The parameter passed to this function specifies the
+ /// (positive or negative) number of seconds relative to current time.
+ ///
+ /// \param offset A number of seconds relative to current time which
+ /// constitutes the new due time.
+ void setRelativeDue(const int offset);
+
+ /// \brief Sets the timestamp of the last sent message to current time.
+ void updateSendTime();
+
+protected:
+
+ /// \brief Convenience function returning current time.
+ ///
+ /// \return current time.
+ static boost::posix_time::ptime currentTime();
+
+ /// \brief Calculates the send due.
+ ///
+ /// This function calculates the send due timestamp using the current time
+ /// and desired rate. The due timestamp is calculated as a sum of the
+ /// timestamp when the last message was sent and the reciprocal of the rate
+ /// in micro or nanoseconds (depending on the timer resolution). If the rate
+ /// is not specified, the duration between two consecutive sends is one
+ /// timer tick.
+ void updateSendDue();
+
+ /// \brief Holds a timestamp when the next message should be sent.
+ boost::posix_time::ptime send_due_;
+
+ /// \brief Holds a timestamp when the last message was sent.
+ boost::posix_time::ptime last_sent_;
+
+ /// \brief Holds an aggressivity value.
+ int aggressivity_;
+
+ /// \brief Holds a desired rate value.
+ int rate_;
+
+ /// \brief A flag which indicates that the calculated due time is in the
+ /// past.
+ bool late_sent_;
+
+};
+
+}
+}
+
+#endif
diff --git a/tests/tools/perfdhcp/stats_mgr.h b/tests/tools/perfdhcp/stats_mgr.h
index 6dbee25..7486b9e 100644
--- a/tests/tools/perfdhcp/stats_mgr.h
+++ b/tests/tools/perfdhcp/stats_mgr.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -15,8 +15,9 @@
#ifndef STATS_MGR_H
#define STATS_MGR_H
-#include <iostream>
-#include <map>
+#include <dhcp/pkt4.h>
+#include <dhcp/pkt6.h>
+#include <exceptions/exceptions.h>
#include <boost/noncopyable.hpp>
#include <boost/shared_ptr.hpp>
@@ -27,7 +28,9 @@
#include <boost/multi_index/mem_fun.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
-#include <exceptions/exceptions.h>
+#include <iostream>
+#include <map>
+
namespace isc {
namespace perfdhcp {
@@ -120,7 +123,9 @@ public:
XCHG_DO, ///< DHCPv4 DISCOVER-OFFER
XCHG_RA, ///< DHCPv4 REQUEST-ACK
XCHG_SA, ///< DHCPv6 SOLICIT-ADVERTISE
- XCHG_RR ///< DHCPv6 REQUEST-REPLY
+ XCHG_RR, ///< DHCPv6 REQUEST-REPLY
+ XCHG_RN, ///< DHCPv6 RENEW-REPLY
+ XCHG_RL ///< DHCPv6 RELEASE-REPLY
};
/// \brief Exchange Statistics.
@@ -260,7 +265,7 @@ public:
/// assumed dropped. Negative value disables it.
/// \param archive_enabled if true packets archive mode is enabled.
/// In this mode all packets are stored throughout the test execution.
- /// \param boot_time time when test was started
+ /// \param boot_time Holds the timestamp when perfdhcp has been started.
ExchangeStats(const ExchangeType xchg_type,
const double drop_time,
const bool archive_enabled,
@@ -456,9 +461,16 @@ public:
packet_period.length().total_seconds() +
(static_cast<double>(packet_period.length().fractional_seconds())
/ packet_period.length().ticks_per_second());
- if (drop_time_ > 0 &&
- (period_fractional > drop_time_)) {
- eraseSent(sent_packets_.template project<0>(it));
+ if (drop_time_ > 0 && (period_fractional > drop_time_)) {
+ // The packet pointed to by 'it' is timed out so we
+ // have to remove it. Removal may invalidate the
+ // next_sent_ pointer if it points to the packet
+ // being removed. So, we set the next_sent_ to point
+ // to the next packet after removed one. This
+ // pointer will be further updated in the following
+ // iterations, if the subsequent packets are also
+ // timed out.
+ next_sent_ = eraseSent(sent_packets_.template project<0>(it));
++collected_;
}
}
@@ -621,12 +633,19 @@ public:
/// Method prints main statistics for particular exchange.
/// Statistics includes: number of sent and received packets,
/// number of dropped packets and number of orphans.
+ ///
+ /// \todo Currently the number of orphans is not displayed because
+ /// Reply messages received for Renew and Releases are counted as
+ /// orphans for the 4-way exchanges, which is wrong. We will need to
+ /// move the orphans counting out of the Statistics Manager so as
+ /// orphans counter is increased only if the particular message is
+ /// not identified as a reponse to any of the messages sent by perfdhcp.
void printMainStats() const {
using namespace std;
cout << "sent packets: " << getSentPacketsNum() << endl
<< "received packets: " << getRcvdPacketsNum() << endl
- << "drops: " << getDroppedPacketsNum() << endl
- << "orphans: " << getOrphans() << endl;
+ << "drops: " << getDroppedPacketsNum() << endl;
+ // << "orphans: " << getOrphans() << endl;
}
/// \brief Print round trip time packets statistics.
@@ -860,6 +879,20 @@ public:
boot_time_));
}
+ /// \brief Check if the exchange type has been specified.
+ ///
+ /// This method checks if the \ref ExchangeStats object of a particular type
+ /// exists (has been added using \ref addExchangeStats function).
+ ///
+ /// \param xchg_type A type of the exchange being repersented by the
+ /// \ref ExchangeStats object.
+ ///
+ /// \return true if the \ref ExchangeStats object has been added for a
+ /// specified exchange type.
+ bool hasExchangeStats(const ExchangeType xchg_type) const {
+ return (exchanges_.find(xchg_type) != exchanges_.end());
+ }
+
/// \brief Add named custom uint64 counter.
///
/// Method creates new named counter and stores in counter's map under
@@ -1148,7 +1181,7 @@ public:
///
/// \param xchg_type exchange type.
/// \return string representing name of the exchange.
- std::string exchangeToString(ExchangeType xchg_type) const {
+ static std::string exchangeToString(ExchangeType xchg_type) {
switch(xchg_type) {
case XCHG_DO:
return("DISCOVER-OFFER");
@@ -1158,6 +1191,10 @@ public:
return("SOLICIT-ADVERTISE");
case XCHG_RR:
return("REQUEST-REPLY");
+ case XCHG_RN:
+ return("RENEW-REPLY");
+ case XCHG_RL:
+ return("RELEASE-REPLY");
default:
return("Unknown exchange type");
}
@@ -1177,7 +1214,7 @@ public:
/// \throw isc::InvalidOperation if no exchange type added to
/// track statistics.
void printStats() const {
- if (exchanges_.size() == 0) {
+ if (exchanges_.empty()) {
isc_throw(isc::InvalidOperation,
"no exchange type added for tracking");
}
@@ -1232,7 +1269,7 @@ public:
/// \throw isc::InvalidOperation if no exchange type added to
/// track statistics or packets archive mode is disabled.
void printTimestamps() const {
- if (exchanges_.size() == 0) {
+ if (exchanges_.empty()) {
isc_throw(isc::InvalidOperation,
"no exchange type added for tracking");
}
@@ -1255,7 +1292,7 @@ public:
///
/// \throw isc::InvalidOperation if no custom counters added for tracking.
void printCustomCounters() const {
- if (custom_counters_.size() == 0) {
+ if (custom_counters_.empty()) {
isc_throw(isc::InvalidOperation, "no custom counters specified");
}
for (CustomCountersMapIterator it = custom_counters_.begin();
diff --git a/tests/tools/perfdhcp/test_control.cc b/tests/tools/perfdhcp/test_control.cc
index 925e9b6..3751e2f 100644
--- a/tests/tools/perfdhcp/test_control.cc
+++ b/tests/tools/perfdhcp/test_control.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -47,7 +47,7 @@ namespace perfdhcp {
bool TestControl::interrupted_ = false;
TestControl::TestControlSocket::TestControlSocket(const int socket) :
- SocketInfo(socket, asiolink::IOAddress("127.0.0.1"), 0),
+ SocketInfo(asiolink::IOAddress("127.0.0.1"), 0, socket),
ifindex_(0), valid_(true) {
try {
initSocketData();
@@ -97,6 +97,83 @@ TestControl::TestControl() {
reset();
}
+void
+TestControl::checkLateMessages(RateControl& rate_control) {
+ // If diagnostics is disabled, there is no need to log late sent messages.
+ // If it is enabled and the rate control object indicates that the last
+ // sent message was late, bump up the counter in Stats Manager.
+ if (rate_control.isLateSent() && testDiags('i')) {
+ CommandOptions& options = CommandOptions::instance();
+ if (options.getIpVersion() == 4) {
+ stats_mgr4_->incrementCounter("latesend");
+ } else if (options.getIpVersion() == 6) {
+ stats_mgr6_->incrementCounter("latesend");
+ }
+ }
+}
+
+void
+TestControl::cleanCachedPackets() {
+ CommandOptions& options = CommandOptions::instance();
+ // When Renews are not sent, Reply packets are not cached so there
+ // is nothing to do.
+ if (options.getRenewRate() == 0) {
+ return;
+ }
+
+ static boost::posix_time::ptime last_clean =
+ microsec_clock::universal_time();
+
+ // Check how much time has passed since last cleanup.
+ time_period time_since_clean(last_clean,
+ microsec_clock::universal_time());
+ // Cleanup every 1 second.
+ if (time_since_clean.length().total_seconds() >= 1) {
+ // Calculate how many cached packets to remove. Actually we could
+ // just leave enough packets to handle Renews for 1 second but
+ // since we want to randomize leases to be renewed so leave 5
+ // times more packets to randomize from.
+ // @todo The cache size might be controlled from the command line.
+ if (reply_storage_.size() > 5 * options.getRenewRate()) {
+ reply_storage_.clear(reply_storage_.size() -
+ 5 * options.getRenewRate());
+ }
+ // Remember when we performed a cleanup for the last time.
+ // We want to do the next cleanup not earlier than in one second.
+ last_clean = microsec_clock::universal_time();
+ }
+}
+
+void
+TestControl::copyIaOptions(const Pkt6Ptr& pkt_from, Pkt6Ptr& pkt_to) {
+ if (!pkt_from || !pkt_to) {
+ isc_throw(BadValue, "NULL pointers must not be specified as arguments"
+ " for the copyIaOptions function");
+ }
+ // IA_NA
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::ADDRESS)) {
+ OptionPtr option = pkt_from->getOption(D6O_IA_NA);
+ if (!option) {
+ isc_throw(OptionNotFound, "IA_NA option not found in the"
+ " server's response");
+ }
+ pkt_to->addOption(option);
+ }
+ // IA_PD
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::PREFIX)) {
+ OptionPtr option = pkt_from->getOption(D6O_IA_PD);
+ if (!option) {
+ isc_throw(OptionNotFound, "IA_PD option not found in the"
+ " server's response");
+ }
+ pkt_to->addOption(option);
+ }
+
+
+}
+
std::string
TestControl::byte2Hex(const uint8_t b) const {
const int b1 = b / 16;
@@ -253,6 +330,46 @@ TestControl::checkExitConditions() const {
return (false);
}
+Pkt6Ptr
+TestControl::createMessageFromReply(const uint16_t msg_type,
+ const dhcp::Pkt6Ptr& reply) {
+ // Restrict messages to Release and Renew.
+ if (msg_type != DHCPV6_RENEW && msg_type != DHCPV6_RELEASE) {
+ isc_throw(isc::BadValue, "invalid message type " << msg_type
+ << " to be created from Reply, expected DHCPV6_RENEW or"
+ " DHCPV6_RELEASE");
+ }
+ // Get the string representation of the message - to be used for error
+ // logging purposes.
+ const char* msg_type_str = (msg_type == DHCPV6_RENEW ? "Renew" : "Release");
+ // Reply message must be specified.
+ if (!reply) {
+ isc_throw(isc::BadValue, "Unable to create " << msg_type_str
+ << " message from the Reply message because the instance of"
+ " the Reply message is NULL");
+ }
+
+ Pkt6Ptr msg(new Pkt6(msg_type, generateTransid()));
+ // Client id.
+ OptionPtr opt_clientid = reply->getOption(D6O_CLIENTID);
+ if (!opt_clientid) {
+ isc_throw(isc::Unexpected, "failed to create " << msg_type_str
+ << " message because client id option has not been found"
+ " in the Reply message");
+ }
+ msg->addOption(opt_clientid);
+ // Server id.
+ OptionPtr opt_serverid = reply->getOption(D6O_SERVERID);
+ if (!opt_serverid) {
+ isc_throw(isc::Unexpected, "failed to create " << msg_type_str
+ << " because server id option has not been found in the"
+ " Reply message");
+ }
+ msg->addOption(opt_serverid);
+ copyIaOptions(reply, msg);
+ return (msg);
+}
+
OptionPtr
TestControl::factoryElapsedTime6(Option::Universe, uint16_t,
const OptionBuffer& buf) {
@@ -290,6 +407,22 @@ TestControl::factoryIana6(Option::Universe, uint16_t,
}
OptionPtr
+TestControl::factoryIapd6(Option::Universe, uint16_t,
+ const OptionBuffer& buf) {
+ // @todo allow different values of T1, T2 and IAID.
+ static const uint8_t buf_array[] = {
+ 0, 0, 0, 1, // IAID = 1
+ 0, 0, 3600 >> 8, 3600 & 0xff, // T1 = 3600
+ 0, 0, 5400 >> 8, 5400 & 0xff, // T2 = 5400
+ };
+ OptionBuffer buf_ia_pd(buf_array, buf_array + sizeof(buf_array));
+ // Append sub-options to IA_PD.
+ buf_ia_pd.insert(buf_ia_pd.end(), buf.begin(), buf.end());
+ return (OptionPtr(new Option(Option::V6, D6O_IA_PD, buf_ia_pd)));
+}
+
+
+OptionPtr
TestControl::factoryRapidCommit6(Option::Universe, uint16_t,
const OptionBuffer&) {
return (OptionPtr(new Option(Option::V6, D6O_RAPID_COMMIT, OptionBuffer())));
@@ -380,6 +513,36 @@ TestControl::generateDuid(uint8_t& randomized) const {
return (duid);
}
+uint32_t
+TestControl::getCurrentTimeout() const {
+ CommandOptions& options = CommandOptions::instance();
+ ptime now(microsec_clock::universal_time());
+ // Check that we haven't passed the moment to send the next set of
+ // packets.
+ if (now >= basic_rate_control_.getDue() ||
+ (options.getRenewRate() != 0 && now >= renew_rate_control_.getDue()) ||
+ (options.getReleaseRate() != 0 &&
+ now >= release_rate_control_.getDue())) {
+ return (0);
+ }
+
+ // Let's assume that the due time for Solicit is the soonest.
+ ptime due = basic_rate_control_.getDue();
+ // If we are sending Renews and due time for Renew occurs sooner,
+ // set the due time to Renew due time.
+ if ((options.getRenewRate()) != 0 && (renew_rate_control_.getDue() < due)) {
+ due = renew_rate_control_.getDue();
+ }
+ // If we are sending Releases and the due time for Release occurs
+ // sooner than the current due time, let's use the due for Releases.
+ if ((options.getReleaseRate() != 0) &&
+ (release_rate_control_.getDue() < due)) {
+ due = release_rate_control_.getDue();
+ }
+ // Return the timeout in microseconds.
+ return (time_period(now, due).length().total_microseconds());
+}
+
int
TestControl::getElapsedTimeOffset() const {
int elp_offset = CommandOptions::instance().getIpVersion() == 4 ?
@@ -408,48 +571,6 @@ TestControl::getElapsedTime(const T& pkt1, const T& pkt2) {
return(elapsed_period.length().total_milliseconds());
}
-
-uint64_t
-TestControl::getNextExchangesNum() const {
- CommandOptions& options = CommandOptions::instance();
- // Reset number of exchanges.
- uint64_t due_exchanges = 0;
- // Get current time.
- ptime now(microsec_clock::universal_time());
- if (now >= send_due_) {
- // If rate is specified from the command line we have to
- // synchornize with it.
- if (options.getRate() != 0) {
- time_period period(send_due_, now);
- time_duration duration = period.length();
- // due_factor indicates the number of seconds that
- // sending next chunk of packets will take.
- double due_factor = duration.fractional_seconds() /
- time_duration::ticks_per_second();
- due_factor += duration.total_seconds();
- // Multiplying due_factor by expected rate gives the number
- // of exchanges to be initiated.
- due_exchanges = static_cast<uint64_t>(due_factor * options.getRate());
- // We want to make sure that at least one packet goes out.
- if (due_exchanges == 0) {
- due_exchanges = 1;
- }
- // We should not exceed aggressivity as it could have been
- // restricted from command line.
- if (due_exchanges > options.getAggressivity()) {
- due_exchanges = options.getAggressivity();
- }
- } else {
- // Rate is not specified so we rely on aggressivity
- // which is the number of packets to be sent in
- // one chunk.
- due_exchanges = options.getAggressivity();
- }
- return (due_exchanges);
- }
- return (0);
-}
-
int
TestControl::getRandomOffset(const int arg_idx) const {
int rand_offset = CommandOptions::instance().getIpVersion() == 4 ?
@@ -570,6 +691,12 @@ TestControl::initializeStatsMgr() {
stats_mgr6_->addExchangeStats(StatsMgr6::XCHG_RR,
options.getDropTime()[1]);
}
+ if (options.getRenewRate() != 0) {
+ stats_mgr6_->addExchangeStats(StatsMgr6::XCHG_RN);
+ }
+ if (options.getReleaseRate() != 0) {
+ stats_mgr6_->addExchangeStats(StatsMgr6::XCHG_RL);
+ }
}
if (testDiags('i')) {
if (options.getIpVersion() == 4) {
@@ -594,15 +721,15 @@ TestControl::openSocket() const {
uint16_t port = options.getLocalPort();
int sock = 0;
- uint8_t family = (options.getIpVersion() == 6) ? AF_INET6 : AF_INET;
+ uint8_t family = (options.getIpVersion() == 6) ? AF_INET6 : AF_INET;
IOAddress remoteaddr(servername);
-
+
// Check for mismatch between IP option and server address
if (family != remoteaddr.getFamily()) {
- isc_throw(InvalidParameter,
- "Values for IP version: " <<
+ isc_throw(InvalidParameter,
+ "Values for IP version: " <<
static_cast<unsigned int>(options.getIpVersion()) <<
- " and server address: " << servername << " are mismatched.");
+ " and server address: " << servername << " are mismatched.");
}
if (port == 0) {
@@ -691,7 +818,7 @@ TestControl::sendPackets(const TestControlSocket& socket,
if (options.getIpVersion() == 4) {
// No template packets means that no -T option was specified.
// We have to build packets ourselfs.
- if (template_buffers_.size() == 0) {
+ if (template_buffers_.empty()) {
sendDiscover4(socket, preload);
} else {
// @todo add defines for packet type index that can be
@@ -701,7 +828,7 @@ TestControl::sendPackets(const TestControlSocket& socket,
} else {
// No template packets means that no -T option was specified.
// We have to build packets ourselfs.
- if (template_buffers_.size() == 0) {
+ if (template_buffers_.empty()) {
sendSolicit6(socket, preload);
} else {
// @todo add defines for packet type index that can be
@@ -723,6 +850,18 @@ TestControl::sendPackets(const TestControlSocket& socket,
}
}
+uint64_t
+TestControl::sendMultipleMessages6(const TestControlSocket& socket,
+ const uint32_t msg_type,
+ const uint64_t msg_num) {
+ for (uint64_t i = 0; i < msg_num; ++i) {
+ if (!sendMessageFromReply(msg_type, socket)) {
+ return (i);
+ }
+ }
+ return (msg_num);
+}
+
void
TestControl::printDiagnostics() const {
CommandOptions& options = CommandOptions::instance();
@@ -920,7 +1059,7 @@ TestControl::readPacketTemplate(const std::string& file_name) {
// Expect even number of digits.
if (hex_digits.size() % 2 != 0) {
isc_throw(OutOfRange, "odd number of digits in template file");
- } else if (hex_digits.size() == 0) {
+ } else if (hex_digits.empty()) {
isc_throw(OutOfRange, "template file " << file_name << " is empty");
}
std::vector<uint8_t> binary_stream;
@@ -978,20 +1117,49 @@ TestControl::processReceivedPacket6(const TestControlSocket& socket,
}
}
} else if (packet_type == DHCPV6_REPLY) {
- stats_mgr6_->passRcvdPacket(StatsMgr6::XCHG_RR, pkt6);
+ // If the received message is Reply, we have to find out which exchange
+ // type the Reply message belongs to. It is doable by matching the Reply
+ // transaction id with the transaction id of the sent Request, Renew
+ // or Release. First we start with the Request.
+ if (stats_mgr6_->passRcvdPacket(StatsMgr6::XCHG_RR, pkt6)) {
+ // The Reply belongs to Request-Reply exchange type. So, we may need
+ // to keep this Reply in the storage if Renews or/and Releases are
+ // being sent. Note that, Reply messages hold the information about
+ // leases assigned. We use this information to construct Renew and
+ // Release messages.
+ if (stats_mgr6_->hasExchangeStats(StatsMgr6::XCHG_RN) ||
+ stats_mgr6_->hasExchangeStats(StatsMgr6::XCHG_RL)) {
+ // Renew or Release messages are sent, because StatsMgr has the
+ // specific exchange type specified. Let's append the Reply
+ // message to a storage.
+ reply_storage_.append(pkt6);
+ }
+ // The Reply message is not a server's response to the Request message
+ // sent within the 4-way exchange. It may be a response to the Renew
+ // or Release message. In the if clause we first check if StatsMgr
+ // has exchange type for Renew specified, and if it has, if there is
+ // a corresponding Renew message for the received Reply. If not,
+ // we check that StatsMgr has exchange type for Release specified,
+ // as possibly the Reply has been sent in response to Release.
+ } else if (!(stats_mgr6_->hasExchangeStats(StatsMgr6::XCHG_RN) &&
+ stats_mgr6_->passRcvdPacket(StatsMgr6::XCHG_RN, pkt6)) &&
+ stats_mgr6_->hasExchangeStats(StatsMgr6::XCHG_RL)) {
+ // At this point, it is only possible that the Reply has been sent
+ // in response to a Release. Try to match the Reply with Release.
+ stats_mgr6_->passRcvdPacket(StatsMgr6::XCHG_RL, pkt6);
+ }
}
}
uint64_t
TestControl::receivePackets(const TestControlSocket& socket) {
- int timeout = 0;
bool receiving = true;
uint64_t received = 0;
while (receiving) {
if (CommandOptions::instance().getIpVersion() == 4) {
Pkt4Ptr pkt4;
try {
- pkt4 = IfaceMgr::instance().receive4(timeout);
+ pkt4 = IfaceMgr::instance().receive4(0, getCurrentTimeout());
} catch (const Exception& e) {
std::cerr << "Failed to receive DHCPv4 packet: "
<< e.what() << std::endl;
@@ -1009,7 +1177,7 @@ TestControl::receivePackets(const TestControlSocket& socket) {
} else if (CommandOptions::instance().getIpVersion() == 6) {
Pkt6Ptr pkt6;
try {
- pkt6 = IfaceMgr::instance().receive6(timeout);
+ pkt6 = IfaceMgr::instance().receive6(0, getCurrentTimeout());
} catch (const Exception& e) {
std::cerr << "Failed to receive DHCPv6 packet: "
<< e.what() << std::endl;
@@ -1079,6 +1247,11 @@ TestControl::registerOptionFactories6() const {
D6O_IA_NA,
&TestControl::factoryIana6);
+ // D6O_IA_PD option factory.
+ LibDHCP::OptionFactoryRegister(Option::V6,
+ D6O_IA_PD,
+ &TestControl::factoryIapd6);
+
}
factories_registered = true;
@@ -1102,10 +1275,16 @@ TestControl::registerOptionFactories() const {
void
TestControl::reset() {
- send_due_ = microsec_clock::universal_time();
- last_sent_ = send_due_;
- last_report_ = send_due_;
+ CommandOptions& options = CommandOptions::instance();
+ basic_rate_control_.setAggressivity(options.getAggressivity());
+ basic_rate_control_.setRate(options.getRate());
+ renew_rate_control_.setAggressivity(options.getAggressivity());
+ renew_rate_control_.setRate(options.getRenewRate());
+ release_rate_control_.setAggressivity(options.getAggressivity());
+ release_rate_control_.setRate(options.getReleaseRate());
+
transid_gen_.reset();
+ last_report_ = microsec_clock::universal_time();
// Actual generators will have to be set later on because we need to
// get command line parameters first.
setTransidGenerator(NumberGeneratorPtr());
@@ -1171,11 +1350,10 @@ TestControl::run() {
// Initialize Statistics Manager. Release previous if any.
initializeStatsMgr();
for (;;) {
- // Calculate send due based on when last exchange was initiated.
- updateSendDue();
// Calculate number of packets to be sent to stay
// catch up with rate.
- uint64_t packets_due = getNextExchangesNum();
+ uint64_t packets_due = basic_rate_control_.getOutboundMessageCount();
+ checkLateMessages(basic_rate_control_);
if ((packets_due == 0) && testDiags('i')) {
if (options.getIpVersion() == 4) {
stats_mgr4_->incrementCounter("shortwait");
@@ -1198,11 +1376,39 @@ TestControl::run() {
// Initiate new DHCP packet exchanges.
sendPackets(socket, packets_due);
+ // If -f<renew-rate> option was specified we have to check how many
+ // Renew packets should be sent to catch up with a desired rate.
+ if ((options.getIpVersion() == 6) && (options.getRenewRate() != 0)) {
+ uint64_t renew_packets_due =
+ renew_rate_control_.getOutboundMessageCount();
+ checkLateMessages(renew_rate_control_);
+ // Send Renew messages.
+ sendMultipleMessages6(socket, DHCPV6_RENEW, renew_packets_due);
+ }
+
+ // If -F<release-rate> option was specified we have to check how many
+ // Release messages should be sent to catch up with a desired rate.
+ if ((options.getIpVersion() == 6) && (options.getReleaseRate() != 0)) {
+ uint64_t release_packets_due =
+ release_rate_control_.getOutboundMessageCount();
+ checkLateMessages(release_rate_control_);
+ // Send Release messages.
+ sendMultipleMessages6(socket, DHCPV6_RELEASE, release_packets_due);
+ }
+
// Report delay means that user requested printing number
// of sent/received/dropped packets repeatedly.
if (options.getReportDelay() > 0) {
printIntermediateStats();
}
+
+ // If we are sending Renews to the server, the Reply packets are cached
+ // so as leases for which we send Renews can be idenitfied. The major
+ // issue with this approach is that most of the time we are caching
+ // more packets than we actually need. This function removes excessive
+ // Reply messages to reduce the memory and CPU utilization. Note that
+ // searches in the long list of Reply packets increases CPU utilization.
+ cleanCachedPackets();
}
printStats();
@@ -1283,7 +1489,7 @@ TestControl::saveFirstPacket(const Pkt6Ptr& pkt) {
void
TestControl::sendDiscover4(const TestControlSocket& socket,
const bool preload /*= false*/) {
- last_sent_ = microsec_clock::universal_time();
+ basic_rate_control_.updateSendTime();
// Generate the MAC address to be passed in the packet.
uint8_t randomized = 0;
std::vector<uint8_t> mac_address = generateMacAddress(randomized);
@@ -1328,9 +1534,7 @@ void
TestControl::sendDiscover4(const TestControlSocket& socket,
const std::vector<uint8_t>& template_buf,
const bool preload /* = false */) {
- // last_sent_ has to be updated for each function that initiates
- // new transaction. The packet exchange synchronization relies on this.
- last_sent_ = microsec_clock::universal_time();
+ basic_rate_control_.updateSendTime();
// Get the first argument if mulitple the same arguments specified
// in the command line. First one refers to DISCOVER packets.
const uint8_t arg_idx = 0;
@@ -1378,6 +1582,39 @@ TestControl::sendDiscover4(const TestControlSocket& socket,
saveFirstPacket(pkt4);
}
+bool
+TestControl::sendMessageFromReply(const uint16_t msg_type,
+ const TestControlSocket& socket) {
+ // We only permit Release or Renew messages to be sent using this function.
+ if (msg_type != DHCPV6_RENEW && msg_type != DHCPV6_RELEASE) {
+ isc_throw(isc::BadValue, "invalid message type " << msg_type
+ << " to be sent, expected DHCPV6_RENEW or DHCPV6_RELEASE");
+ }
+ // We track the timestamp of last Release and Renew in different variables.
+ if (msg_type == DHCPV6_RENEW) {
+ renew_rate_control_.updateSendTime();
+ } else {
+ release_rate_control_.updateSendTime();
+ }
+ Pkt6Ptr reply = reply_storage_.getRandom();
+ if (!reply) {
+ return (false);
+ }
+ // Prepare the message of the specified type.
+ Pkt6Ptr msg = createMessageFromReply(msg_type, reply);
+ setDefaults6(socket, msg);
+ msg->pack();
+ // And send it.
+ IfaceMgr::instance().send(msg);
+ if (!stats_mgr6_) {
+ isc_throw(Unexpected, "Statistics Manager for DHCPv6 "
+ "hasn't been initialized");
+ }
+ stats_mgr6_->passSentPacket((msg_type == DHCPV6_RENEW ? StatsMgr6::XCHG_RN
+ : StatsMgr6::XCHG_RL), msg);
+ return (true);
+}
+
void
TestControl::sendRequest4(const TestControlSocket& socket,
const dhcp::Pkt4Ptr& discover_pkt4,
@@ -1577,13 +1814,13 @@ TestControl::sendRequest6(const TestControlSocket& socket,
}
pkt6->addOption(opt_serverid);
}
- // Set IA_NA option.
- OptionPtr opt_ia_na = advertise_pkt6->getOption(D6O_IA_NA);
- if (!opt_ia_na) {
- isc_throw(Unexpected, "DHCPv6 IA_NA option not found in received "
- "packet");
- }
- pkt6->addOption(opt_ia_na);
+
+ // Copy IA_NA or IA_PD option from the Advertise message to the Request
+ // message being sent to the server. This will throw exception if the
+ // option to be copied is not found. Note that this function will copy
+ // one of IA_NA or IA_PD options, depending on the lease-type value
+ // specified in the command line.
+ copyIaOptions(advertise_pkt6, pkt6);
// Set default packet data.
setDefaults6(socket, pkt6);
@@ -1716,7 +1953,7 @@ TestControl::sendRequest6(const TestControlSocket& socket,
void
TestControl::sendSolicit6(const TestControlSocket& socket,
const bool preload /*= false*/) {
- last_sent_ = microsec_clock::universal_time();
+ basic_rate_control_.updateSendTime();
// Generate DUID to be passed to the packet
uint8_t randomized = 0;
std::vector<uint8_t> duid = generateDuid(randomized);
@@ -1732,7 +1969,20 @@ TestControl::sendSolicit6(const TestControlSocket& socket,
}
pkt6->addOption(Option::factory(Option::V6, D6O_CLIENTID, duid));
pkt6->addOption(Option::factory(Option::V6, D6O_ORO));
- pkt6->addOption(Option::factory(Option::V6, D6O_IA_NA));
+
+ // Depending on the lease-type option specified, we should request
+ // IPv6 address (with IA_NA) or IPv6 prefix (IA_PD) or both.
+
+ // IA_NA
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::ADDRESS)) {
+ pkt6->addOption(Option::factory(Option::V6, D6O_IA_NA));
+ }
+ // IA_PD
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::PREFIX)) {
+ pkt6->addOption(Option::factory(Option::V6, D6O_IA_PD));
+ }
setDefaults6(socket, pkt6);
pkt6->pack();
@@ -1752,7 +2002,7 @@ void
TestControl::sendSolicit6(const TestControlSocket& socket,
const std::vector<uint8_t>& template_buf,
const bool preload /*= false*/) {
- last_sent_ = microsec_clock::universal_time();
+ basic_rate_control_.updateSendTime();
const int arg_idx = 0;
// Get transaction id offset.
size_t transid_offset = getTransactionIdOffset(arg_idx);
@@ -1851,46 +2101,5 @@ TestControl::testDiags(const char diag) const {
return (false);
}
-void
-TestControl::updateSendDue() {
- // If default constructor was called, this should not happen but
- // if somebody has changed default constructor it is better to
- // keep this check.
- if (last_sent_.is_not_a_date_time()) {
- isc_throw(Unexpected, "time of last sent packet not initialized");
- }
- // Get the expected exchange rate.
- CommandOptions& options = CommandOptions::instance();
- int rate = options.getRate();
- // If rate was not specified we will wait just one clock tick to
- // send next packet. This simulates best effort conditions.
- long duration = 1;
- if (rate != 0) {
- // We use number of ticks instead of nanoseconds because
- // nanosecond resolution may not be available on some
- // machines. Number of ticks guarantees the highest possible
- // timer resolution.
- duration = time_duration::ticks_per_second() / rate;
- }
- // Calculate due time to initiate next chunk of exchanges.
- send_due_ = last_sent_ + time_duration(0, 0, 0, duration);
- // Check if it is already due.
- ptime now(microsec_clock::universal_time());
- // \todo verify if this condition is not too tight. In other words
- // verify if this will not produce too many late sends.
- // We might want to look at this once we are done implementing
- // microsecond timeouts in IfaceMgr.
- if (now > send_due_) {
- if (testDiags('i')) {
- if (options.getIpVersion() == 4) {
- stats_mgr4_->incrementCounter("latesend");
- } else if (options.getIpVersion() == 6) {
- stats_mgr6_->incrementCounter("latesend");
- }
- }
- }
-}
-
-
} // namespace perfdhcp
} // namespace isc
diff --git a/tests/tools/perfdhcp/test_control.h b/tests/tools/perfdhcp/test_control.h
index 3983fa6..5a7ef48 100644
--- a/tests/tools/perfdhcp/test_control.h
+++ b/tests/tools/perfdhcp/test_control.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -15,20 +15,22 @@
#ifndef TEST_CONTROL_H
#define TEST_CONTROL_H
-#include <string>
-#include <vector>
-
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-#include <boost/function.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
+#include "packet_storage.h"
+#include "rate_control.h"
+#include "stats_mgr.h"
#include <dhcp/iface_mgr.h>
#include <dhcp/dhcp6.h>
#include <dhcp/pkt4.h>
#include <dhcp/pkt6.h>
-#include "stats_mgr.h"
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/function.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+#include <string>
+#include <vector>
namespace isc {
namespace perfdhcp {
@@ -55,6 +57,13 @@ static const size_t DHCPV6_SERVERID_OFFSET = 22;
/// Default DHCPV6 IA_NA offset in the packet template.
static const size_t DHCPV6_IA_NA_OFFSET = 40;
+/// @brief Exception thrown when the required option is not found in a packet.
+class OptionNotFound : public Exception {
+public:
+ OptionNotFound(const char* file, size_t line, const char* what) :
+ isc::Exception(file, line, what) { };
+};
+
/// \brief Test Control class.
///
/// This singleton class is used to run the performance test with
@@ -160,7 +169,7 @@ public:
/// \param socket socket descriptor.
TestControlSocket(const int socket);
- /// \brief Destriuctor of the socket wrapper class.
+ /// \brief Destructor of the socket wrapper class.
///
/// Destructor closes wrapped socket.
~TestControlSocket();
@@ -197,7 +206,7 @@ public:
/// The default generator pointer.
typedef boost::shared_ptr<NumberGenerator> NumberGeneratorPtr;
- /// \brief Sequential numbers generatorc class.
+ /// \brief Sequential numbers generator class.
class SequentialGenerator : public NumberGenerator {
public:
/// \brief Constructor.
@@ -213,7 +222,7 @@ public:
}
}
- /// \brief Generate number sequentialy.
+ /// \brief Generate number sequentially.
///
/// \return generated number.
virtual uint32_t generate() {
@@ -241,7 +250,7 @@ public:
/// brief\ Run performance test.
///
/// Method runs whole performance test. Command line options must
- /// be parsed prior to running this function. Othewise function will
+ /// be parsed prior to running this function. Otherwise function will
/// throw exception.
///
/// \throw isc::InvalidOperation if command line options are not parsed.
@@ -280,7 +289,7 @@ protected:
/// only via \ref instance method.
TestControl();
- /// \brief Check if test exit condtitions fulfilled.
+ /// \brief Check if test exit conditions fulfilled.
///
/// Method checks if the test exit conditions are fulfilled.
/// Exit conditions are checked periodically from the
@@ -292,6 +301,37 @@ protected:
/// \return true if any of the exit conditions is fulfilled.
bool checkExitConditions() const;
+ /// \brief Removes cached DHCPv6 Reply packets every second.
+ ///
+ /// This function wipes cached Reply packets from the storage.
+ /// The number of packets left in the storage after the call
+ /// to this function should guarantee that the Renew packets
+ /// can be sent at the given rate. Note that the Renew packets
+ /// are generated for the existing leases, represented here as
+ /// replies from the server.
+ /// @todo Instead of cleaning packets periodically we could
+ /// just stop adding new packets when the certain threshold
+ /// has been reached.
+ void cleanCachedPackets();
+
+ /// \brief Creates DHCPv6 message from the Reply packet.
+ ///
+ /// This function creates DHCPv6 Renew or Release message using the
+ /// data from the Reply message by copying options from the Reply
+ /// message.
+ ///
+ /// \param msg_type A type of the message to be createad.
+ /// \param reply An instance of the Reply packet which contents should
+ /// be used to create an instance of the new message.
+ ///
+ /// \return created Release or Renew message
+ /// \throw isc::BadValue if the msg_type is neither DHCPV6_RENEW nor
+ /// DHCPV6_RELEASE or if the reply is NULL.
+ /// \throw isc::Unexpected if mandatory options are missing in the
+ /// Reply message.
+ dhcp::Pkt6Ptr createMessageFromReply(const uint16_t msg_type,
+ const dhcp::Pkt6Ptr& reply);
+
/// \brief Factory function to create DHCPv6 ELAPSED_TIME option.
///
/// This factory function creates DHCPv6 ELAPSED_TIME option instance.
@@ -338,6 +378,17 @@ protected:
uint16_t type,
const dhcp::OptionBuffer& buf);
+ /// \brief Factory function to create IA_PD option.
+ ///
+ /// this factory function creates DHCPv6 IA_PD option instance.
+ ///
+ /// \param u universe (ignored).
+ /// \param type option-type (ignored).
+ /// \param buf option-buffer carrying sub-options.
+ static dhcp::OptionPtr factoryIapd6(dhcp::Option::Universe u,
+ uint16_t type,
+ const dhcp::OptionBuffer& buf);
+
/// \brief Factory function to create DHCPv6 ORO option.
///
/// This factory function creates DHCPv6 Option Request Option instance.
@@ -371,7 +422,7 @@ protected:
/// \brief Factory function to create DHCPv4 Request List option.
///
- /// This factory function creayes DHCPv4 PARAMETER_REQUEST_LIST option
+ /// This factory function creates DHCPv4 PARAMETER_REQUEST_LIST option
/// instance with the following set of requested options:
/// - DHO_SUBNET_MASK,
/// - DHO_BROADCAST_ADDRESS,
@@ -432,15 +483,14 @@ protected:
return (transid_gen_->generate());
}
- /// \brief Returns number of exchanges to be started.
+ /// \brief Returns a timeout for packet reception.
///
- /// Method returns number of new exchanges to be started as soon
- /// as possible to satisfy expected rate. Calculation used here
- /// is based on current time, due time calculated with
- /// \ref updateSendDue function and expected rate.
+ /// The calculation is based on the value of the timestamp
+ /// when the next set of packets is to be sent. If no packet is
+ /// received until then, new packets are sent.
///
- /// \return number of exchanges to be started immediately.
- uint64_t getNextExchangesNum() const;
+ /// \return A current timeout in microseconds.
+ uint32_t getCurrentTimeout() const;
/// \brief Return template buffer.
///
@@ -527,7 +577,7 @@ protected:
/// \brief Process received DHCPv6 packet.
///
/// Method performs processing of the received DHCPv6 packet,
- /// updates statistics and responsds to the server if required,
+ /// updates statistics and responds to the server if required,
/// e.g. when ADVERTISE packet arrives, this function will initiate
/// REQUEST message to the server.
///
@@ -578,7 +628,7 @@ protected:
/// \brief Register option factory functions for DHCPv4 or DHCPv6.
///
/// Method registers option factory functions for DHCPv4 or DHCPv6,
- /// depending in whch mode test is currently running.
+ /// depending in which mode test is currently running.
void registerOptionFactories() const;
@@ -612,7 +662,7 @@ protected:
/// type and keeps them around until test finishes. Then they
/// are printed to the user. If packet of specified type has
/// been already stored this function perfroms no operation.
- /// This function does not perform sainty check if packet
+ /// This function does not perform sanity check if packet
/// pointer is valid. Make sure it is before calling it.
///
/// \param pkt packet to be stored.
@@ -684,6 +734,33 @@ protected:
const uint64_t packets_num,
const bool preload = false);
+ /// \brief Send number of DHCPv6 Renew or Release messages to the server.
+ ///
+ /// \param socket An object representing socket to be used to send packets.
+ /// \param msg_type A type of the messages to be sent (DHCPV6_RENEW or
+ /// DHCPV6_RELEASE).
+ /// \param msg_num A number of messages to be sent.
+ ///
+ /// \return A number of messages actually sent.
+ uint64_t sendMultipleMessages6(const TestControlSocket& socket,
+ const uint32_t msg_type,
+ const uint64_t msg_num);
+
+ /// \brief Send DHCPv6 Renew or Release message using specified socket.
+ ///
+ /// This method will select an existing lease from the Reply packet cache
+ /// If there is no lease that can be renewed or released this method will
+ /// return false.
+ ///
+ /// \param msg_type A type of the message to be sent (DHCPV6_RENEW or
+ /// DHCPV6_RELEASE).
+ /// \param socket An object encapsulating socket to be used to send
+ /// a packet.
+ ///
+ /// \return true if the message has been sent, false otherwise.
+ bool sendMessageFromReply(const uint16_t msg_type,
+ const TestControlSocket& socket);
+
/// \brief Send DHCPv4 REQUEST message.
///
/// Method creates and sends DHCPv4 REQUEST message to the server.
@@ -825,14 +902,36 @@ protected:
/// \return true if diagnostics flag has been set.
bool testDiags(const char diag) const;
- /// \brief Update due time to initiate next chunk of exchanges.
+protected:
+
+ /// \brief Increments counter of late sent messages if required.
///
- /// Method updates due time to initiate next chunk of exchanges.
- /// Function takes current time, last sent packet's time and
- /// expected rate in its calculations.
- void updateSendDue();
+ /// This function checks if the message or set of messages of a given type,
+ /// were sent later than their due time. If they were sent late, it is
+ /// an indication that the perfdhcp doesn't catch up with the desired rate
+ /// for sending messages.
+ ///
+ /// \param rate_control An object tracking due times for a particular
+ /// type of messages.
+ void checkLateMessages(RateControl& rate_control);
-private:
+ /// \brief Copies IA_NA or IA_PD option from one packet to another.
+ ///
+ /// This function checks the lease-type specified in the command line
+ /// with option -e<lease-type>. If 'address-only' value has been specified
+ /// this function expects that IA_NA option is present in the packet
+ /// encapsulated by pkt_from object. If 'prefix-only' value has been
+ /// specified, this function expects that IA_PD option is present in the
+ /// packet encapsulated by pkt_to object.
+ ///
+ /// \param [in] pkt_from A packet from which options should be copied.
+ /// \param [out] pkt_to A packet to which options should be copied.
+ ///
+ /// \throw isc::perfdhcp::OptionNotFound if a required option is not
+ /// found in the packet from which options should be copied.
+ /// \throw isc::BadValue if any of the specified pointers to packets
+ /// is NULL.
+ void copyIaOptions(const dhcp::Pkt6Ptr& pkt_from, dhcp::Pkt6Ptr& pkt_to);
/// \brief Convert binary value to hex string.
///
@@ -844,7 +943,7 @@ private:
/// \brief Calculate elapsed time between two packets.
///
- /// \param T Pkt4Ptr or Pkt6Ptr class.
+ /// \tparam T Pkt4Ptr or Pkt6Ptr class.
/// \param pkt1 first packet.
/// \param pkt2 second packet.
/// \throw InvalidOperation if packet timestamps are invalid.
@@ -957,16 +1056,20 @@ private:
std::string vector2Hex(const std::vector<uint8_t>& vec,
const std::string& separator = "") const;
- boost::posix_time::ptime send_due_; ///< Due time to initiate next chunk
- ///< of exchanges.
- boost::posix_time::ptime last_sent_; ///< Indicates when the last exchange
- /// was initiated.
+ /// \brief A rate control class for Discover and Solicit messages.
+ RateControl basic_rate_control_;
+ /// \brief A rate control class for Renew messages.
+ RateControl renew_rate_control_;
+ /// \brief A rate control class for Release messages.
+ RateControl release_rate_control_;
boost::posix_time::ptime last_report_; ///< Last intermediate report time.
StatsMgr4Ptr stats_mgr4_; ///< Statistics Manager 4.
StatsMgr6Ptr stats_mgr6_; ///< Statistics Manager 6.
+ PacketStorage<dhcp::Pkt6> reply_storage_; ///< A storage for reply messages.
+
NumberGeneratorPtr transid_gen_; ///< Transaction id generator.
NumberGeneratorPtr macaddr_gen_; ///< Numbers generator for MAC address.
diff --git a/tests/tools/perfdhcp/tests/Makefile.am b/tests/tools/perfdhcp/tests/Makefile.am
index aa4c0cf..6e808c6 100644
--- a/tests/tools/perfdhcp/tests/Makefile.am
+++ b/tests/tools/perfdhcp/tests/Makefile.am
@@ -1,6 +1,7 @@
SUBDIRS = . testdata
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -I$(srcdir)/.. -I$(builddir)/..
AM_CPPFLAGS += -DTEST_DATA_DIR=\"$(abs_srcdir)/testdata\"
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(B10_CXXFLAGS)
@@ -25,6 +26,8 @@ run_unittests_SOURCES += command_options_unittest.cc
run_unittests_SOURCES += perf_pkt6_unittest.cc
run_unittests_SOURCES += perf_pkt4_unittest.cc
run_unittests_SOURCES += localized_option_unittest.cc
+run_unittests_SOURCES += packet_storage_unittest.cc
+run_unittests_SOURCES += rate_control_unittest.cc
run_unittests_SOURCES += stats_mgr_unittest.cc
run_unittests_SOURCES += test_control_unittest.cc
run_unittests_SOURCES += command_options_helper.h
@@ -32,6 +35,7 @@ run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/command_options.cc
run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/pkt_transform.cc
run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/perf_pkt6.cc
run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/perf_pkt4.cc
+run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/rate_control.cc
run_unittests_SOURCES += $(top_builddir)/tests/tools/perfdhcp/test_control.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
diff --git a/tests/tools/perfdhcp/tests/command_options_helper.h b/tests/tools/perfdhcp/tests/command_options_helper.h
index 253fe12..afa0dd3 100644
--- a/tests/tools/perfdhcp/tests/command_options_helper.h
+++ b/tests/tools/perfdhcp/tests/command_options_helper.h
@@ -15,11 +15,15 @@
#ifndef COMMAND_OPTIONS_HELPER_H
#define COMMAND_OPTIONS_HELPER_H
+#include "../command_options.h"
+#include <exceptions/exceptions.h>
+
+#include <assert.h>
+#include <iterator>
+#include <cstring>
#include <string>
#include <vector>
-#include <exceptions/exceptions.h>
-#include "../command_options.h"
namespace isc {
namespace perfdhcp {
@@ -115,7 +119,7 @@ private:
// Tokenize string (space is a separator) using begin and end iteratos
std::vector<std::string> tokens(text_iterator, text_end);
- if (tokens.size() > 0) {
+ if (!tokens.empty()) {
// Allocate array of C-strings where we will store tokens
results = new char*[tokens.size()];
// Store tokens in C-strings array
diff --git a/tests/tools/perfdhcp/tests/command_options_unittest.cc b/tests/tools/perfdhcp/tests/command_options_unittest.cc
index 7a109fb..3d2ce2e 100644
--- a/tests/tools/perfdhcp/tests/command_options_unittest.cc
+++ b/tests/tools/perfdhcp/tests/command_options_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -28,6 +28,112 @@ using namespace isc;
using namespace isc::perfdhcp;
using namespace boost::posix_time;
+// Verify that default constructor sets lease type to the expected value.
+TEST(LeaseTypeTest, defaultConstructor) {
+ CommandOptions::LeaseType lease_type;
+ EXPECT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+}
+
+// Verify that the constructor sets the lease type to the specified value.
+TEST(LeaseTypeTest, constructor) {
+ CommandOptions::LeaseType
+ lease_type1(CommandOptions::LeaseType::ADDRESS);
+ EXPECT_TRUE(lease_type1.is(CommandOptions::LeaseType::ADDRESS));
+
+ CommandOptions::LeaseType
+ lease_type2(CommandOptions::LeaseType::PREFIX);
+ EXPECT_TRUE(lease_type2.is(CommandOptions::LeaseType::PREFIX));
+}
+
+// Verify that the lease type can be modified using set() function.
+TEST(LeaseTypeTest, set) {
+ CommandOptions::LeaseType
+ lease_type(CommandOptions::LeaseType::ADDRESS);
+ EXPECT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+
+ lease_type.set(CommandOptions::LeaseType::PREFIX);
+ EXPECT_TRUE(lease_type.is(CommandOptions::LeaseType::PREFIX));
+}
+
+// Verify that the includes() function returns true when the lease type
+// specified with the function argument is the same as the lease type
+// encapsulated by the LeaseType object on which include function is called
+// or when the lease type value encapsulated by this object is
+// ADDRESS_AND_PREFIX.
+TEST(LeaseTypeTest, includes) {
+ // Lease type: ADDRESS
+ CommandOptions::LeaseType lease_type(CommandOptions::LeaseType::ADDRESS);
+ // Lease type IS ADDRESS.
+ ASSERT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+ // Lease type includes the ADDRESS.
+ EXPECT_TRUE(lease_type.includes(CommandOptions::LeaseType::ADDRESS));
+ // Lease type does not include PREFIX.
+ EXPECT_FALSE(lease_type.includes(CommandOptions::LeaseType::PREFIX));
+ // Lease type does not include ADDRESS_AND_PREFIX.
+ EXPECT_FALSE(
+ lease_type.includes(CommandOptions::LeaseType::ADDRESS_AND_PREFIX)
+ );
+
+ // Do the same check for PREFIX.
+ lease_type.set(CommandOptions::LeaseType::PREFIX);
+ EXPECT_FALSE(lease_type.includes(CommandOptions::LeaseType::ADDRESS));
+ EXPECT_TRUE(lease_type.includes(CommandOptions::LeaseType::PREFIX));
+ EXPECT_FALSE(
+ lease_type.includes(CommandOptions::LeaseType::ADDRESS_AND_PREFIX)
+ );
+
+ // When lease type is set to 'address-and-prefix' it means that client
+ // requests both address and prefix (IA_NA and IA_PD). Therefore, the
+ // LeaseType::includes() function should return true for both ADDRESS
+ // and PREFIX.
+ lease_type.set(CommandOptions::LeaseType::ADDRESS_AND_PREFIX);
+ EXPECT_TRUE(lease_type.includes(CommandOptions::LeaseType::ADDRESS));
+ EXPECT_TRUE(lease_type.includes(CommandOptions::LeaseType::PREFIX));
+ EXPECT_TRUE(
+ lease_type.includes(CommandOptions::LeaseType::ADDRESS_AND_PREFIX)
+ );
+
+}
+
+// Verify that the LeaseType::fromCommandLine() function parses the lease-type
+// argument specified as -e<lease-type>.
+TEST(LeaseTypeTest, fromCommandLine) {
+ CommandOptions::LeaseType
+ lease_type(CommandOptions::LeaseType::ADDRESS);
+ ASSERT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+
+ lease_type.fromCommandLine("prefix-only");
+ ASSERT_TRUE(lease_type.is(CommandOptions::LeaseType::PREFIX));
+
+ lease_type.fromCommandLine("address-only");
+ EXPECT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+
+ lease_type.fromCommandLine("address-and-prefix");
+ EXPECT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS_AND_PREFIX));
+
+ EXPECT_THROW(lease_type.fromCommandLine("bogus-parameter"),
+ isc::InvalidParameter);
+
+}
+
+// Verify that the LeaseType::toText() function returns the textual
+// representation of the lease type specified.
+TEST(LeaseTypeTest, toText) {
+ CommandOptions::LeaseType lease_type;
+ ASSERT_TRUE(lease_type.is(CommandOptions::LeaseType::ADDRESS));
+ EXPECT_EQ("address-only (IA_NA option added to the client's request)",
+ lease_type.toText());
+
+ lease_type.set(CommandOptions::LeaseType::PREFIX);
+ EXPECT_EQ("prefix-only (IA_PD option added to the client's request)",
+ lease_type.toText());
+
+ lease_type.set(CommandOptions::LeaseType::ADDRESS_AND_PREFIX);
+ EXPECT_EQ("address-and-prefix (Both IA_NA and IA_PD options added to the"
+ " client's request)", lease_type.toText());
+
+}
+
/// \brief Test Fixture Class
///
/// This test fixture class is used to perform
@@ -60,7 +166,10 @@ protected:
EXPECT_NO_THROW(process("perfdhcp 192.168.0.1"));
EXPECT_EQ(4, opt.getIpVersion());
EXPECT_EQ(CommandOptions::DORA_SARR, opt.getExchangeMode());
+ EXPECT_TRUE(opt.getLeaseType().is(CommandOptions::LeaseType::ADDRESS));
EXPECT_EQ(0, opt.getRate());
+ EXPECT_EQ(0, opt.getRenewRate());
+ EXPECT_EQ(0, opt.getReleaseRate());
EXPECT_EQ(0, opt.getReportDelay());
EXPECT_EQ(0, opt.getClientsNum());
@@ -181,6 +290,32 @@ TEST_F(CommandOptionsTest, IpVersion) {
EXPECT_THROW(process("perfdhcp -c -l ethx all"), isc::InvalidParameter);
}
+TEST_F(CommandOptionsTest, LeaseType) {
+ CommandOptions& opt = CommandOptions::instance();
+ // Check that the -e address-only works for IPv6.
+ ASSERT_NO_THROW(process("perfdhcp -6 -l etx -e address-only all"));
+ EXPECT_EQ(6, opt.getIpVersion());
+ EXPECT_EQ("etx", opt.getLocalName());
+ EXPECT_TRUE(opt.getLeaseType().is(CommandOptions::LeaseType::ADDRESS));
+ // Check that the -e address-only works for IPv4.
+ ASSERT_NO_THROW(process("perfdhcp -4 -l etx -e address-only all"));
+ EXPECT_EQ(4, opt.getIpVersion());
+ EXPECT_EQ("etx", opt.getLocalName());
+ EXPECT_TRUE(opt.getLeaseType().is(CommandOptions::LeaseType::ADDRESS));
+ // Check that the -e prefix-only works.
+ ASSERT_NO_THROW(process("perfdhcp -6 -l etx -e prefix-only all"));
+ EXPECT_EQ(6, opt.getIpVersion());
+ EXPECT_EQ("etx", opt.getLocalName());
+ EXPECT_TRUE(opt.getLeaseType().is(CommandOptions::LeaseType::PREFIX));
+ // Check that -e prefix-only must not coexist with -4 option.
+ EXPECT_THROW(process("perfdhcp -4 -l ethx -e prefix-only all"),
+ InvalidParameter);
+ // Check that -e prefix-only must not coexist with -T options.
+ EXPECT_THROW(process("perfdhcp -6 -l ethx -e prefix-only -T file1.hex"
+ " -T file2.hex -E 4 all"), InvalidParameter);
+
+}
+
TEST_F(CommandOptionsTest, Rate) {
CommandOptions& opt = CommandOptions::instance();
EXPECT_NO_THROW(process("perfdhcp -4 -r 10 -l ethx all"));
@@ -201,6 +336,99 @@ TEST_F(CommandOptionsTest, Rate) {
isc::InvalidParameter);
}
+TEST_F(CommandOptionsTest, RenewRate) {
+ CommandOptions& opt = CommandOptions::instance();
+ // If -f is specified together with -r the command line should
+ // be accepted and the renew rate should be set.
+ EXPECT_NO_THROW(process("perfdhcp -6 -r 10 -f 10 -l ethx all"));
+ EXPECT_EQ(10, opt.getRenewRate());
+ // Check that the release rate can be set to different value than
+ // rate specified as -r<rate>. Also, swap -f and -r to make sure
+ // that order doesn't matter.
+ EXPECT_NO_THROW(process("perfdhcp -6 -f 5 -r 10 -l ethx all"));
+ EXPECT_EQ(5, opt.getRenewRate());
+ // The renew rate should not be greater than the rate.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -f 11 -l ethx all"),
+ isc::InvalidParameter);
+ // The renew-rate of 0 is invalid.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -f 0 -l ethx all"),
+ isc::InvalidParameter);
+ // The negative renew-rate is invalid.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -f -5 -l ethx all"),
+ isc::InvalidParameter);
+ // If -r<rate> is not specified the -f<renew-rate> should not
+ // be accepted.
+ EXPECT_THROW(process("perfdhcp -6 -f 10 -l ethx all"),
+ isc::InvalidParameter);
+ // Currently the -f<renew-rate> can be specified for IPv6 mode
+ // only.
+ EXPECT_THROW(process("perfdhcp -4 -r 10 -f 10 -l ethx all"),
+ isc::InvalidParameter);
+ // Renew rate should be specified.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -f -l ethx all"),
+ isc::InvalidParameter);
+
+ // -f and -i are mutually exclusive
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -f 10 -l ethx -i all"),
+ isc::InvalidParameter);
+}
+
+TEST_F(CommandOptionsTest, ReleaseRate) {
+ CommandOptions& opt = CommandOptions::instance();
+ // If -F is specified together with -r the command line should
+ // be accepted and the release rate should be set.
+ EXPECT_NO_THROW(process("perfdhcp -6 -r 10 -F 10 -l ethx all"));
+ EXPECT_EQ(10, opt.getReleaseRate());
+ // Check that the release rate can be set to different value than
+ // rate specified as -r<rate>. Also, swap -F and -r to make sure
+ // that order doesn't matter.
+ EXPECT_NO_THROW(process("perfdhcp -6 -F 5 -r 10 -l ethx all"));
+ EXPECT_EQ(5, opt.getReleaseRate());
+ // The release rate should not be greater than the rate.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -F 11 -l ethx all"),
+ isc::InvalidParameter);
+ // The release-rate of 0 is invalid.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -F 0 -l ethx all"),
+ isc::InvalidParameter);
+ // The negative rlease-rate is invalid.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -F -5 -l ethx all"),
+ isc::InvalidParameter);
+ // If -r<rate> is not specified the -F<release-rate> should not
+ // be accepted.
+ EXPECT_THROW(process("perfdhcp -6 -F 10 -l ethx all"),
+ isc::InvalidParameter);
+ // Currently the -F<release-rate> can be specified for IPv6 mode
+ // only.
+ EXPECT_THROW(process("perfdhcp -4 -r 10 -F 10 -l ethx all"),
+ isc::InvalidParameter);
+ // Release rate should be specified.
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -F -l ethx all"),
+ isc::InvalidParameter);
+
+ // -F and -i are mutually exclusive
+ EXPECT_THROW(process("perfdhcp -6 -r 10 -F 10 -l ethx -i all"),
+ isc::InvalidParameter);
+}
+
+TEST_F(CommandOptionsTest, ReleaseRenew) {
+ CommandOptions& opt = CommandOptions::instance();
+ // It should be possible to specify the -F, -f and -r options.
+ EXPECT_NO_THROW(process("perfdhcp -6 -r 10 -F 3 -f 5 -l ethx all"));
+ EXPECT_EQ(10, opt.getRate());
+ EXPECT_EQ(3, opt.getReleaseRate());
+ EXPECT_EQ(5, opt.getRenewRate());
+ // It should be possible to specify the -F and -f with the values which
+ // sum is equal to the rate specified as -r<rate>.
+ EXPECT_NO_THROW(process("perfdhcp -6 -r 8 -F 3 -f 5 -l ethx all"));
+ EXPECT_EQ(8, opt.getRate());
+ EXPECT_EQ(3, opt.getReleaseRate());
+ EXPECT_EQ(5, opt.getRenewRate());
+ // Check that the sum of the release and renew rate is not greater
+ // than the rate specified as -r<rate>.
+ EXPECT_THROW(process("perfdhcp -6 -F 6 -f 5 -r 10 -l ethx all"),
+ isc::InvalidParameter);
+}
+
TEST_F(CommandOptionsTest, ReportDelay) {
CommandOptions& opt = CommandOptions::instance();
EXPECT_NO_THROW(process("perfdhcp -r 100 -t 17 -l ethx all"));
diff --git a/tests/tools/perfdhcp/tests/packet_storage_unittest.cc b/tests/tools/perfdhcp/tests/packet_storage_unittest.cc
new file mode 100644
index 0000000..b6e415b
--- /dev/null
+++ b/tests/tools/perfdhcp/tests/packet_storage_unittest.cc
@@ -0,0 +1,205 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include "../packet_storage.h"
+#include <dhcp/dhcp6.h>
+#include <dhcp/pkt6.h>
+
+#include <gtest/gtest.h>
+
+namespace {
+
+using namespace isc;
+using namespace isc::dhcp;
+using namespace perfdhcp;
+
+/// @todo Implement the tests which use Pkt4 objects once the support for
+/// DHCPv4 renewals / releases is added.
+
+/// The number of packets in the test storage.
+const unsigned int STORAGE_SIZE = 20;
+
+/// Test fixture class for PacketStorage container testing.
+class PacketStorageTest : public ::testing::Test {
+public:
+ /// \brief Constructor, initializes the storage for each test.
+ PacketStorageTest() {
+ for (uint32_t i = 0; i < STORAGE_SIZE; ++i) {
+ storage_.append(createPacket6(DHCPV6_REPLY, i));
+ }
+ }
+
+ /// \brief Creates an instance of the Pkt6.
+ ///
+ /// \param packet_type A type of the packet.
+ /// \param transid Transaction id.
+ /// \return An instance of the Pkt6.
+ Pkt6Ptr createPacket6(const uint16_t packet_type,
+ const uint32_t transid) {
+ return (Pkt6Ptr(new Pkt6(packet_type, transid)));
+ }
+
+ /// Packet storage under test.
+ PacketStorage<Pkt6> storage_;
+
+};
+
+// This test verifies that the packets in the storage can be accessed
+// sequentially and when a packet is returned, it is removed from the
+// storage. It also verifies the correctness of the behaviour of the
+// empty() and size() functions.
+TEST_F(PacketStorageTest, getNext) {
+ ASSERT_EQ(STORAGE_SIZE, storage_.size());
+ for (int i = 0; i < STORAGE_SIZE; ++i) {
+ Pkt6Ptr packet = storage_.getNext();
+ ASSERT_TRUE(packet) << "NULL packet returned by storage_.getNext() for"
+ << " iteration number " << i;
+ EXPECT_EQ(i, packet->getTransid());
+ EXPECT_EQ(STORAGE_SIZE - i - 1, storage_.size());
+ }
+ EXPECT_TRUE(storage_.empty());
+ // When storage is empty, the attempt to get the next packet should
+ // result in returning NULL pointer.
+ EXPECT_FALSE(storage_.getNext());
+ // Let's try it again to see if the previous call to getNext didn't
+ // put the storage into the state in which the subsequent calls to
+ // getNext would result in incorrect behaviour.
+ EXPECT_FALSE(storage_.getNext());
+
+ // Let's add a new packet to the empty storage to check that storage
+ // "recovers" from being empty, i.e. that the internal indicator
+ // which points to current packet reinitializes correctly.
+ storage_.append(createPacket6(DHCPV6_REPLY, 100));
+ ASSERT_EQ(1, storage_.size());
+ Pkt6Ptr packet = storage_.getNext();
+ EXPECT_EQ(100, packet->getTransid());
+ EXPECT_FALSE(storage_.getNext());
+}
+
+// This test verifies that the packets in the storage can be accessed
+// randomly and when a packet is returned, it is removed from the
+// storage. It also verifies the correctness of the behaviour of the
+// empty() and size() functions.
+TEST_F(PacketStorageTest, getRandom) {
+ ASSERT_EQ(STORAGE_SIZE, storage_.size());
+ int cnt_equals = 0;
+ for (int i = 0; i < STORAGE_SIZE; ++i) {
+ Pkt6Ptr packet = storage_.getRandom();
+ ASSERT_TRUE(packet) << "NULL packet returned by storage_.getRandom()"
+ " for iteration number " << i;
+ EXPECT_EQ(STORAGE_SIZE - i - 1, storage_.size());
+ cnt_equals += (i == packet->getTransid() ? 1 : 0);
+ }
+ // If the number of times id is equal to i, is the same as the number
+ // of elements then they were NOT accessed randomly.
+ // The odds of 20 elements being randomly accessed sequential order
+ // is nil isn't it?
+ EXPECT_NE(cnt_equals, STORAGE_SIZE);
+
+ EXPECT_TRUE(storage_.empty());
+ // When storage is empty, the attempt to get the random packet should
+ // result in returning NULL pointer.
+ EXPECT_FALSE(storage_.getRandom());
+ // Let's try it again to see if the previous call to getRandom didn't
+ // put the storage into the state in which the subsequent calls to
+ // getRandom would result in incorrect behaviour.
+ EXPECT_FALSE(storage_.getRandom());
+
+ // Let's add a new packet to the empty storage to check that storage
+ // "recovers" from being empty, i.e. that the internal indicator
+ // which points to the current packet reinitializes correctly.
+ storage_.append(createPacket6(DHCPV6_REPLY, 100));
+ ASSERT_EQ(1, storage_.size());
+ Pkt6Ptr packet = storage_.getRandom();
+ ASSERT_TRUE(packet);
+ EXPECT_EQ(100, packet->getTransid());
+ EXPECT_FALSE(storage_.getRandom());
+}
+
+// This test verifies that the packets in the storage can be accessed
+// either randomly or sequentially in the same time. It verifies that
+// each returned packet is removed from the storage.
+TEST_F(PacketStorageTest, getNextAndRandom) {
+ ASSERT_EQ(STORAGE_SIZE, storage_.size());
+ for (int i = 0; i < STORAGE_SIZE / 2; ++i) {
+ Pkt6Ptr packet_random = storage_.getRandom();
+ ASSERT_TRUE(packet_random) << "NULL packet returned by"
+ " storage_.getRandom() for iteration number " << i;
+ EXPECT_EQ(STORAGE_SIZE - 2 *i - 1, storage_.size());
+ Pkt6Ptr packet_seq = storage_.getNext();
+ ASSERT_TRUE(packet_seq) << "NULL packet returned by"
+ " storage_.getNext() for iteration number " << i;
+ EXPECT_EQ(STORAGE_SIZE - 2 * i - 2, storage_.size());
+ }
+ EXPECT_TRUE(storage_.empty());
+ EXPECT_FALSE(storage_.getRandom());
+ EXPECT_FALSE(storage_.getNext());
+
+ // Append two packets to the storage to check if it can "recover"
+ // from being empty and that new elements can be accessed.
+ storage_.append(createPacket6(DHCPV6_REPLY, 100));
+ storage_.append(createPacket6(DHCPV6_REPLY, 101));
+ ASSERT_EQ(2, storage_.size());
+ // The newly added elements haven't been accessed yet. So, if we
+ // call getNext the first one should be returned.
+ Pkt6Ptr packet_next = storage_.getNext();
+ ASSERT_TRUE(packet_next);
+ // The first packet has transaction id equal to 100.
+ EXPECT_EQ(100, packet_next->getTransid());
+ // There should be just one packet left in the storage.
+ ASSERT_EQ(1, storage_.size());
+ // The call to getRandom should return the sole packet from the
+ // storage.
+ Pkt6Ptr packet_random = storage_.getRandom();
+ ASSERT_TRUE(packet_random);
+ EXPECT_EQ(101, packet_random->getTransid());
+ // Any further calls to getRandom and getNext should return NULL.
+ EXPECT_FALSE(storage_.getRandom());
+ EXPECT_FALSE(storage_.getNext());
+}
+
+// This test verifies that all packets are removed from the storage when
+// clear() function is invoked.
+TEST_F(PacketStorageTest, clearAll) {
+ ASSERT_EQ(STORAGE_SIZE, storage_.size());
+ ASSERT_NO_THROW(storage_.clear());
+ EXPECT_TRUE(storage_.empty());
+}
+
+// This test verifies that a set of packets can be removed from the
+// storage when a number of packets to be removed is specified. If
+// number of packets to be removed exceeds the storage size, all
+// packets should be removed.
+TEST_F(PacketStorageTest, clear) {
+ // Initially storage should have 20 elements.
+ ASSERT_EQ(STORAGE_SIZE, storage_.size());
+ // Remove 10 of them.
+ ASSERT_NO_THROW(storage_.clear(10));
+ // We should have 10 remaining.
+ ASSERT_EQ(10, storage_.size());
+
+ // Check that the retrieval still works after partial clear.
+ EXPECT_TRUE(storage_.getNext());
+ EXPECT_TRUE(storage_.getRandom());
+ // We should have 10 - 2 = 8 packets in the storage after retrieval.
+ ASSERT_EQ(8, storage_.size());
+
+ // Try to remove more elements that actually is. It
+ // should result in removal of all elements.
+ ASSERT_NO_THROW(storage_.clear(15));
+ EXPECT_TRUE(storage_.empty());
+}
+
+
+} // anonymous namespace
diff --git a/tests/tools/perfdhcp/tests/perf_pkt6_unittest.cc b/tests/tools/perfdhcp/tests/perf_pkt6_unittest.cc
index de134cc..02c009e 100644
--- a/tests/tools/perfdhcp/tests/perf_pkt6_unittest.cc
+++ b/tests/tools/perfdhcp/tests/perf_pkt6_unittest.cc
@@ -111,8 +111,8 @@ TEST_F(PerfPkt6Test, Constructor) {
// Test constructor to be used for incoming messages.
// Use default (1) offset value and don't specify transaction id.
boost::scoped_ptr<PerfPkt6> pkt1(new PerfPkt6(data, sizeof(data)));
- EXPECT_EQ(sizeof(data), pkt1->getData().size());
- EXPECT_EQ(0, memcmp(&pkt1->getData()[0], data, sizeof(data)));
+ EXPECT_EQ(sizeof(data), pkt1->data_.size());
+ EXPECT_EQ(0, memcmp(&pkt1->data_[0], data, sizeof(data)));
EXPECT_EQ(1, pkt1->getTransidOffset());
// Test constructor to be used for outgoing messages.
@@ -121,8 +121,8 @@ TEST_F(PerfPkt6Test, Constructor) {
const uint32_t transid = 0x010203;
boost::scoped_ptr<PerfPkt6> pkt2(new PerfPkt6(data, sizeof(data),
offset_transid, transid));
- EXPECT_EQ(sizeof(data), pkt2->getData().size());
- EXPECT_EQ(0, memcmp(&pkt2->getData()[0], data, sizeof(data)));
+ EXPECT_EQ(sizeof(data), pkt2->data_.size());
+ EXPECT_EQ(0, memcmp(&pkt2->data_[0], data, sizeof(data)));
EXPECT_EQ(0x010203, pkt2->getTransid());
EXPECT_EQ(10, pkt2->getTransidOffset());
}
@@ -163,7 +163,7 @@ TEST_F(PerfPkt6Test, RawPackUnpack) {
// Get output buffer from packet 1 to create new packet
// that will be later validated.
util::OutputBuffer pkt1_output = pkt1->getBuffer();
- ASSERT_EQ(pkt1_output.getLength(), pkt1->getData().size());
+ ASSERT_EQ(pkt1_output.getLength(), pkt1->data_.size());
const uint8_t* pkt1_output_data = static_cast<const uint8_t*>
(pkt1_output.getData());
boost::scoped_ptr<PerfPkt6> pkt2(new PerfPkt6(pkt1_output_data,
diff --git a/tests/tools/perfdhcp/tests/rate_control_unittest.cc b/tests/tools/perfdhcp/tests/rate_control_unittest.cc
new file mode 100644
index 0000000..829d37f
--- /dev/null
+++ b/tests/tools/perfdhcp/tests/rate_control_unittest.cc
@@ -0,0 +1,207 @@
+// Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <exceptions/exceptions.h>
+#include "rate_control.h"
+#include <gtest/gtest.h>
+
+
+using namespace isc;
+using namespace isc::perfdhcp;
+
+/// \brief A class which exposes protected methods and members of the
+/// RateControl class (under test).
+class NakedRateControl : public RateControl {
+public:
+
+ /// \brief Default constructor.
+ NakedRateControl()
+ : RateControl() {
+ }
+
+ /// \brief Constructor which sets up the rate and aggressivity.
+ ///
+ /// \param rate A rate at which messages are sent.
+ /// \param aggressivity A value of aggressivity. This value controls the
+ /// maximal number of messages sent in one chunk.
+ NakedRateControl(const int rate, const int aggressivity)
+ : RateControl(rate, aggressivity) {
+ }
+
+ using RateControl::currentTime;
+ using RateControl::updateSendTime;
+ using RateControl::updateSendDue;
+ using RateControl::send_due_;
+ using RateControl::last_sent_;
+ using RateControl::late_sent_;
+
+};
+
+// Test default constructor.
+TEST(RateControl, constructorDefault) {
+ NakedRateControl rc;
+ EXPECT_EQ(1, rc.getAggressivity());
+ EXPECT_EQ(0, rc.getRate());
+ EXPECT_FALSE(rc.getDue().is_not_a_date_time());
+ EXPECT_FALSE(rc.last_sent_.is_not_a_date_time());
+ EXPECT_FALSE(rc.isLateSent());
+}
+
+// Test the constructor which sets the rate and aggressivity.
+TEST(RateControl, constructor) {
+ // Call the constructor and verify that it sets the appropriate
+ // values.
+ NakedRateControl rc1(3, 2);
+ EXPECT_EQ(2, rc1.getAggressivity());
+ EXPECT_EQ(3, rc1.getRate());
+ EXPECT_FALSE(rc1.getDue().is_not_a_date_time());
+ EXPECT_FALSE(rc1.last_sent_.is_not_a_date_time());
+ EXPECT_FALSE(rc1.isLateSent());
+
+ // Call the constructor again and make sure that different values
+ // will be set correctly.
+ NakedRateControl rc2(5, 6);
+ EXPECT_EQ(6, rc2.getAggressivity());
+ EXPECT_EQ(5, rc2.getRate());
+ EXPECT_FALSE(rc2.getDue().is_not_a_date_time());
+ EXPECT_FALSE(rc2.last_sent_.is_not_a_date_time());
+ EXPECT_FALSE(rc2.isLateSent());
+
+ // The 0 value of aggressivity < 1 is not acceptable.
+ EXPECT_THROW(RateControl(3, 0), isc::BadValue);
+ // The negative value of rate is not acceptable.
+ EXPECT_THROW(RateControl(-1, 3), isc::BadValue);
+}
+
+// Check the aggressivity accessor.
+TEST(RateControl, getAggressivity) {
+ RateControl rc;
+ ASSERT_EQ(1, rc.getAggressivity());
+ rc.setAggressivity(5);
+ ASSERT_EQ(5, rc.getAggressivity());
+ rc.setAggressivity(10);
+ EXPECT_EQ(10, rc.getAggressivity());
+}
+
+// Check the due time accessor.
+TEST(RateControl, getDue) {
+ NakedRateControl rc;
+ ASSERT_FALSE(rc.getDue().is_not_a_date_time());
+ rc.send_due_ = NakedRateControl::currentTime();
+ EXPECT_TRUE(NakedRateControl::currentTime() >= rc.getDue());
+ rc.send_due_ = NakedRateControl::currentTime() +
+ boost::posix_time::seconds(10);
+ EXPECT_TRUE(NakedRateControl::currentTime() < rc.getDue());
+}
+
+// Check the rate accessor.
+TEST(RateControl, getRate) {
+ RateControl rc;
+ ASSERT_EQ(0, rc.getRate());
+ rc.setRate(5);
+ ASSERT_EQ(5, rc.getRate());
+ rc.setRate(10);
+ EXPECT_EQ(10, rc.getRate());
+}
+
+// Check if late send flag accessor.
+TEST(RateControl, isLateSent) {
+ NakedRateControl rc;
+ ASSERT_FALSE(rc.isLateSent());
+ rc.late_sent_ = true;
+ EXPECT_TRUE(rc.isLateSent());
+}
+
+// Check that the function returns the number of messages to be sent "now"
+// correctly.
+// @todo Possibly extend this test to cover more complex scenarios. Note that
+// it is quite hard to fully test this function as its behaviour strongly
+// depends on time.
+TEST(RateControl, getOutboundMessageCount) {
+ NakedRateControl rc1(1000, 1);
+ // Set the timestamp of the last sent message well to the past.
+ // The resulting due time will be in the past too.
+ rc1.last_sent_ =
+ NakedRateControl::currentTime() - boost::posix_time::seconds(5);
+ // The number of messages to be sent must be greater than 0.
+ uint64_t count;
+ ASSERT_NO_THROW(count = rc1.getOutboundMessageCount());
+ EXPECT_GT(count, 0);
+ // Now, don't specify the rate. In this case the aggressivity dictates
+ // how many messages to send.
+ NakedRateControl rc2(0, 3);
+ rc2.last_sent_ =
+ NakedRateControl::currentTime() - boost::posix_time::seconds(5);
+ ASSERT_NO_THROW(count = rc2.getOutboundMessageCount());
+ EXPECT_EQ(3, count);
+ // Specify the rate and set the timestamp of the last sent message well
+ // to the future. If the resulting due time is well in the future too,
+ // the number of messages to be sent must be 0.
+ NakedRateControl rc3(10, 3);
+ rc3.last_sent_ = NakedRateControl::currentTime() +
+ boost::posix_time::seconds(5);
+ ASSERT_NO_THROW(count = rc3.getOutboundMessageCount());
+ EXPECT_EQ(0, count);
+
+}
+
+// Test the aggressivity modifier for valid and invalid values.
+TEST(RateControl, setAggressivity) {
+ NakedRateControl rc;
+ ASSERT_NO_THROW(rc.setAggressivity(1));
+ EXPECT_THROW(rc.setAggressivity(0), isc::BadValue);
+ EXPECT_THROW(rc.setAggressivity(-1), isc::BadValue);
+}
+
+// Test the rate modifier for valid and invalid rate values.
+TEST(RateControl, setRate) {
+ NakedRateControl rc;
+ EXPECT_NO_THROW(rc.setRate(1));
+ EXPECT_NO_THROW(rc.setRate(0));
+ EXPECT_THROW(rc.setRate(-1), isc::BadValue);
+}
+
+// Test the function which calculates the due time to send next set of
+// messages.
+TEST(RateControl, updateSendDue) {
+ NakedRateControl rc;
+ // Set the send due timestamp to the value which is well in the future.
+ // If we don't hit the due time, the function should not modify the
+ // due time.
+ rc.send_due_ =
+ NakedRateControl::currentTime() + boost::posix_time::seconds(10);
+ boost::posix_time::ptime last_send_due = rc.send_due_;
+ ASSERT_NO_THROW(rc.updateSendDue());
+ EXPECT_TRUE(rc.send_due_ == last_send_due);
+ // Set the due time to the value which is already behind.
+ rc.send_due_ =
+ NakedRateControl::currentTime() - boost::posix_time::seconds(10);
+ last_send_due = rc.send_due_;
+ ASSERT_NO_THROW(rc.updateSendDue());
+ // The value should be modified to the new value.
+ EXPECT_TRUE(rc.send_due_ > last_send_due);
+
+}
+
+// Test that the message send time is updated to the current time.
+TEST(RateControl, updateSendTime) {
+ NakedRateControl rc;
+ // Set the timestamp to the future.
+ rc.last_sent_ =
+ NakedRateControl::currentTime() + boost::posix_time::seconds(5);
+ rc.updateSendTime();
+ // Updated timestamp should be set to now or to the past.
+ EXPECT_TRUE(rc.last_sent_ <= NakedRateControl::currentTime());
+
+}
diff --git a/tests/tools/perfdhcp/tests/stats_mgr_unittest.cc b/tests/tools/perfdhcp/tests/stats_mgr_unittest.cc
index ebb4f34..6147bf7 100644
--- a/tests/tools/perfdhcp/tests/stats_mgr_unittest.cc
+++ b/tests/tools/perfdhcp/tests/stats_mgr_unittest.cc
@@ -81,6 +81,10 @@ public:
///
/// Method simulates sending or receiving multiple DHCPv6 packets.
///
+ /// \note The xchg_type parameter is passed as non-const value to avoid
+ /// false cppcheck errors which expect enum value being passed by reference.
+ /// This error is not reported when non-const enum is passed by value.
+ ///
/// \param stats_mgr Statistics Manager instance to be used.
/// \param xchg_type packet exchange types.
/// \param packet_type DHCPv6 packet type.
@@ -88,7 +92,7 @@ public:
/// \param receive simulated packets are received (if true)
/// or sent (if false)
void passMultiplePackets6(const boost::shared_ptr<StatsMgr6> stats_mgr,
- const StatsMgr6::ExchangeType xchg_type,
+ StatsMgr6::ExchangeType xchg_type,
const uint8_t packet_type,
const int num_packets,
const bool receive = false) {
@@ -183,6 +187,8 @@ TEST_F(StatsMgrTest, Exchange) {
common_transid));
// This is expected to throw because XCHG_DO was not yet
// added to Stats Manager for tracking.
+ ASSERT_FALSE(stats_mgr->hasExchangeStats(StatsMgr4::XCHG_DO));
+ ASSERT_FALSE(stats_mgr->hasExchangeStats(StatsMgr4::XCHG_RA));
EXPECT_THROW(
stats_mgr->passSentPacket(StatsMgr4::XCHG_DO, sent_packet),
BadValue
@@ -192,8 +198,11 @@ TEST_F(StatsMgrTest, Exchange) {
BadValue
);
+
// Adding DISCOVER-OFFER exchanges to be tracked by Stats Manager.
stats_mgr->addExchangeStats(StatsMgr4::XCHG_DO);
+ ASSERT_TRUE(stats_mgr->hasExchangeStats(StatsMgr4::XCHG_DO));
+ ASSERT_FALSE(stats_mgr->hasExchangeStats(StatsMgr4::XCHG_RA));
// The following two attempts are expected to throw because
// invalid exchange types are passed (XCHG_RA instead of XCHG_DO)
EXPECT_THROW(
@@ -255,6 +264,21 @@ TEST_F(StatsMgrTest, MultipleExchanges) {
stats_mgr->getRcvdPacketsNum(StatsMgr6::XCHG_RR));
}
+TEST_F(StatsMgrTest, ExchangeToString) {
+ // Test DHCPv4 specific exchange names.
+ EXPECT_EQ("DISCOVER-OFFER",
+ StatsMgr4::exchangeToString(StatsMgr4::XCHG_DO));
+ EXPECT_EQ("REQUEST-ACK", StatsMgr4::exchangeToString(StatsMgr4::XCHG_RA));
+
+ // Test DHCPv6 specific exchange names.
+ EXPECT_EQ("SOLICIT-ADVERTISE",
+ StatsMgr6::exchangeToString(StatsMgr6::XCHG_SA));
+ EXPECT_EQ("REQUEST-REPLY", StatsMgr6::exchangeToString(StatsMgr6::XCHG_RR));
+ EXPECT_EQ("RENEW-REPLY", StatsMgr6::exchangeToString(StatsMgr6::XCHG_RN));
+ EXPECT_EQ("RELEASE-REPLY", StatsMgr6::exchangeToString(StatsMgr6::XCHG_RL));
+
+}
+
TEST_F(StatsMgrTest, SendReceiveSimple) {
boost::scoped_ptr<StatsMgr4> stats_mgr(new StatsMgr4());
boost::shared_ptr<Pkt4> sent_packet(createPacket4(DHCPDISCOVER,
@@ -347,7 +371,6 @@ TEST_F(StatsMgrTest, Delays) {
// Send DISCOVER, wait 2s and receive OFFER. This will affect
// counters in Stats Manager.
- const unsigned int delay1 = 2;
passDOPacketsWithDelay(stats_mgr, 2, common_transid);
// Initially min delay is equal to MAX_DOUBLE. After first packets
diff --git a/tests/tools/perfdhcp/tests/test_control_unittest.cc b/tests/tools/perfdhcp/tests/test_control_unittest.cc
index 3e0145c..b84d58d 100644
--- a/tests/tools/perfdhcp/tests/test_control_unittest.cc
+++ b/tests/tools/perfdhcp/tests/test_control_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -12,21 +12,22 @@
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
+#include "command_options_helper.h"
+#include "../test_control.h"
+
+#include <asiolink/io_address.h>
+#include <exceptions/exceptions.h>
+#include <dhcp/dhcp4.h>
+#include <dhcp/iface_mgr.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
#include <cstddef>
#include <stdint.h>
#include <string>
#include <fstream>
#include <gtest/gtest.h>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-#include <exceptions/exceptions.h>
-#include <asiolink/io_address.h>
-#include <dhcp/dhcp4.h>
-#include <dhcp/iface_mgr.h>
-#include "command_options_helper.h"
-#include "../test_control.h"
-
using namespace std;
using namespace boost::posix_time;
using namespace isc;
@@ -40,7 +41,7 @@ using namespace isc::perfdhcp;
class NakedTestControl: public TestControl {
public:
- /// \brief Incremental transaction id generaator.
+ /// \brief Incremental transaction id generator.
///
/// This is incremental transaction id generator. It overrides
/// the default transaction id generator that generates transaction
@@ -63,11 +64,42 @@ public:
virtual uint32_t generate() {
return (++transid_);
}
+
+ /// \brief Return next transaction id value.
+ uint32_t getNext() const {
+ return (transid_ + 1);
+ }
+
private:
uint32_t transid_; ///< Last generated transaction id.
};
+ /// \brief Sets the due times for sedning Solicit, Renew and Release.
+ ///
+ /// There are three class members that hold the due time for sending DHCP
+ /// messages:
+ /// - send_due_ - due time to send Solicit,
+ /// - renew_due_ - due time to send Renew,
+ /// - release_due_ - due time to send Release.
+ /// Some tests in this test suite need to modify these values relative to
+ /// the current time. This function modifies this values using time
+ /// offset values (positive or negative) specified as a difference in
+ /// seconds between current time and the due time.
+ ///
+ /// \param send_secs An offset of the due time for Solicit.
+ /// \param renew_secs An offset of the due time for Renew.
+ /// \param release_secs An offset of the due time for Release.
+ void setRelativeDueTimes(const int send_secs, const int renew_secs = 0,
+ const int release_secs = 0) {
+ ptime now = microsec_clock::universal_time();
+ basic_rate_control_.setRelativeDue(send_secs);
+ renew_rate_control_.setRelativeDue(renew_secs);
+ release_rate_control_.setRelativeDue(release_secs);
+
+ }
+
using TestControl::checkExitConditions;
+ using TestControl::createMessageFromReply;
using TestControl::factoryElapsedTime6;
using TestControl::factoryGeneric;
using TestControl::factoryIana6;
@@ -76,7 +108,7 @@ public:
using TestControl::factoryRequestList4;
using TestControl::generateDuid;
using TestControl::generateMacAddress;
- using TestControl::getNextExchangesNum;
+ using TestControl::getCurrentTimeout;
using TestControl::getTemplateBuffer;
using TestControl::initPacketTemplates;
using TestControl::initializeStatsMgr;
@@ -84,10 +116,22 @@ public:
using TestControl::processReceivedPacket4;
using TestControl::processReceivedPacket6;
using TestControl::registerOptionFactories;
+ using TestControl::reset;
using TestControl::sendDiscover4;
+ using TestControl::sendPackets;
+ using TestControl::sendMultipleMessages6;
+ using TestControl::sendRequest6;
using TestControl::sendSolicit6;
using TestControl::setDefaults4;
using TestControl::setDefaults6;
+ using TestControl::basic_rate_control_;
+ using TestControl::renew_rate_control_;
+ using TestControl::release_rate_control_;
+ using TestControl::last_report_;
+ using TestControl::transid_gen_;
+ using TestControl::macaddr_gen_;
+ using TestControl::first_packet_serverid_;
+ using TestControl::interrupted_;
NakedTestControl() : TestControl() {
uint32_t clients_num = CommandOptions::instance().getClientsNum() == 0 ?
@@ -123,7 +167,7 @@ public:
/// truncated.
///
/// \param filename template file to be created.
- /// \param buffer with binary datato be stored in file.
+ /// \param buffer with binary data to be stored in file.
/// \param size target size of the file.
/// \param invalid_chars inject invalid chars to the template file.
/// \return true if file creation successful.
@@ -241,8 +285,9 @@ public:
size_t matched_num = 0;
for (size_t i = 0; i < buf.size(); i += 2) {
for (int j = 0; j < requested_options.size(); j += 2) {
- uint16_t opt_i = buf[i + 1] << 8 + buf[i] & 0xFF;
- uint16_t opt_j = requested_options[j + 1] << 8 + requested_options[j] & 0xFF;
+ uint16_t opt_i = (buf[i + 1] << 8) + (buf[i] & 0xFF);
+ uint16_t opt_j = (requested_options[j + 1] << 8)
+ + (requested_options[j] & 0xFF);
if (opt_i == opt_j) {
// Requested option has been found.
++matched_num;
@@ -280,9 +325,9 @@ public:
return (cnt);
}
- /// brief Test generation of mulitple DUIDs
+ /// \brief Test generation of mulitple DUIDs
///
- /// Thie method checks the generation of multiple DUIDs. Number
+ /// This method checks the generation of multiple DUIDs. Number
/// of iterations depends on the number of simulated clients.
/// It is expected that DUID's size is 14 (consists of DUID-LLT
/// HW type field, 4 octets of time value and MAC address). The
@@ -496,7 +541,7 @@ public:
// Incremental transaction id generator will generate
// predictable values of transaction id for each iteration.
- // This is important because we need to simulate reponses
+ // This is important because we need to simulate responses
// from the server and use the same transaction ids as in
// packets sent by client.
TestControl::NumberGeneratorPtr
@@ -520,7 +565,8 @@ public:
boost::shared_ptr<Pkt6>
advertise_pkt6(createAdvertisePkt6(transid));
// Receive ADVERTISE and send REQUEST.
- ASSERT_NO_THROW(tc.processReceivedPacket6(sock, advertise_pkt6));
+ ASSERT_NO_THROW(tc.processReceivedPacket6(sock,
+ advertise_pkt6));
++transid;
}
if (tc.checkExitConditions()) {
@@ -613,6 +659,167 @@ public:
}
}
+ /// \brief Test that the DHCPv4 Release or Renew message is created
+ /// correctly and comprises expected options.
+ ///
+ /// \param msg_type A type of the message to be tested: DHCPV6_RELEASE
+ /// or DHCPV6_RENEW.
+ void testCreateRenewRelease(const uint16_t msg_type) {
+ // This command line specifies that the Release/Renew messages should
+ // be sent with the same rate as the Solicit messages.
+ std::ostringstream s;
+ s << "perfdhcp -6 -l lo -r 10 ";
+ s << (msg_type == DHCPV6_RELEASE ? "-F" : "-f") << " 10 ";
+ s << "-R 10 -L 10547 -n 10 -e address-and-prefix ::1";
+ ASSERT_NO_THROW(processCmdLine(s.str()));
+ // Create a test controller class.
+ NakedTestControl tc;
+ // Set the transaction id generator which will be used by the
+ // createRenew or createRelease function to generate transaction id.
+ boost::shared_ptr<NakedTestControl::IncrementalGenerator>
+ generator(new NakedTestControl::IncrementalGenerator());
+ tc.setTransidGenerator(generator);
+
+ // Create a Reply packet. The createRelease or createReply function will
+ // need Reply packet to create a corresponding Release or Reply.
+ Pkt6Ptr reply = createReplyPkt6(1);
+
+ Pkt6Ptr msg;
+ // Check that the message is created.
+ ASSERT_NO_THROW(msg = tc.createMessageFromReply(msg_type, reply));
+
+ ASSERT_TRUE(msg);
+ // Check that the message type and transaction id is correct.
+ EXPECT_EQ(msg_type, msg->getType());
+ EXPECT_EQ(1, msg->getTransid());
+
+ // Check that the message has expected options. These are the same for
+ // Release and Renew.
+
+ // Client Identifier.
+ OptionPtr opt_clientid = msg->getOption(D6O_CLIENTID);
+ ASSERT_TRUE(opt_clientid);
+ EXPECT_TRUE(reply->getOption(D6O_CLIENTID)->getData() ==
+ opt_clientid->getData());
+
+ // Server identifier
+ OptionPtr opt_serverid = msg->getOption(D6O_SERVERID);
+ ASSERT_TRUE(opt_serverid);
+ EXPECT_TRUE(reply->getOption(D6O_SERVERID)->getData() ==
+ opt_serverid->getData());
+
+ // IA_NA
+ OptionPtr opt_ia_na = msg->getOption(D6O_IA_NA);
+ ASSERT_TRUE(opt_ia_na);
+ EXPECT_TRUE(reply->getOption(D6O_IA_NA)->getData() ==
+ opt_ia_na->getData());
+
+ // IA_PD
+ OptionPtr opt_ia_pd = msg->getOption(D6O_IA_PD);
+ ASSERT_TRUE(opt_ia_pd);
+ EXPECT_TRUE(reply->getOption(D6O_IA_PD)->getData() ==
+ opt_ia_pd->getData());
+
+ // Make sure that exception is thrown if the Reply message is NULL.
+ EXPECT_THROW(tc.createMessageFromReply(msg_type, Pkt6Ptr()),
+ isc::BadValue);
+
+ }
+
+ /// \brief Test sending DHCPv6 Releases or Renews.
+ ///
+ /// This function simulates acquiring 10 leases from the server. Returned
+ /// Reply messages are cached and used to send Renew or Release messages.
+ /// The maxmimal number of Renew or Release messages which can be sent is
+ /// equal to the number of leases acquired (10). This function also checks
+ /// that an attempt to send more Renew or Release messages than the number
+ /// of leases acquired will fail.
+ ///
+ /// \param msg_type A type of the message which is simulated to be sent
+ /// (DHCPV6_RENEW or DHCPV6_RELEASE).
+ void testSendRenewRelease(const uint16_t msg_type) {
+ std::string loopback_iface(getLocalLoopback());
+ if (loopback_iface.empty()) {
+ std::cout << "Skipping the test because loopback interface could"
+ " not be detected" << std::endl;
+ return;
+ }
+ // Build a command line. Depending on the message type, we will use
+ // -f<renew-rate> or -F<release-rate> parameter.
+ std::ostringstream s;
+ s << "perfdhcp -6 -l " << loopback_iface << " -r 10 ";
+ s << (msg_type == DHCPV6_RENEW ? "-f" : "-F");
+ s << " 10 -R 10 -L 10547 -n 10 ::1";
+ ASSERT_NO_THROW(processCmdLine(s.str()));
+ // Create a test controller class.
+ NakedTestControl tc;
+ tc.initializeStatsMgr();
+ // Set the transaction id generator to sequential to control to
+ // guarantee that transaction ids are predictable.
+ boost::shared_ptr<NakedTestControl::IncrementalGenerator>
+ generator(new NakedTestControl::IncrementalGenerator());
+ tc.setTransidGenerator(generator);
+ // Socket has to be created so as we can actually send packets.
+ int sock_handle = 0;
+ ASSERT_NO_THROW(sock_handle = tc.openSocket());
+ TestControl::TestControlSocket sock(sock_handle);
+
+ // Send a number of Solicit messages. Each generated Solicit will be
+ // assigned a different transaction id, starting from 1 to 10.
+ tc.sendPackets(sock, 10);
+
+ // Simulate Advertise responses from the server. Each advertise is
+ // assigned a transaction id from the range of 1 to 10, so as they
+ // match the transaction ids from the Solicit messages.
+ for (int i = generator->getNext() - 10; i < generator->getNext(); ++i) {
+ Pkt6Ptr advertise(createAdvertisePkt6(i));
+ // If Advertise is matched with the Solicit the call below will
+ // trigger a corresponding Request. They will be assigned
+ // transaction ids from the range from 11 to 20 (the range of
+ // 1 to 10 has been used by Solicit-Advertise).
+ ASSERT_NO_THROW(tc.processReceivedPacket6(sock, advertise));
+ }
+
+ // Requests have been sent, so now let's simulate responses from the
+ // server. Generate corresponding Reply messages with the transaction
+ // ids from the range from 11 to 20.
+ for (int i = generator->getNext() - 10; i < generator->getNext(); ++i) {
+ Pkt6Ptr reply(createReplyPkt6(i));
+ // Each Reply packet corresponds to the new lease acquired. Since
+ // -f<renew-rate> option has been specified, received Reply
+ // messages are held so as Renew messages can be sent for
+ // existing leases.
+ ASSERT_NO_THROW(tc.processReceivedPacket6(sock, reply));
+ }
+
+ uint64_t msg_num;
+ // Try to send 5 messages. It should be successful because 10 Reply
+ // messages has been received. For each of them we should be able to
+ // send Renew or Release.
+ ASSERT_NO_THROW(
+ msg_num = tc.sendMultipleMessages6(sock, msg_type, 5)
+ );
+ // Make sure that we have sent 5 messages.
+ EXPECT_EQ(5, msg_num);
+
+ // Try to do it again. We should still have 5 Reply packets for
+ // which Renews or Releases haven't been sent yet.
+ ASSERT_NO_THROW(
+ msg_num = tc.sendMultipleMessages6(sock, msg_type, 5)
+ );
+ EXPECT_EQ(5, msg_num);
+
+ // We used all the Reply packets (we sent Renew or Release for each of
+ // them already). Therefore, no further Renew or Release messages should
+ // be sent before we acquire new leases.
+ ASSERT_NO_THROW(
+ msg_num = tc.sendMultipleMessages6(sock, msg_type, 5)
+ );
+ // Make sure that no message has been sent.
+ EXPECT_EQ(0, msg_num);
+
+ }
+
/// \brief Parse command line string with CommandOptions.
///
/// \param cmdline command line string to be parsed.
@@ -622,7 +829,6 @@ public:
CommandOptionsHelper::process(cmdline);
}
-private:
/// \brief Create DHCPv4 OFFER packet.
///
/// \param transid transaction id.
@@ -643,24 +849,80 @@ private:
///
/// \param transid transaction id.
/// \return instance of the packet.
- boost::shared_ptr<Pkt6>
- createAdvertisePkt6(uint32_t transid) const {
- OptionPtr opt_ia_na = Option::factory(Option::V6, D6O_IA_NA);
+ Pkt6Ptr
+ createAdvertisePkt6(const uint32_t transid) const {
+ boost::shared_ptr<Pkt6> advertise(new Pkt6(DHCPV6_ADVERTISE, transid));
+ // Add IA_NA if requested by the client.
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::ADDRESS)) {
+ OptionPtr opt_ia_na = Option::factory(Option::V6, D6O_IA_NA);
+ advertise->addOption(opt_ia_na);
+ }
+ // Add IA_PD if requested by the client.
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::PREFIX)) {
+ OptionPtr opt_ia_pd = Option::factory(Option::V6, D6O_IA_PD);
+ advertise->addOption(opt_ia_pd);
+ }
OptionPtr opt_serverid(new Option(Option::V6, D6O_SERVERID));
NakedTestControl tc;
uint8_t randomized = 0;
std::vector<uint8_t> duid(tc.generateDuid(randomized));
OptionPtr opt_clientid(Option::factory(Option::V6, D6O_CLIENTID, duid));
- boost::shared_ptr<Pkt6> advertise(new Pkt6(DHCPV6_ADVERTISE, transid));
- advertise->addOption(opt_ia_na);
advertise->addOption(opt_serverid);
advertise->addOption(opt_clientid);
advertise->updateTimestamp();
return (advertise);
}
+ Pkt6Ptr
+ createReplyPkt6(const uint32_t transid) const {
+ Pkt6Ptr reply(new Pkt6(DHCPV6_REPLY, transid));
+ // Add IA_NA if requested by the client.
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::ADDRESS)) {
+ OptionPtr opt_ia_na = Option::factory(Option::V6, D6O_IA_NA);
+ reply->addOption(opt_ia_na);
+ }
+ // Add IA_PD if requested by the client.
+ if (CommandOptions::instance().getLeaseType()
+ .includes(CommandOptions::LeaseType::PREFIX)) {
+ OptionPtr opt_ia_pd = Option::factory(Option::V6, D6O_IA_PD);
+ reply->addOption(opt_ia_pd);
+ }
+ OptionPtr opt_serverid(new Option(Option::V6, D6O_SERVERID));
+ NakedTestControl tc;
+ uint8_t randomized = 0;
+ std::vector<uint8_t> duid(tc.generateDuid(randomized));
+ OptionPtr opt_clientid(Option::factory(Option::V6, D6O_CLIENTID, duid));
+ reply->addOption(opt_serverid);
+ reply->addOption(opt_clientid);
+ reply->updateTimestamp();
+ return (reply);
+
+ }
+
};
+// This test verifies that the class members are reset to expected values.
+TEST_F(TestControlTest, reset) {
+ ASSERT_NO_THROW(processCmdLine("perfdhcp -6 -l ethx -r 50 -f 30 -F 10 -a 3 all"));
+ NakedTestControl tc;
+ tc.reset();
+ EXPECT_EQ(3, tc.basic_rate_control_.getAggressivity());
+ EXPECT_EQ(3, tc.renew_rate_control_.getAggressivity());
+ EXPECT_EQ(3, tc.release_rate_control_.getAggressivity());
+ EXPECT_EQ(50, tc.basic_rate_control_.getRate());
+ EXPECT_EQ(30, tc.renew_rate_control_.getRate());
+ EXPECT_EQ(10, tc.release_rate_control_.getRate());
+ EXPECT_FALSE(tc.last_report_.is_not_a_date_time());
+ EXPECT_FALSE(tc.transid_gen_);
+ EXPECT_FALSE(tc.macaddr_gen_);
+ EXPECT_TRUE(tc.first_packet_serverid_.empty());
+ EXPECT_FALSE(tc.interrupted_);
+
+}
+
TEST_F(TestControlTest, GenerateDuid) {
// Simple command line that simulates one client only. Always the
// same DUID will be generated.
@@ -736,7 +998,7 @@ TEST_F(TestControlTest, Options4) {
// Get the option buffer. It should hold the combination of values
// listed in requested_options array. However their order can be
- // different in general so we need to search each value separatelly.
+ // different in general so we need to search each value separately.
const OptionBuffer& requested_options_buf =
opt_requested_options->getData();
EXPECT_EQ(requested_options_ref.size(), requested_options_buf.size());
@@ -817,10 +1079,12 @@ TEST_F(TestControlTest, Options6) {
// Prepare the reference buffer with requested options.
const uint8_t requested_options[] = {
0, D6O_NAME_SERVERS,
- 0, D6O_DOMAIN_SEARCH,
+ 0, D6O_DOMAIN_SEARCH
};
- int requested_options_num =
- sizeof(requested_options) / sizeof(requested_options[0]);
+ // Each option code in ORO is 2 bytes long. We calculate the number of
+ // requested options by dividing the size of the buffer holding options
+ // by the size of each individual option.
+ int requested_options_num = sizeof(requested_options) / sizeof(uint16_t);
OptionBuffer
requested_options_ref(requested_options,
requested_options + sizeof(requested_options));
@@ -960,7 +1224,7 @@ TEST_F(TestControlTest, Packet4Exchange) {
EXPECT_EQ(12, iterations_performed);
}
-TEST_F(TestControlTest, Packet6Exchange) {
+TEST_F(TestControlTest, Packet6ExchangeFromTemplate) {
// Get the local loopback interface to open socket on
// it and test packets exchanges. We don't want to fail
// the test if interface is not available.
@@ -995,11 +1259,128 @@ TEST_F(TestControlTest, Packet6Exchange) {
// then test should be interrupted and actual number of iterations will
// be 6.
const int received_num = 3;
+ // Simulate the number of Solicit-Advertise-Request-Reply (SARR) echanges.
+ // The test function generates server's responses and passes it to the
+ // TestControl class methods for processing. The number of exchanges
+ // actually performed is returned in 'iterations_performed' argument. If
+ // processing is successful, the number of performed iterations should be
+ // equal to the number of exchanges specified with the '-n' command line
+ // parameter (10 in this case). All exchanged packets carry the IA_NA option
+ // to simulate the IPv6 address acquisition and to verify that the
+ // IA_NA options returned by the server are processed correctly.
testPkt6Exchange(iterations_num, received_num, use_templates,
iterations_performed);
EXPECT_EQ(6, iterations_performed);
}
+TEST_F(TestControlTest, Packet6Exchange) {
+ // Get the local loopback interface to open socket on
+ // it and test packets exchanges. We don't want to fail
+ // the test if interface is not available.
+ std::string loopback_iface(getLocalLoopback());
+ if (loopback_iface.empty()) {
+ std::cout << "Unable to find the loopback interface. Skip test."
+ << std::endl;
+ return;
+ }
+
+ const int iterations_num = 100;
+ // Set number of iterations to 10.
+ processCmdLine("perfdhcp -l " + loopback_iface
+ + " -e address-only"
+ + " -6 -r 100 -n 10 -R 20 -L 10547 ::1");
+ int iterations_performed = 0;
+ // Set number of received packets equal to number of iterations.
+ // This simulates no packet drops.
+ bool use_templates = false;
+
+ // Simulate the number of Solicit-Advertise-Request-Reply (SARR) echanges.
+ // The test function generates server's responses and passes it to the
+ // TestControl class methods for processing. The number of exchanges
+ // actually performed is returned in 'iterations_performed' argument. If
+ // processing is successful, the number of performed iterations should be
+ // equal to the number of exchanges specified with the '-n' command line
+ // parameter (10 in this case). All exchanged packets carry the IA_NA option
+ // to simulate the IPv6 address acqusition and to verify that the IA_NA
+ // options returned by the server are processed correctly.
+ testPkt6Exchange(iterations_num, iterations_num, use_templates,
+ iterations_performed);
+ // Actual number of iterations should be 10.
+ EXPECT_EQ(10, iterations_performed);
+}
+
+TEST_F(TestControlTest, Packet6ExchangePrefixDelegation) {
+ // Get the local loopback interface to open socket on
+ // it and test packets exchanges. We don't want to fail
+ // the test if interface is not available.
+ std::string loopback_iface(getLocalLoopback());
+ if (loopback_iface.empty()) {
+ std::cout << "Unable to find the loopback interface. Skip test."
+ << std::endl;
+ return;
+ }
+
+ const int iterations_num = 100;
+ // Set number of iterations to 10.
+ processCmdLine("perfdhcp -l " + loopback_iface
+ + " -e prefix-only"
+ + " -6 -r 100 -n 10 -R 20 -L 10547 ::1");
+ int iterations_performed = 0;
+ // Set number of received packets equal to number of iterations.
+ // This simulates no packet drops.
+ bool use_templates = false;
+
+ // Simulate the number of Solicit-Advertise-Request-Reply (SARR) echanges.
+ // The test function generates server's responses and passes it to the
+ // TestControl class methods for processing. The number of exchanges
+ // actually performed is returned in 'iterations_performed' argument. If
+ // processing is successful, the number of performed iterations should be
+ // equal to the number of exchanges specified with the '-n' command line
+ // parameter (10 in this case). All exchanged packets carry the IA_PD option
+ // to simulate the Prefix Delegation and to verify that the IA_PD options
+ // returned by the server are processed correctly.
+ testPkt6Exchange(iterations_num, iterations_num, use_templates,
+ iterations_performed);
+ // Actual number of iterations should be 10.
+ EXPECT_EQ(10, iterations_performed);
+}
+
+TEST_F(TestControlTest, Packet6ExchangeAddressAndPrefix) {
+ // Get the local loopback interface to open socket on
+ // it and test packets exchanges. We don't want to fail
+ // the test if interface is not available.
+ std::string loopback_iface(getLocalLoopback());
+ if (loopback_iface.empty()) {
+ std::cout << "Unable to find the loopback interface. Skip test."
+ << std::endl;
+ return;
+ }
+
+ const int iterations_num = 100;
+ // Set number of iterations to 10.
+ processCmdLine("perfdhcp -l " + loopback_iface
+ + " -e address-and-prefix"
+ + " -6 -r 100 -n 10 -R 20 -L 10547 ::1");
+ int iterations_performed = 0;
+ // Set number of received packets equal to number of iterations.
+ // This simulates no packet drops.
+ bool use_templates = false;
+ // Simulate the number of Solicit-Advertise-Request-Reply (SARR) echanges.
+ // The test function generates server's responses and passes it to the
+ // TestControl class methods for processing. The number of exchanges
+ // actually performed is returned in 'iterations_performed' argument. If
+ // processing is successful, the number of performed iterations should be
+ // equal to the number of exchanges specified with the '-n' command line
+ // parameter (10 in this case). All exchanged packets carry either IA_NA
+ // or IA_PD options to simulate the address and prefix acquisition with
+ // the single message and to verify that the IA_NA and IA_PD options
+ // returned by the server are processed correctly.
+ testPkt6Exchange(iterations_num, iterations_num, use_templates,
+ iterations_performed);
+ // Actual number of iterations should be 10.
+ EXPECT_EQ(10, iterations_performed);
+}
+
TEST_F(TestControlTest, PacketTemplates) {
std::vector<uint8_t> template1(256);
std::string file1("test1.hex");
@@ -1014,7 +1395,7 @@ TEST_F(TestControlTest, PacketTemplates) {
// Size of the file is 2 times larger than binary data size.
ASSERT_TRUE(createTemplateFile(file1, template1, template1.size() * 2));
ASSERT_TRUE(createTemplateFile(file2, template2, template2.size() * 2));
- CommandOptions& options = CommandOptions::instance();
+
NakedTestControl tc;
ASSERT_NO_THROW(
@@ -1058,27 +1439,166 @@ TEST_F(TestControlTest, PacketTemplates) {
EXPECT_THROW(tc.initPacketTemplates(), isc::BadValue);
}
-TEST_F(TestControlTest, RateControl) {
- // We don't specify the exchange rate here so the aggressivity
- // value will determine how many packets are to be send each
- // time we query the getNextExchangesNum.
- ASSERT_NO_THROW(processCmdLine("perfdhcp -l 127.0.0.1 all"));
- CommandOptions& options = CommandOptions::instance();
+TEST_F(TestControlTest, processRenew) {
+ testSendRenewRelease(DHCPV6_RENEW);
+}
- NakedTestControl tc1;
- uint64_t xchgs_num = tc1.getNextExchangesNum();
- EXPECT_EQ(options.getAggressivity(), xchgs_num);
+TEST_F(TestControlTest, processRelease) {
+ testSendRenewRelease(DHCPV6_RELEASE);
+}
+
+// This test verifies that the DHCPV6 Renew message is created correctly
+// and that it comprises all required options.
+TEST_F(TestControlTest, createRenew) {
+ testCreateRenewRelease(DHCPV6_RENEW);
+}
+
+// This test verifies that the DHCPv6 Release message is created correctly
+// and that it comprises all required options.
+TEST_F(TestControlTest, createRelease) {
+ testCreateRenewRelease(DHCPV6_RELEASE);
+}
+
+// This test verifies that the current timeout value for waiting for
+// the server's responses is valid. The timeout value corresponds to the
+// time period between now and the next message to be sent from the
+// perfdhcp to a server.
+TEST_F(TestControlTest, getCurrentTimeout) {
+ // Process the command line: set the rate for Discovers to 10,
+ // and set Renew rate to 0 (-f flag absent).
+ ASSERT_NO_THROW(processCmdLine("perfdhcp -4 -l lo -r 10 ::1"));
+ NakedTestControl tc;
+ // Make sure that the renew rate is 0.
+ ASSERT_EQ(0, CommandOptions::instance().getRenewRate());
+ // Simulate the case when we are already behind the due time for
+ // the next Discover to be sent.
+ tc.setRelativeDueTimes(-3);
+ // Expected timeout value is 0, which means that perfdhcp should
+ // not wait for server's response but rather send the next
+ // message to a server immediately.
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+ // Now, let's do set the due time to a value in the future. The returned
+ // timeout value should be somewhere between now and this time in the
+ // future. The value of ten seconds ahead should be safe and guarantee
+ // that the returned timeout value is non-zero, even though there is a
+ // delay between setting the send_due_ value and invoking the function.
+ tc.setRelativeDueTimes(10);
+ uint32_t timeout = tc.getCurrentTimeout();
+ EXPECT_GT(timeout, 0);
+ EXPECT_LE(timeout, 10000000);
+}
+
+// This test verifies that the current timeout value for waiting for the
+// server's responses is valid. In this case, we are simulating that perfdhcp
+// sends Renew requests to the server, apart from the regular 4-way exchanges.
+// The timeout value depends on both the due time to send next Solicit and the
+// due time to send Renew - the timeout should be ajusted to the due time that
+// occurs sooner.
+TEST_F(TestControlTest, getCurrentTimeoutRenew) {
+ // Set the Solicit rate to 10 and the Renew rate 5.
+ ASSERT_NO_THROW(processCmdLine("perfdhcp -6 -l lo -r 10 -f 5 ::1"));
+ NakedTestControl tc;
+
+ // Make sure, that the Renew rate has been set to 5.
+ ASSERT_EQ(5, CommandOptions::instance().getRenewRate());
+ // The send_due_ is in the past, the renew_due_ is in the future.
+ tc.setRelativeDueTimes(-3, 3);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ // Swap the due times from the previous check. The effect should be the
+ // same.
+ tc.setRelativeDueTimes(3, -3);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ // Set both due times to the future. The renew due time is to occur
+ // sooner. The timeout should be a value between now and the
+ // renew due time.
+ tc.setRelativeDueTimes(10, 5);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 5000000);
+
+ // Repeat the same check, but swap the due times.
+ tc.setRelativeDueTimes(5, 10);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 5000000);
+
+}
+
+// This test verifies that the current timeout value for waiting for the
+// server's responses is valid. In this case, we are simulating that perfdhcp
+// sends Release requests to the server, apart from the regular 4-way exchanges.
+TEST_F(TestControlTest, getCurrentTimeoutRelease) {
+ // Set the Solicit rate to 10 and the Release rate 5.
+ ASSERT_NO_THROW(processCmdLine("perfdhcp -6 -l lo -r 10 -F 5 ::1"));
+ NakedTestControl tc;
+
+ // Make sure, that the Release rate has been set to 5.
+ ASSERT_EQ(5, CommandOptions::instance().getReleaseRate());
+ // The send_due_ is in the past, the renew_due_ is in the future.
+ tc.setRelativeDueTimes(-3, 0, 3);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ // Swap the due times from the previous check. The effect should be the
+ // same.
+ tc.setRelativeDueTimes(3, 0, -3);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ // Set both due times to the future. The renew due time is to occur
+ // sooner. The timeout should be a value between now and the
+ // release due time.
+ tc.setRelativeDueTimes(10, 0, 5);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 5000000);
+
+ // Repeat the same check, but swap the due times.
+ tc.setRelativeDueTimes(5, 0, 10);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 5000000);
+
+}
+
+// This test verifies that the current timeout value for waiting for the
+// server's responses is valid. In this case, we are simulating that perfdhcp
+// sends both Renew and Release requests to the server, apart from the regular
+// 4-way exchanges.
+TEST_F(TestControlTest, getCurrentTimeoutRenewRelease) {
+ // Set the Solicit rate to 10 and, Renew rate to 5, Release rate to 3.
+ ASSERT_NO_THROW(processCmdLine("perfdhcp -6 -l lo -r 10 -f 5 -F 3 ::1"));
+ NakedTestControl tc;
+
+ // Make sure the Renew and Release rates has been set to a non-zero value.
+ ASSERT_EQ(5, CommandOptions::instance().getRenewRate());
+ ASSERT_EQ(3, CommandOptions::instance().getReleaseRate());
+
+ // If any of the due times is in the past, the timeout value should be 0,
+ // to indicate that the next message should be sent immediately.
+ tc.setRelativeDueTimes(-3, 3, 5);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ tc.setRelativeDueTimes(-3, 5, 3);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ tc.setRelativeDueTimes(3, -3, 5);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ tc.setRelativeDueTimes(3, 2, -5);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ tc.setRelativeDueTimes(-3, -2, -5);
+ EXPECT_EQ(0, tc.getCurrentTimeout());
+
+ // If due times are in the future, the timeout value should be aligned to
+ // the due time which occurs the soonest.
+ tc.setRelativeDueTimes(10, 9, 8);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 8000000);
+
+ tc.setRelativeDueTimes(10, 8, 9);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 8000000);
+
+ tc.setRelativeDueTimes(5, 8, 9);
+ EXPECT_GT(tc.getCurrentTimeout(), 0);
+ EXPECT_LE(tc.getCurrentTimeout(), 5000000);
- // The exchange rate is now 1 per second. We don't know how many
- // exchanges have to initiated exactly but for sure it has to be
- // non-zero value. Also, since aggressivity is very high we expect
- // that it will not be restricted by aggressivity.
- ASSERT_NO_THROW(
- processCmdLine("perfdhcp -l 127.0.0.1 -a 1000000 -r 1 all")
- );
- NakedTestControl tc2;
- xchgs_num = tc2.getNextExchangesNum();
- EXPECT_GT(xchgs_num, 0);
- EXPECT_LT(xchgs_num, options.getAggressivity());
- // @todo add more thorough checks for rate values.
}
More information about the bind10-changes
mailing list