Monkeying around with virtual machines and pxe configs.
- Setup
- Requirements
- Configuration
- Usage
- Name origin
- Issues
- License
- Contact
- Contribution and Development
Clone the repo into the home of a normal user, then copy the service file into the systemd directory and reload systemd to recognize the file:
groupadd marmoset
useradd --create-home --gid marmoset --shell /bin/bash marmoset
pacman -Syu git
su marmoset
cd ~
git clone https://github.com/virtapi/marmoset.git
exit
cp /home/marmoset/marmoset/ext/marmoset.service /etc/systemd/system/
systemctl daemon-reload
Copy the marmoset.conf.example
to marmoset.conf
and adjust the settings to your needs.
Checkout the Comments in the file or in our Configuration section.
Now we need to setup a virtualenv and install the required python packages (remove libvirt from the requirements.txt and pkg-config libvirt gcc
from the list of packages to install if you don't want to manage VMs with marmoset):
pacman -Syu python-virtualenv pkg-config libvirt gcc
su marmoset
virtualenv prod
source prod/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
Please checkout our requirements.txt for a complete and authoritative list!
- aniso8601
- Flask
- Flask-RESTful
- itsdangerous
- Jinja2
- ldap3
- libvirt-python
- MarkupSafe
- pyasn1
- python-dateutil
- pytz
- six
- Werkzeug
- wheel
In addition to these python packages, you also need Python 3. This project originally started with Python 3.3, we are currently developing and testing on 3.6 and 3.7.
We also need pkg-config and libvirt, which you need to install via your system-wide package manager (these aren't python packages).
The configuration file has to be placed in the app's root directory
as marmoset.conf
. It is necessary to define a PXELabel
section.
The first entry in this section will be the default label.
All other sections are optional and have defaults set.
An example file can be found as marmoset.conf.example
.
If you want to customize the XML templates for libvirt objects, copy
the template dir marmoset/virt/templates/
and specify the new path
in the Libvirt
section.
Marmoset can be used via CLI directly or as a HTTP server.
To see all available subcommands and their aliases, just run the script with the command:
$ ./marmoset.py -h
Each subcommand provides its own help text:
$ ./marmoset.py pxe -h
List all entries:
$ ./marmoset.py pxe list
Create an PXE entry with the default label:
$ ./marmoset.py pxe create 3.4.5.6
Create an PXE entry for a non-default label:
$ ./marmoset.py pxe create -l freebsd 3.4.5.6
If the used label has a callback method set that sets a custom root password for the PXE boot target, you can provide a pasword:
$ ./marmoset.py pxe c -l freebsd -p SoSecretPW 3.4.5.6
Remove the entry for an IP address:
$ ./marmoset.py pxe remove 3.4.5.6
List all defined libvirt domains and their attributes:
$ ./marmoset.py vm list
Start it like this:
$ ./marmoset.py server
Or use our systemd service:
$ systemctl start marmoset
A third solution is to use nginx + uwsgi to power the app. This is the recommended way if you expect a high amount of requests.
curl -u admin:secret https://localhost:5000/v1/pxe
200 on success (empty array if no records are present)
[
{
"ip_address": "1.2.3.4",
"label": "rescue"
}
]
Create a new PXE config entry. Label defaults to "rescue". If no password is given and a callback method is set, a random password is generated and returned. Takes JSON Input as well:
curl -u admin:secret --data 'ip_address=10.10.1.1&label=rescue&password=SeCrEt' https://localhost:5000/v1/pxe
201 on success
{
"ip_address": "10.10.1.1",
"label": "rescue",
"password": "SeCrEt"
}
409 if there is already an entry
Check if there is an entry currently set:
curl -u admin:secret https://localhost:5000/v1/pxe/10.10.1.1
200 if found
{
"ip_address": "10.10.1.1",
"label": "rescue"
}
404 if not found
You can also provide IPv6 parameters for the PXE config. These parameters will be appended to the kernel command line.
curl -u admin:secret --data 'ip_address=10.10.1.1&label=archrescue&password=SeCrEt&ipv6_address=2001::1&ipv6_gateway=fe80::1&ipv6_prefix=64' https://localhost:5000/v1/pxe
{
"ip_address": "10.10.1.1",
"ipv6_address": "2001::1",
"ipv6_gateway": "fe80::1",
"ipv6_prefix": "64",
"label": "archrescue",
"password": "SeCrEt",
"script": null,
"uuid": null
}
All IPv6 parameters (ipv6_address, ipv6_gateway and ipv6_prefix) need to be present, else they will be ignored.
curl -u admin:secret -X DELETE https://localhost:5000/v1/pxe/10.10.1.1
204 on success
Create a new VM:
curl -u admin:secret -d 'name=testvm&user=testuser&ip_address=10.10.1.1&memory=1G&disk=10G' https://localhost:5000/v1/vm
201 on success
{
"disks": [
{
"bus": "virtio",
"capacity": "10 GiB",
"device": "disk",
"path": "/mnt/data/test-pool/testuser_testvm",
"target": "hda",
"type": "block"
}
],
"interfaces": [
{
"ip_address": "10.10.1.1",
"mac_address": "52:54:00:47:b0:09",
"model": "virtio",
"network": "default",
"type": "network"
}
],
"memory": "1 GiB",
"name": "test",
"state": {
"reason": "unknown",
"state": "shutoff"
},
"user": "testuser",
"uuid": "cd412122-ec04-46d7-ba12-a7757aa5af11",
"vcpu": "1",
"vnc_data": {
"vnc_port": 5900,
"ws_port": 5700,
"password": "gferhhpehrehjrekhtngfmbfdkbkre"
}
}
422 if there is an error
{
"message": "useful error message"
}
List all currently defined VMs:
curl -u admin:secret https://localhost:5000/v1/vm
[
{
"disks": [
{
"bus": "virtio",
"capacity": "10 GiB",
"device": "disk",
"path": "/mnt/data/test-pool/testo",
"target": "hda",
"type": "block"
}
],
"interfaces": [
{
"ip_address": "10.10.1.1",
"mac_address": "52:54:00:47:b0:09",
"model": "virtio",
"network": "default",
"type": "network"
}
],
"memory": "1 GiB",
"name": "test",
"state": {
"reason": "unknown",
"state": "shutoff"
},
"user": "testuser",
"uuid": "cd412122-ec04-46d7-ba12-a7757aa5af11",
"vcpu": "1",
"vnc_data": {
"vnc_port": 5900,
"ws_port": 5700,
"password": "gferhhpehrehjrekhtngfmbfdkbkre"
}
}
]
Get info for a specific VM:
curl -u admin:secret https://localhost:5000/v1/vm/cd412122-ec04-46d7-ba12-a7757aa5af11
200 on success
{
"disks": [
{
"bus": "virtio",
"capacity": "10 GiB",
"device": "disk",
"path": "/mnt/data/test-pool/testuser_testvm",
"target": "hda",
"type": "block"
}
],
"interfaces": [
{
"ip_address": "10.10.1.1",
"mac_address": "52:54:00:47:b0:09",
"model": "virtio",
"network": "default",
"type": "network"
}
],
"memory": "1 GiB",
"name": "test",
"state": {
"reason": "unknown",
"state": "shutoff"
},
"user": "testuser",
"uuid": "cd412122-ec04-46d7-ba12-a7757aa5af11",
"vcpu": "1",
"vnc_data": {
"vnc_port": 5900,
"ws_port": 5700,
"password": "gferhhpehrehjrekhtngfmbfdkbkre"
}
}
404 if the uuid doesn't exist
Update parameters of a VM:
curl -u admin:secret -X PUT -d 'memory=3 GiB&cpu=2&password=sEcReT' https://localhost:5000/v1/vm/cd412122-ec04-46d7-ba12-a7757aa5af11
200 on success
{
"disks": [
{
"bus": "virtio",
"capacity": "10 GiB",
"device": "disk",
"path": "/mnt/data/test-pool/testuser_testvm",
"target": "hda",
"type": "block"
}
],
"interfaces": [
{
"ip_address": "10.10.1.1",
"mac_address": "52:54:00:47:b0:09",
"model": "virtio",
"network": "default",
"type": "network"
}
],
"memory": "3 GiB",
"name": "test",
"state": {
"reason": "unknown",
"state": "shutoff"
},
"user": "testuser",
"uuid": "cd412122-ec04-46d7-ba12-a7757aa5af11",
"vcpu": "2",
"vnc_data": {
"vnc_port": 5900,
"ws_port": 5700,
"password": "sEcReT"
}
}
- 404 if the uuid doesn't exist
- 422 if input values are not processable
Remove a VM:
curl -u admin:secret -X DELETE https://localhost:5000/v1/vm/cd412122-ec04-46d7-ba12-a7757aa5af11
204 on success
This endpoint is meant to work together with our installimage. We identify each dataset by its MAC address and store the key:value config pairs for the installimage.
curl -u admin:secret https://localhost:5000/v1/installimage
[
{
"mac": "00_00_00_00_00_00",
"variables": {
"BOOTLOADER": "grub",
"DRIVE1": "/dev/sda",
"HOSTNAME": "CentOS-71-64-minimal",
"IMAGE": "/root/.installimage/../images/CentOS-71-64-minimal.tar.gz",
"PART": "/ ext4 all"
}
},
{
"mac": "b8_ac_6f_97_7e_77",
"variables": {
"BOOTLOADER": "grub",
"DRIVE1": "/dev/sda",
"HOSTNAME": "CentOS-71-64-minimal",
"IMAGE": "/root/.installimage/../images/CentOS-71-64-minimal.tar.gz",
"PART": "/ ext4 all",
}
}
]
curl -u admin:secret https://localhost:5000/v1/installimage/b8:ac:6f:97:7e:77
{
"mac": "b8:ac:6f:97:7e:77",
"variables": {
"BOOTLOADER": "grub",
"DRIVE1": "/dev/sda",
"HOSTNAME": "CentOS-71-64-minimal",
"IMAGE": "/root/.installimage/../images/CentOS-71-64-minimal.tar.gz",
"PART": "/ ext4 all",
}
}
curl -u admin:secret https://localhost:5000/v1/installimage/b8:ac:6f:97:7e:77/config
PART / ext4 all
IMAGE /root/.installimage/../images/CentOS-71-64-minimal.tar.gz
DRIVE1 /dev/sda
BOOTLOADER grub
HOSTNAME CentOS-71-64-minimal
curl -u admin:secret --data "drive1=/dev/sda&bootloader=grub&hostname=CentOS-71-64-minimal&PART=/ ext4 all&image=/root/.installimage/../images/CentOS-71-64-minimal.tar.gz" https://localhost:5000/v1/installimage/b8:ac:6f:97:7e:77
Returns the created record:
{
"mac": "b8:ac:6f:97:7e:77",
"variables": {
"BOOTLOADER": "grub",
"DRIVE1": "/dev/sda",
"HOSTNAME": "CentOS-71-64-minimal",
"IMAGE": "/root/.installimage/../images/CentOS-71-64-minimal.tar.gz",
"PART": "/ ext4 all",
}
}
curl -u admin:secret -X DELETE https://localhost:5000/v1/installimage/b8:ac:6f:97:7e:77
Errormessage if you want to delete or list a nonexistent entry:
{
"message": "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. You have requested this URI [/v1/installimage/b8:ac:6f:97:7e:77] but did you mean /v1/installimage/<mac> or /v1/installimage/<mac>/config or /v1/installimage ?"
}
This endpoint allows us the throw static IP/MAC combinations into a ldap database. Currently tested is only the openldap backend. This database is connected to an isc-dhcpd. We can identify an object by its IP or MAC address.
curl -u admin:secret https://localhost:5000/v1/dhcp
[
{
"additional_statements": {},
"dhcp_hostname": "odin.fritz.box",
"gateway": "192.168.10.1",
"ip_address": "192.168.10.5",
"mac": "00:00:00:00:00:00",
"networkmask": "255.255.255.0"
},
{
"additional_statements": {},
"dhcp_hostname": "odin.fritz.box",
"gateway": "10.3.7.1",
"ip_address": "10.3.7.41",
"mac": "00:00:00:00:00:00",
"networkmask": "255.255.255.0"
}
]
curl -u admin:secret https://localhost:5000/v1/dhcp/mac/00:00:00:00:00:00
{
"additional_statements": {},
"dhcp_hostname": "odin.fritz.box",
"gateway": "10.3.7.1",
"ip_address": "10.3.7.41",
"mac": "00:00:00:00:00:00",
"networkmask": "255.255.255.0"
}
or: curl -u admin:secret https://localhost:5000/v1/dhcp/mac/23.45.67.8
"please provide a valid mac address"
curl -u admin:secret https://localhost:5000/v1/dhcp/ipv4/10.3.7.41
{
"additional_statements": {},
"dhcp_hostname": "odin.fritz.box",
"gateway": "10.3.7.1",
"ip_address": "10.3.7.41",
"mac": "00:00:00:00:00:00",
"networkmask": "255.255.255.0"
}
or: curl -u admin:secret https://localhost:5000/v1/dhcp/ipv4/23.45.67.888
"please provide a valid ipv4 address"
this will return the new created entry:
curl -u admin:secret --data 'ip_address=10.3.7.41&mac=b8:ac:6f:97:7e:77&gateway=10.3.7.1&networkmask=255.255.255.0' https://localhost:5000/v1/dhcp
{
"additional_statements": {},
"dhcp_hostname": "example.com",
"gateway": "10.3.7.1",
"ip_address": "10.3.7.41",
"mac": "b8:ac:6f:97:7e:77",
"networkmask": "255.255.255.0"
}
an update works the same way, just submit the command again and change new updated params, again the API will respond with the updated entry.
curl -u admin:secret --data 'ip_address=10.10.10.2&mac=fe:54:6c:9e:10:e8&gateway=10.10.10.1&networkmask=255.255.255.128&option domain-name-servers=192.168.178.1,192.168.178.2' https://localhost:5000/v1/dhcp
{
"additional_statements": {
"option domain-name-servers": "192.168.178.1,192.168.178.2"
},
"dhcp_hostname": "example.com",
"gateway": "10.10.10.1",
"ip_address": "10.10.10.2",
"mac": "fe:54:6c:9e:10:e8",
"networkmask": "255.255.255.128"
}
curl -u admin:secret -X DELETE https://localhost:5000/v1/dhcp/ipv4/10.3.7.41
Deleting a nonexistent entry:
curl -u admin:secret -X DELETE https://localhost:5000/v1/dhcp/ipv4/10.3.7.41
will return:
{
"message": "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. You have requested this URI [/v1/dhcp/ipv4/10.3.7.41] but did you mean /v1/dhcp/ipv4/<ipv4> ?"
}
We will do all this in a clean Arch nspawn container (login for a new container is always root without password):
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 or use the package that I built. The needed data can be found here. 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.
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";
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:
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).
The marmosets is a group of monkey species, checkout wikipedia for detailed infos.
The original project and all of our changes are based on the AGPL, you can find the license here.
You can meet us in #virtapi at freenode.
We've defined our contribution rules in CONTRIBUTING.md.
Let's assume you need to install one of the python dependencies from a git repo, because you rely on unreleased code. Instead of waiting for a release, you can depend on the git repo instead. And example: Add the following to the requirements.txt if you want to install a specific commit from the pylint-flask repo.
-e git+https://github.com/simpleweb/pylint-flask.git@ad5ab3048a6975a56667cefd303af1173a554bbb#egg=pylint-flask
List all installed packages in a virtual env:
pip list
List all oudated packages:
pip list --outdated
Update every outdated package:
pip list --outdated | awk '{print $1}' | xargs -n1 pip install --upgrade