Skip to content

Commit

Permalink
Merge pull request #31 from virtapi/feat-isc-dhcp
Browse files Browse the repository at this point in the history
Feat isc dhcp
  • Loading branch information
bastelfreak committed Feb 15, 2016
2 parents 916fe1b + dc26635 commit 304396c
Show file tree
Hide file tree
Showing 11 changed files with 581 additions and 4 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,61 @@ Errormessage if you want to delete or list a nonexistent entry:

---

## Setup LDAP + isc-dhcpd
We will do all this in a clean Arch nspawn container (login for a new container is always root without password):
```bash
mkdir marmoset_container
sudo pacman -Syu arch-install-scripts
sudo pacstrap -c -d marmoset_container
sudo systemd-nspawn -b -D marmoset_container/
pacman -Syu git openldap
```

Installing the isc-dhcpd is currently a bit tricky because the default package is not linked against ldap. You can get the sources and replace the PKGBUILD with [mine](https://p.bastelfreak.de/U6f5/) or use the package that I [built](https://p.bastelfreak.de/C6An/). The needed data can be found [here](https://p.bastelfreak.de/t5XS/). Please adjust the params `suffix` and `rootdn` for your needs (in /etc/openldap/slapd.conf), we use `dc=example,dc=com` and `cn=root,dc=example,dc=com` in our example.

```bash
curl -JO https://p.bastelfreak.de/C6An/
pacman -U dhcp*.pkg.tar.xz
cd /etc/openldap/schema
curl -JO https://raw.githubusercontent.com/dcantrell/ldap-for-dhcp/master/dhcp.schema
cd ..
echo 'include /etc/openldap/schema/dhcp.schema' >> slapd.conf
echo 'index dhcpHWAddress eq' >> slapd.conf
echo 'index dhcpClassData eq' >> slapd.conf
cp DB_CONFIG.example /var/lib/openldap/openldap-data/DB_CONFIG
curl https://p.bastelfreak.de/j63 > initial_data.ldif
systemctl start slapd
ldapadd -x -W -D 'cn=root,dc=example,dc=com' -f initial_data.ldif -c
```

Last step, you need to add the following settings to your `/etc/dhcpd.conf` before you start the daemon:
```
ldap-server "localhost";
ldap-port 389;
ldap-username "cn=root, dc=example, dc=com";
ldap-password "secret";
ldap-base-dn "dc=example, dc=com";
ldap-method dynamic;
ldap-debug-file "/var/log/dhcp-ldap-startup.log";
```

```bash
systemctl start dhcpd4.service
```

We also provide a prepacked nspawn container. It has a working openldap + DHCP server,
the openldap is configured to start at boot. Systemd is so awesome that it supports downloading the tar,
so no fiddeling with curl/wget. machinectl will throw the image into a btrfs subvol and requires you to run /var/lib/machines on btrfs:
```bash
pacman -Syu btrfs-progs
modprobe loop
machinectl --verify=no pull-tar https://bastelfreak.de/marmoset_container.tar marmoset_container
machinectl start marmoset_container
machinectl login marmoset_container
```

If you don't run btrfs you can still download the tar to /var/lib/machines, extract it by hand and then continue with the machinectl commands (or start it oldschool like with systemd-nspawn).

## Issues

Find this code at [the git repo](https://www.github.com/virtapi/marmoset/). Find the original code at [the git repo](https://www.aibor.de/cgit/marmoset/).
Expand Down
7 changes: 5 additions & 2 deletions marmoset/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from . import config, cli
from marmoset import config
from marmoset import cli
from marmoset import validation

def run(config_file = None):

def run(config_file=None):
cfg = config.load(config_file)
cli.parse(cfg)

4 changes: 3 additions & 1 deletion marmoset/app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from . import config, webserver

app = webserver.app(config.load())
config = config.load()

app = webserver.app(config)

8 changes: 7 additions & 1 deletion marmoset/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from os import path
import configparser, warnings
import configparser
import warnings
import socket

PATH = path.join(path.dirname(__file__), '../marmoset.conf')

def default():
config = configparser.ConfigParser()

config['Common'] = dict(
FQDN=socket.getfqdn()
)

config['Modules'] = dict(
Webserver = 'True',
PXE = 'True',
Expand Down
2 changes: 2 additions & 0 deletions marmoset/dhcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .dhcp_config import DhcpConfig
from .isc_dhcp_ldap_config import ISCDhcpLdapConfig
80 changes: 80 additions & 0 deletions marmoset/dhcp/dhcp_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from .isc_dhcp_ldap_config import ISCDhcpLdapConfig
from marmoset import validation
from marmoset import config as config_reader

config = config_reader.load()


class DhcpConfig:
def __init__(self, mac, ip_address, gateway=None, networkmask=None):
self.additional_statements = {}

self.mac = None
self.ip_address = None
self.gateway = None
self.networkmask = None
self.dhcp_hostname = None

self.set_settings(True, mac, ip_address, gateway, networkmask)

def set_settings(self, allow_none_value_for_not_required_parameter=False, mac=None, ip_address=None,
gateway=None, networkmask=None):
self.mac = mac

if gateway is not None or allow_none_value_for_not_required_parameter:
self.gateway = gateway

if networkmask is not None or allow_none_value_for_not_required_parameter:
self.networkmask = networkmask

if validation.is_cidr(ip_address):
self.ip_address = validation.get_ip_from_cidr(ip_address)

if self.networkmask is None:
self.networkmask = validation.get_nm_from_cidr(ip_address)

if self.gateway is None:
self.gateway = validation.get_gw_from_cidr(ip_address)
else:
self.ip_address = ip_address

self.dhcp_hostname = config['Common'].get('FQDN')

def add_additional_statement(self, key, value):
self.additional_statements[key] = value

def create_isc_ldap(self):
isc_dhcp_config = ISCDhcpLdapConfig(self)
isc_dhcp_config.save()

def remove_by_ipv4(self):
return ISCDhcpLdapConfig.remove_by_ipv4(self.ip_address) > 0

def remove_by_mac(self):
return ISCDhcpLdapConfig.remove_by_mac(self.mac) > 0

def remove_all(self):
ipv4_removed_count = ISCDhcpLdapConfig.remove_by_ipv4(self.ip_address)
mac_removed_count = ISCDhcpLdapConfig.remove_by_mac(self.mac)

return (ipv4_removed_count + mac_removed_count) > 0

@staticmethod
def all():
return ISCDhcpLdapConfig.all()

@staticmethod
def get_by_ip(ip_address):
return ISCDhcpLdapConfig.get_by_ip(ip_address)

@staticmethod
def get_by_mac(mac):
return ISCDhcpLdapConfig.get_by_mac(mac)

@staticmethod
def exists_ipv4(ip_address):
return ISCDhcpLdapConfig.get_by_ip(ip_address) is not None

@staticmethod
def exists_mac(mac_address):
return ISCDhcpLdapConfig.get_by_mac(mac_address) is not None
200 changes: 200 additions & 0 deletions marmoset/dhcp/isc_dhcp_ldap_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from ldap3 import Server, Connection, ALL, SUBTREE, BASE, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
from datetime import datetime
from marmoset import config as config_reader

import re

config = config_reader.load()


class ISCDhcpLdapConfig:
def __init__(self, dhcp_config):
self.dhcp_config = dhcp_config

@staticmethod
def __get_server_connection():
server = Server(config['DHCPConfig'].get('ldap_server'),
port=int(config['DHCPConfig'].get('ldap_port')),
get_info=ALL)

conn = Connection(server,
config['DHCPConfig'].get('ldap_bind_dn'),
config['DHCPConfig'].get('ldap_passwort'),
auto_bind=True)

return conn

def save(self):
conn = self.__get_server_connection()

dhcpStatements = ["fixed-address %s;" % self.dhcp_config.ip_address,
"option subnet-mask %s;" % self.dhcp_config.networkmask,
"option routers %s;" % self.dhcp_config.gateway]

for additional_statement in self.dhcp_config.additional_statements:
dhcpStatements.append("%s %s;" % (additional_statement,
self.dhcp_config.additional_statements[additional_statement]))

entry_attributes = {'dhcpHWAddress': "ethernet %s" % self.dhcp_config.mac,
'dhcpStatements': dhcpStatements,
'dhcpComments': "date=%s dhcp-hostname=%s" % (datetime.now().strftime("%Y%m%d_%H%M%S"),
self.dhcp_config.dhcp_hostname)}

conn.add("cn=%s,%s" % (self.dhcp_config.ip_address, config['DHCPConfig'].get('ldap_client_base_dn')),
'dhcpHost',
entry_attributes)

@staticmethod
def all():
conn = ISCDhcpLdapConfig.__get_server_connection()

entry_generator = conn.extend.standard.paged_search(search_base=config['DHCPConfig'].get('ldap_client_base_dn'),
search_filter='(objectClass=dhcpHost)',
search_scope=SUBTREE,
attributes=['cn'],
paged_size=5,
generator=True)
result = []
for entry in entry_generator:
result.append(ISCDhcpLdapConfig.get_by_ip(entry['attributes']['cn'][0]))

return result

@staticmethod
def __get_dn_by_ipv4(ip_address, multi=False):
conn = ISCDhcpLdapConfig.__get_server_connection()
conn.search(search_base=config['DHCPConfig'].get('ldap_client_base_dn'),
search_filter='(cn=%s)' % ip_address,
search_scope=SUBTREE,
paged_size=5,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])

entries = conn.response

if entries is None or len(entries) == 0:
if multi:
return []
return None

if multi:
dn_list = []
for entry in entries:
dn_list.append(entry['dn'])

return dn_list

return entries[0]['dn']

@staticmethod
def __get_dn_by_mac(mac_address, multi=False):
conn = ISCDhcpLdapConfig.__get_server_connection()
conn.search(search_base=config['DHCPConfig'].get('ldap_client_base_dn'),
search_filter='(dhcpHWAddress=ethernet %s)' % mac_address,
search_scope=SUBTREE,
paged_size=5,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])

entries = conn.response

if entries is None or len(entries) == 0:
if multi:
return []
return None

if multi:
dn_list = []
for entry in entries:
dn_list.append(entry['dn'])

return dn_list

return entries[0]['dn']

@staticmethod
def __get_dhcp_config(dn):
from marmoset.dhcp import DhcpConfig

conn = ISCDhcpLdapConfig.__get_server_connection()
conn.search(search_base=dn,
search_filter='(objectClass=dhcpHost)',
search_scope=SUBTREE,
paged_size=5,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES])

entries = conn.response

if len(entries) == 0:
return None

mac_option = str(entries[0]['attributes']['dhcpHWAddress'])

regex_gateway = 'option routers\s+([0-9]+.[0-9]+.[0-9]+.[0-9]+)'
regex_networkmask = 'option subnet-mask\s+([0-9]+.[0-9]+.[0-9]+.[0-9]+)'

mac = re.search('(([0-9a-f]{2}:){5}[0-9a-f]{2})', mac_option).group(0)
ip = entries[0]['attributes']['cn'][0]

gateway = None
networkmask = None

for dhcpStatement in entries[0]['attributes']['dhcpStatements']:
if re.match(regex_gateway, dhcpStatement):
gateway = re.search(regex_gateway, dhcpStatement).group(1)

if re.match(regex_networkmask, dhcpStatement):
networkmask = re.search(regex_networkmask, dhcpStatement).group(1)

dhcp_config = DhcpConfig(mac, ip, gateway, networkmask)

additional_statements_str = config['DHCPConfig'].get('additional_statements')
additional_statements = additional_statements_str.split(',')

for ldap_additional_statement in entries[0]['attributes']['dhcpStatements']:
for additional_statement in additional_statements:
regex_additional_statement = '%s\s+(.*);' % additional_statement

if re.match(regex_additional_statement, ldap_additional_statement):
value = re.search(regex_additional_statement, ldap_additional_statement).group(1)
dhcp_config.add_additional_statement(additional_statement, value)

return dhcp_config

@staticmethod
def get_by_ip(ip_address):
dn = ISCDhcpLdapConfig.__get_dn_by_ipv4(ip_address)

if dn is None:
return None

return ISCDhcpLdapConfig.__get_dhcp_config(dn)

@staticmethod
def get_by_mac(mac_address):
dn = ISCDhcpLdapConfig.__get_dn_by_mac(mac_address)

if dn is None:
return None

return ISCDhcpLdapConfig.__get_dhcp_config(dn)

@staticmethod
def remove_by_ipv4(ipv4):
dn_list = ISCDhcpLdapConfig.__get_dn_by_ipv4(ipv4, multi=True)

for dn in dn_list:
conn = ISCDhcpLdapConfig.__get_server_connection()
conn.delete(dn)
conn.unbind()

return len(dn_list)

@staticmethod
def remove_by_mac(mac):
dn_list = ISCDhcpLdapConfig.__get_dn_by_mac(mac, multi=True)

for dn in dn_list:
conn = ISCDhcpLdapConfig.__get_server_connection()
conn.delete(dn)
conn.unbind()

return len(dn_list)
Loading

0 comments on commit 304396c

Please sign in to comment.