Kea Migration Assistant
From:     https://gitlab.isc.org/isc-projects/keama-leases


Intro Kea Lease Migration keaMA Running Keama Leases
Reporting Issues Background Kea Lease File Format DHCPv4 Lease File in Kea
DHCPv6 Lease File in Kea Additional Considerations Example Files Converter Source Code
Useful Docs dhcp2kea.php




Keama Leases Project ID: 246 Name Last commit Last update
KeaMA Lease Migration This tool facilitates one-way migration from ISC DHCP, which is now End of Life, to Kea, a newer DHCP server, also from ISC. Translating and transferring active leases is only one step in the process of migration from the obsolete system to the newer one. Because the lease files record the active state of the system, this step should be done when the old system is ready to be decommissioned. Example files are provided for demonstration purposes only. The user should use their own current ISC DHCP lease file as the input file, and then load the output lease file into the target Kea server. This tool is used to migrate the lease file, which contains the running state ("address assignments" or "leases") of your DHCP server. The other, even more important aspect is migration of your configuration file, which can be done using KeaMA (the Kea Migration Assistant).
KeaMA (Kea Migration Assistant) To assist in your ISC DHCP server configuration migration to Kea configuration, KeaMA can be used. It is available from the ISC DHCP repository as source code, or from Cloudsmith as RPM and DEB packages. For details, see the KeaMA wiki page.
Running KeaMA-leases You need to get the dhcp2kea.py into your system. You can simply download it from this website or use git to clone the whole repository: git clone https://gitlab.isc.org/isc-projects/keama-leases.git You can run it with python interpreter. Any modern python 3 will do: $ python dhcp2kea/dhcp2kea.py Usage: python3 dhcp2kea.py ["[addr ]key=value"] [-e[dateTtime] [-v] [-h] [ -d] [-t] leases-filename(s) Options: -h : help - prints this help -v : verbose - print results to stdout -d : debug - print internal state "key=value" : default values for each lease "addr key=value" : default value for addr -e[dateTtime] : expire - overriding expire value with datetime (defaults to current) -t[n] : test - execute test/demo functions, optional n=1,2,3,4,6 (internal test no) defaults to all Key names: uid, subnet_id, client-hostname, lease_type, prefix_len, hwaddr, state, user_context, hwtype, hwaddr_source, expire, valid_liftime Examples: 1.setting subnet_id=2 for all leases: python3 dhcp2kea.py "subnet_id=2" example1-dhcpd4.leases 2.setting subnet_id=3 for 192.168.1.12 to 192.168.1.16 and subnet_id=2 for 192.168.1.20 to 192.168.1.29 python3 dhcp2kea.py "192.168.1.1[2-6] subnet_id=3" "192.168.1.2\d subnet_id=2" example1-dhcpd4.leases 3.setting subnet_id=3 for 192.168.1.12 and subnet_id=8 for 3ffe:501:ffff:100::c9d9 in two processed files python3 dhcp2kea.py "192.168.1.12 subnet_id=3" example1-dhcpd4.leases "3ffe:501:ffff:100::c9d9 subnet_id=8" example2-dhcpd6.leases 4.setting expiration time to January,4th 2023 10 o'clock localtime for all leases python3 dhcp2kea.py -e2023/01/04T10:00:00 example1-dhcpd4.leases 5.setting liftime of each lease starting from current time for internal test no 6 (dhcp6 string) python3 dhcp2kea.py "valid_liftime=3600" -e -t6 Typical usage would take a single parameter - the lease file you want to convert. Since there is no concept of subnet-id in the ISC DHCP and subnet -ids are required in Kea, there is an additional step. Typically, once your configuration is migrated using KeaMA, you should enable lease checks and set it to fix. This will tell Kea to attempt to find appropriate subnets from the configuration and correct subnet-ids for the leases. To do so, your Kea configuration should contain the following snippet: "sanity-checks": { "lease-checks": "fix-del" }, For more details, see Kea ARM Section 8.2.25 and Kea ARM Section 9.2.25.
Reporting issues To report issues about KeaMA-leases, use this link. To report issues about KeaMA, use this link. Please make sure to either mention KeaMA by name in the issue title or add keama label if you have sufficient privileges.
Background During its operation, the DHCP server temporarily assigns (leases) IPv4 addresses, IPv6 addresses, and IPv6 prefixes to end devices. The devices (clients) renew leases and eventually the leases are returned to the server when the client goes off-line. Each time a lease state changes, an entry is written to a lease file. A lease file is essentially a journal file. The same principle is used by both ISC DHCP server and Kea servers, however the syntax is different. The goal of this project is to come up with a software solution that would allow converting ISC DHCP lease file to Kea lease file syntax. Since the lease file typically contains many entries for the same lease (e.g. an address is leased to client A, client A renews 3 times, client A goes away and the address expires, server then grants the address to client B) would result in many lease entries. From the conversion perspective, only the last entry is important. The earlier entries can be discarded. There are two versions of the DHCP protocol. DHCP for IPv4 addresses is referred to as DHCPv4, and DHCPv6 handles IPv6 addresses and prefixes. While the overall concept is the same, there are some differences. DHCPv4 uses the concept of MAC address and client-id whereas DHCPv6 uses the DUID to identify clients. DHCPv6 has a preferred lease lifetime in addition to the valid lifetime parameter in DHCPv4. DHCPv6 leases can be for an address (ia_na) or prefix (ia_pd). The prefix has an additional parameter prefix length. For addresses, the prefix length should be 128.
Kea lease file format - the target Kea uses CSV notation for lease files. The columns have the following meaning: address: IPv4 or IPv6 address hwaddr: hardware (MAC) address (this field is DHCPv4 only) client_id: client identifier (this field is DHCPv4 only) duid: DHCP Unique Identifier, similar to MAC address, but may be longer (this field is DHCPv6 only) valid_lifetime: expressed in seconds pref_lifetime: expressed in seconds (this field is DHCPv6 only) lease_type: specifies type of a lease - address or prefix (this field is DHCPv6 only) iaid: Identity Association Identifier, a value client sends in DHCPv6 (this field is DHCPv6 only) prefix_len: prefix length (1-128 for prefixes, always 128 for addresses) (this field is DHCPv6 only) expire: expiration time, expressed as UNIX time (seconds since midnight 1 Jan 1970 UTC) subnet_id: identifier of the subnet the lease belongs to fqdn_fwd - a boolean flag specifying if forward DNS update (A or AAAA record) was conducted (forward fully qualified domain name) fqdn_rev - a boolean flag specifying if a reverse DNS update (PTR record) was conducted (reverse fully qualified domain name) hostname - name of the host state - state of the lease, typically 0 user_context - contains any additional information and for this migration may be left empty. hwtype - hardware type, can be left empty hwaddr_source - source of the hardware address, can be left empty
DHCPv4 lease file in Kea: address,hwaddr,client_id,valid_lifetime,expire,subn et_id,fqdn_fwd,fqdn_rev,hostname,state,user_context 192.0.2.4,3a:3b:3c:3d:3e:3f,33:30,40,1642000000,50,1,1,example -workstation1,0, 192.0.2.5,,31:32:33,40,1643210000,50,1,1,,1,{ } 192.0.2.6,32:32,,40,1643212345,50,1,1,three&#x2cexample&#x2ccom,2,{ "a": 1&#x2c "b": "c" }
DHCPv6 lease file in Kea: address,duid,valid_lifetime,expire,subnet_id,pref_l ifetime,lease_type,iaid,prefix_len,fqdn_fwd,fqdn_rev ,hostname,hwaddr,state,user_context,hwtype,hwaddr_source 2001:db8::10,32:30,30,1642000000,1,50,1,60,70,1,1,example -workstation1,38:30,0,,90,16 2001:db8:abcd::11,32:31,30,1643210000,2,50,1,60,70,1,1,,38:30,1,{ },90,1 2001:db8:5555::12,32:32,30,1643212345,3,50,1,60,70,1,1,three &#x2cexample&#x2ccom,38:30,2,{ "a": 1&#x2c "b": "c" },90,4
Additional considerations
  1. subnet_id - Both servers have defined subnets that the addresses are allocated from. Kea requires the use of subnet-ids, which are non-existent in ISC DHCP. For the sake of migration, two approaches could be used. One is to simply use "subnet_id" and run kea with sanity-checks set to repair the values, as described in the Kea ARM. Another approach would be to use the Kea migration assistant (keama), see keama article or keama sources.
  2. uid to client-id - The field is called uid in ISC DHCP and client-id in Kea. The formatting is different. ISC DHCP uses a colon-separated hexadecimal list or as a quoted string. If it is recorded as a quoted string and it contains one or more non-printable characters, those characters are represented as octal escapes - a backslash character followed by three octal digits. Kea just uses hex notation separated by colons (similar to MAC addresses). The migration tool will handle this translation.
  3. hostname - Since Kea uses commas to separate values, the actual comma needs to be encoded with &#x2c. The migration tool will handle this translation.
  4. Consider the size of your lease. We are aware of lease files that are over 2GB, although many of those are just renewing the same leases many times. Loading the entire lease file to memory may not the best strategy.
  5. Leases have a start time, valid lifetime and expiration time bound by a simple rule: start + lifetime = expiration. ISC DHCP records start and end (as absolute value in human readable form), while Kea uses valid-lifetime (in seconds) and expiration time in UNIX time (seconds since midnight 1 jan 1970 UTC). The migration tool will handle this translation.
  6. The fqdn_fwd and fqdn_rev fields are not explicitly specified in ISC DHCP. They could be set to true if hostname is non-empty. It may be useful to add a knob to overwrite their values as needed. This is a piece of local network configuration that the admin doing conversion should know if the DNS updates were done or not.
  7. Contrary to its name, the ISC DHCP lease file may contain other information, in addition to the leases. This information will not be migrated to the Kea lease file.
    Example files Several files were added to examples/ directory. These are:
    • example1-dhcp4.conf - config file for ISC DHCP. This is not really needed, except perhaps it's useful to generate more data.
    • example1-dhcp4.leases - this file contains lease information. This is the input file.
    • example1-kea-leases4.csv - this is the converted (output) file.
    • example1-kea4.conf - this is an example kea config file that allows starting Kea and loading the .csv file.
    The conversion was done by hand and may contain some mistakes.
    Converter source code dhcp2kea/ directory contains:
    • dhcp2kea.py - converter source code written in Python3
    • dhcp2kea.php - the driver for running the converter from a web-browser with simple user interface
    • dhcp2kea.html - web interface for testing the converter with syntax colored items
    • dhcp2kea.png - web interface screendump

    Useful documentation
    • On-line version of the dhcp.leases man page: https://kb.isc.org/docs/isc -dhcp-41-manual-pages-dhcpdleases. The actual man page is available in dhcp source code


dhcp2kea.php <html> <head> <meta charset="UTF-8"> </head> <script> function conv() { convert( document.getElementById( 'ta' ).innerHTML .replace( /<[^>]*>?/gm,''),document.getElementById('ta2')); } function k( e ) { if ((e.ctrlKey || e.metaKey) ) { if(e.keyCode == 19) { if( confirm("Save?") ) local Storage["dhcp2kea"] = document.getElementById("ta").innerHTML; } if( e.keyCode == 18) { if(("dhcp2kea" in localStorage) && confirm("Restore?") ) document.getElementById( "ta" ). innerHTML=localStorage["dhcp2kea"]; } } if( (e.ctrlKey || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) { e.preventDefault(); conv(); // var e=document.getElementById("title");e.inne rHTML=e.innerHTML.replace(/ \([\s\S]*?\)/g, ''); } } function tabulate( csv, p ) { var ss = csv.split('\n'); var s = '<table style="text-align:center">'; for( var i=1;i<ss.length-1;i++) { s += i==1?'<tr style="color:white;background-color:#ee4 444;">':i%2==1?'<tr style="">':'<tr>'; var ss2= ss[i].split(','); if(ss2.length>1) for(var j=0;j<ss2.length;j++) { s += (i==1?'<th>':'<td>') + ss2[j] + (i==1?'</th>':'</td>'); } s += '</tr>'; } p.innerHTML = s +'</table>'; } function convert( s, ta2 ) { var xhttp = new XMLHttpRequest(); xhttp.open( "POST", "dhcp2kea.php" , true); xhttp.setRequestHeader( 'Content-type', 'application/x-www-form -urlencoded'); xhttp.onload = function() { ta2.innerHTML = xhttp. responseText.split( '\n' ).slice( 1 ).join( '\n' ); tabulate( xhttp.responseText, document.getElementById( 'p' )); }; xhttp.send( "ta=" + encodeURIComponent( s ) ); } function colored( s ) { // syntax coloring var w = '(\nlease|starts|ends|hardware ethernet|client -hostname| \\buid|\nia -na|\nia -pd|max -life|preferred -life|iaaddr|set ddns -fwd -name =)'; s = s.replace( /(\>|\<)/gm, /* < > */ function( m ) { return( "<span style='color:red'>"+(m=='>'?'> ':'<')+'</span>' ); } ); s = s.replace( new RegExp( w, "g") , "<span style='color:blue'>$&</span>"); s = s.replace( /\/\/.*/g , "<span style='color:gray'>$&</span>"); // comment s = s.replace(/\/\*(.|\n)*?\*\//gm,"<span style='color:gray'&g t;$&</span>"); //comment s = s.replace(/"(.|\n)*?"/gm, function( m ) { return( "<span style='color:green'>"+m.replac e(/<[^>]*>?/gm,'')+"</span>"); } ); //string return( s ); } function colored2( s, flag) { // syntax coloring, when flag is true with line numbering var w = '(class|def |import)'; var w1 = '(self|len|range|join|replace)'; var w2 = '(for |if |in |try:|except:|else:|elif|return|print)'; // s = s.replace(/(\>|\<)/gm, /* < > */ // function( m ) // { // return "<span style='color:red'>"+(m=='>'? // '>':'<')+'</span>';}); s = s.replace( /\b(\d+)\b/gm ,"<span style='color:red'>$&</span>"); // number s = s.replace( new RegExp(w,"g") , "<span style='color:blue'>$&</span>"); s = s.replace( new RegExp( w1,"g"),"<span style='color:darkma genta'>$&</span>"); s = s.replace( new RegExp( w2,"g"),"<span style='font-weight: bold;'>$&</span>"); s = s.replace( /(#.*$)/gm, function( m ) { return("<span style='color:gray'>"+m.repla ce(/<[^>]*>?/gm,'')+"</span>"); }) // python comment // s = s.replace( /\/\/.*/g // ,"<span style='color:gray'>$&</span>"); // C++ // comment // s = s.replace(/\/\*(.|\n)*?\*\//gm,"<span style='color:gray' // >$&</span>"); // C comment // s = s.replace(/"(.|\n)*?"/gm // ,"<span style='color:green'>$&</span>"); // string // s = s.replace( /"""(.|\n)*?"""/gm, // function( m ) // { // return( "<span style='color:green'>"+m.replac // e(/<[^>]*>?/gm,'')+"</span>"); // } //); //string s = s.replace( /"(.|\n)*?"/gm, function( m ) { return( "<span style='color:green'>"+m.replac e(/<[^>]*>?/gm,'')+"</span>"); } ); //string var ss = s.split('\n'); flag = flag || false; if( flag ) { s = ''; for( var i =0; i < ss.length; i++) { // make line numbers s += '<span style="color:gray;border-right: thin solid gray; ">' + (" " + (i+1)).slice(-4) + ' </span> '+ ss[i] + '\n'; } } return(s); } function readFile( file ) { var f = new XMLHttpRequest(); f.open( "GET", file, false); f.onreadystatechange = function () { if( f.readyState === 4) { if(f.status === 200 || f.status == 0) { document.getElementById('ta').innerHTML = colored( f.responseText); document.getElementById( 'ta' ).focus(); } } } f.send( null ); } function readLocalFile( e ) { var file = e.files[0]; if( !file ) { return; } var reader = new FileReader(); reader.onload = function( e ) { document.getElementById('ta').innerHTML = colored(e.target.result); document.getElementById('ta').focus(); }; reader.readAsText( file ); } function saveFile( ta, fname ) { var text = ta.innerHTML.replace(/\n/g, "\r\n"); var blob = new Blob( [text], { type: "text/plain"}); var a = document.createElement("a"); a.download = fname; a.href = window.URL.createObjectURL( blob ); a.target ="_blank"; a.style.display = "none"; document.body.appendChild( a ); a.click(); document.body.removeChild( a ); } function html2md( s ) { var html = s .replace(/^\<h1\>(.*?)\<\/h1\>/gim,'# $1') .replace(/^\<h2\>(.*?)\<\/h2\>/gim,'# $2') .replace(/^\<h3\>(.*?)\<\/h3\>/gim,'# $3') return( html ); } function md2html( s ) { var html = s .replace(/^#### (.*$)/gim,'<h4 id="$1">$1</h4>') .replace(/^### (.*$)/gim,'<h3 id="$1">$1</h3>') .replace(/^## (.*$)/gim,'<h2>$1</h2>') .replace(/^# (.*$)/gim,'<h1>$1</h1>') .replace(/^\> (.*$)/gim,'<blockquote>$1</blockquote>') .replace(/\*\*(.*)\*\*/gim,'<b>$1</b>') .replace(/\*(.*)\*/gim,'<i>$1</i>') .replace(/!\[(.*?)\]\((.*?)\)/gim, "<br><br><center><img alt='$1' src = '$2' width=720/></center>") .replace( /\[(.*?)\]\((.*?)\)/gim,"<a href='$2'>$1</a>") .replace( /\n\- (.*$)/gim,'\n<ul><li>$1</li></ul>') .replace(/(\<\/ul\>\n(.*)\<ul\>*)+/gm,'\n') .replace(/\n\d+\. (.*$)/gim,'\n<ol><li>$1</li></ol>') .replace(/(\<\/ol\>\n(.*)\<ol\>*)+/gm,'\n') // .replace(/\n$/gim,"<br />") .replace( /```([\s\S]+?)```/gim, function( m ) { return '<pre><div wrap="off" style="margin-left:32;width:640;min -height:128;max-height:420;-moz-appearance: textfield-multiline; -webkit -appearance: textarea;overflow:auto;resize: both;background -color:#eeeeee">' +colored2(html2md(m).replace(/```(.*) +\n/gim,'').replace(/```$/gim,''),true) +'</div></pre>'; } ) return( html.trim() ); } function f_onload() { var e = document.getElementById( "ta" ); e.innerHTML = colored( e.innerHTML ); var fname="dhcp2kea.md" var f = new XMLHttpRequest(); f.open("GET", fname, false); f.onreadystatechange = function () { if( f.readyState === 4) { if( f.status === 200 || f.status == 0) { document.getElementById('md').innerHTML = md2html(f.responseText); } } } f.send( null ); } </script> <body onload='f_onload();'> <center> <h1 id="title">dhcp2kea</h1> <table style="width:960px"> <tr> <td style="width:480px"> <div style="color:white;background-color:#009900;width:480px;">  <input type="file" title="Load local lease file" onchange="javascript:readLocalFile(this);" />  <button title="Load example 1" onclick="readFile('example1 -dhcpd4.leases')">1</button>  <button title="Load example 2" onclick="readFile('example2 -dhcpd6.leases')">2</button>  <button title="Load example 3" onclick="readFile('example3 -dhcpd6.leases')">3</button> </div> <!-- <textarea autofocus id="ta" name="ta" onkeypress="k(event);" autofocus style="width:480px;height:480px;font-family:courier;font -size:14"><?=$src?></textarea> --> <pre style="margin:0;"> <div id="ta" contenteditable="true" onkeypress="k(event);" style="width:476px;height:480px;border:1px solid #ccc;padding:2px; font-size:14px;overflow:auto; word-break: unset;word-wrap: unset;overflow-wrap: unset; -webkit-hyphens: unset;-moz-hyphens: unset;-ms-hyphens: unset;hyphens: unset; background-color:white;text-align:left; -moz-appearance: textfield-multiline;-webkit-appearance: textarea; resize: both; "># The format of this file is documented in the dhcpd.leases(5) manual page. # This lease file was written by isc-dhcp-4.4.2 # authoring-byte-order entry is generated, DO NOT DELETE authoring-byte-order little-endian; server-duid 00:01:00:01:28:f8:2c:53:08:00:27:c7:c6:20; ia-na cc:bb:aa:00:00:03:00:01:31:32:33:34:35:36 { cltt 2 2021/10/12 11:22:10; iaaddr 3002::200 { binding state active; preferred-life 225; max-life 360; ends 2 2021/10/12 11:28:10; set ddns-fwd-name = "toms.four.example.com"; set ddns-dhcid = "\000\002\001'\223`\344\240\350\230:[\264\220va3p -\335R\253\233\215\354\315%\346\034_\351\206k\002\224"; set ddns-rev-name = "0.0.2.0.0.0.0.0.0.0.0.0.0.0 .0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.0.3.ip6.arpa."; } } server-duid 00:01:00:01:28:f8:2c:53:08:00:27:c7:c6:20; ia-na cc:bb:aa:00:00:03:00:01:31:32:33:34:35:36 { cltt 2 2021/10/12 11:22:10; iaaddr 3002::200 { binding state expired; preferred-life 225; max-life 360; ends 2 2021/10/12 11:28:10; } } </div> </pre> </td> <td style="width:480px"> <div style="color:white;background-color:#0099cc;width:480px;"> <span style="position:relative;left:-20px;"><button title="Convert" onclick="conv();">>></span></button> <button style="float:right" title="Save to local disk" onclick="saveFile(document.getElementById('ta2'),'a.csv')">Save</button> </div> <pre style="margin:0"> <div id="ta2" name="ta2" style="width:476px;height:480px;border:1px solid #ccc;padding:2px; font-size:14px;overflow:auto;background-color:white;text-align:left; -moz-appearance: textfield-multiline;-webkit-appearance: textarea; resize: both; "> </div> </pre> </td> </tr> <tr> <td> </td> </tr> <tr> <td colspan=2"> <pre><div id="p" style="width:966px;overflow:auto;resize:both;-moz -appearance:textfield-multiline;-webkit-appearance: textarea;"></div></pre> </td> </tr> </table> </center> <hr> <div id="md" style="width:960"></div> </body></html>