Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,54 @@ on:
tags:
- 'v[0-9]+.*'

permissions:
contents: read

jobs:
deploy:
deploy-to-ghcr-io:
name: Publish to GitHub Container Registry
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Prepare and push Docker plugin
run: |
export NAME="ghcr.io/${{ github.repository }}"
export VERSIONS="latest ${GITHUB_REF/refs\/tags\//}"
./package.sh
for VERSION in ${VERSIONS}; do
docker plugin push "ghcr.io/${{ github.repository }}:${VERSION}"
done

deploy-to-docker-hub:
name: Publish to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v6

- name: Login to Docker Hub
run: |
docker login -u 'jacekkow' -p '${{ secrets.DOCKER_PASSWORD }}'
uses: docker/login-action@v4
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Prepare and push Docker plugin
run: |
VERSIONS="latest ${GITHUB_REF/refs\/tags\//}"
export VERSIONS
export NAME="${{ github.actor }}/pyipam"
export VERSIONS="latest ${GITHUB_REF/refs\/tags\//}"
./package.sh
for VERSION in ${VERSIONS}; do
docker plugin push "jacekkow/pyipam:${VERSION}"
docker plugin push "${{ github.actor }}/pyipam:${VERSION}"
done
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ on:
branches:
- '**'

permissions:
contents: read

jobs:
unit_test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v6
with:
python-version: 3.x

Expand All @@ -35,7 +38,7 @@ jobs:
VERSIONS: dev
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v6

- name: Prepare Docker plugin
run: |
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ ENV HOME=/usr/src/app
WORKDIR /usr/src/app

COPY --chown=nobody:nobody requirements.txt .
RUN pip3 install --user --no-cache-dir -r requirements.txt
RUN python -m venv venv && ./venv/bin/pip install --no-cache-dir -r requirements.txt

COPY --chown=nobody:nobody . .

CMD [ "./run.py" ]
CMD [ "./venv/bin/python", "run.py" ]
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ docker network create --ipam-driver jacekkow/pyipam:latest new-network

Check out [`test_integration.sh`](test_integration.sh) for more examples.

## Options

To use options, add `--ipam-opt option=value` as an argument of `docker network create`:

```bash
docker network create --ipam-driver jacekkow/pyipam:latest --ipam-opt ptp=1 new-network
```

Available options:

`ptp=1`

When set addresses with netmask /32 (IPv4) or /128 (IPv6) are handed out.
In this mode all IP addresses are handed out from the subnet,
including ones that would be "network address" and "broadcast address"!

`validate=0`

Do not validate duplicate IP address assignment. This IPAM plugin would
then happily hand out already-used addresses if such were manually specified.
This option does not affect automatic assignments.
Note that this module does not track how many times the IP was handed out,
hence if two containers have the same IP and one of them stops,
IP will be marked as free!

## Manual packaging

In order to test this module in development environment, you can build it
Expand Down
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"description": "pyIPAM - IPAM driver in Python",
"documentation": "https://github.com/jacekkow/docker-plugin-pyipam",
"workdir": "/usr/src/app",
"entrypoint": ["./run.py"],
"entrypoint": ["./venv/bin/python", "./run.py"],
"env": [
{
"name": "ENVIRONMENT",
Expand Down
26 changes: 16 additions & 10 deletions lib/Ipam.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ class Pool:
def __init__(self, pool: str = None, options: dict = None, subPool: str = None, v6: bool = None):
if pool == '':
pool = None
if options is None:
options = {}
self.options = options or {}
if subPool == '':
subPool = None

Expand Down Expand Up @@ -42,8 +41,11 @@ def __init__(self, pool: str = None, options: dict = None, subPool: str = None,
if not self.subpool.subnet_of(self.pool):
raise InputValidationException('Subpool must be a subnet of pool')

self.validate = self.options.get('validate', '1') == '1'
self.ptp = self.options.get('ptp', '0') == '1'

self.allocations = set()
self.current = self.subpool.hosts()
self.current = self.subpool.hosts() if not self.ptp else self.subpool.__iter__()

self.v6 = isinstance(self.pool, ipaddress.IPv6Network)

Expand All @@ -62,7 +64,7 @@ def _find_next_address(self):
for address in self.current:
if not self._is_allocated(address):
return address
self.current = self.subpool.hosts()
self.current = self.subpool.hosts() if not self.ptp else self.subpool.__iter__()
for address in self.current:
if not self._is_allocated(address):
return address
Expand All @@ -74,19 +76,23 @@ def allocate(self, address: str = None) -> str:
else:
address = ipaddress.ip_address(address)

if self.pool.network_address == address:
raise InputValidationException('Cannot allocate network address to a host')
if not self.v6 and self.pool.broadcast_address == address:
raise InputValidationException('Cannot allocate broadcast address to a host')
if not self.ptp:
if self.pool.network_address == address:
raise InputValidationException('Cannot allocate network address to a host')
if not self.v6 and self.pool.broadcast_address == address:
raise InputValidationException('Cannot allocate broadcast address to a host')
if address not in self.pool:
raise InputValidationException('Requested address does not belong to a pool')

address = str(address)
if self._is_allocated(address):
if self.validate and self._is_allocated(address):
raise InputValidationException('Requested address {} is already used'.format(address))
self.allocations.add(address)

return '{}/{}'.format(address, self.pool.prefixlen)
prefixlen = self.pool.prefixlen
if self.ptp:
prefixlen = 128 if self.v6 else 32
return '{}/{}'.format(address, prefixlen)

def deallocate(self, address: str):
address = ipaddress.ip_address(address)
Expand Down
2 changes: 1 addition & 1 deletion package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ docker export "${id}" | sudo tar -x -C rootfs
docker rm -vf "${id}"
docker plugin disable "${NAME}"
docker plugin rm "${NAME}"
sudo chmod 755 rootfs rootfs/usr/src/app/.local && sudo chmod -R o=g rootfs/usr/src
sudo chmod 755 rootfs && sudo chmod -R o=g rootfs/usr/src
for VERSION in ${VERSIONS}; do
sudo docker plugin create "${NAME}:${VERSION}" .
done
Expand Down
32 changes: 32 additions & 0 deletions test/IpamPoolTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,35 @@ def test_pool_allocate_release_ipv6(self):
pool.deallocate('fe80::2')
self.assertEqual(pool.allocate('fe80::2'), 'fe80::2/125')
self.assertEqual(pool.allocate(), 'fe80::1/125')


class TestPoolPointToPoint(unittest.TestCase):
def test_pool_allocate_ptp_ipv4(self):
pool = Pool(pool='127.0.0.0/30', options={'ptp': '1'})
self.assertEqual(pool.allocate(), '127.0.0.0/32')
self.assertEqual(pool.allocate(), '127.0.0.1/32')
self.assertEqual(pool.allocate(), '127.0.0.2/32')
self.assertEqual(pool.allocate(), '127.0.0.3/32')

def test_pool_allocate_ptp_ipv6(self):
pool = Pool(pool='fe80::/126', options={'ptp': '1'})
self.assertEqual(pool.allocate(), 'fe80::/128')
self.assertEqual(pool.allocate(), 'fe80::1/128')
self.assertEqual(pool.allocate(), 'fe80::2/128')
self.assertEqual(pool.allocate(), 'fe80::3/128')

class TestPoolWithoutValidation(unittest.TestCase):
def test_pool_allocate_duplicates_ipv4(self):
pool = Pool(pool='127.0.0.0/30', options={'validate': '0'})
self.assertEqual(pool.allocate(), '127.0.0.1/30')
self.assertEqual(pool.allocate(), '127.0.0.2/30')
self.assertEqual(pool.allocate('127.0.0.1'), '127.0.0.1/30')
self.assertEqual(pool.allocate('127.0.0.1'), '127.0.0.1/30')

def test_pool_allocate_duplicates_ipv6(self):
pool = Pool(pool='fe80::/126', options={'validate': '0'})
self.assertEqual(pool.allocate(), 'fe80::1/126')
self.assertEqual(pool.allocate(), 'fe80::2/126')
self.assertEqual(pool.allocate(), 'fe80::3/126')
self.assertEqual(pool.allocate('fe80::1'), 'fe80::1/126')
self.assertEqual(pool.allocate('fe80::1'), 'fe80::1/126')
52 changes: 36 additions & 16 deletions test_integration.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#!/bin/bash

set -e
set -e -x

NAME=${NAME:-jacekkow/pyipam}
VERSION=${VERSION:-latest}

PLUGIN="${NAME}:${VERSION}"

docker network rm test1 || true
docker network rm test2 || true

docker network create \
--internal \
--driver bridge \
Expand All @@ -21,26 +24,37 @@ docker network create \
test1

ADDRESSES=$(docker run --rm --network test1 \
debian \
/bin/ip addr show
alpine \
/sbin/ip addr show
)
echo "${ADDRESSES}" | grep 192.168.255.129/24
echo "${ADDRESSES}" | grep 2001:db8:aaaa:bbbb::1/32
if ! echo "${ADDRESSES}" | grep 192.168.255.129/24; then
echo "ERROR: invalid IPv4 address assigned"
exit 1
fi
if ! echo "${ADDRESSES}" | grep 2001:db8:aaaa:bbbb::1/32; then
echo "ERROR: invalid IPv6 address assigned"
exit 1
fi


ADDRESSES=$(docker run --rm --network test1 \
--ip 192.168.255.25 --ip6 2001:db8:dddd:eeee:ffff:1:2:3 \
debian \
/bin/ip addr show
alpine \
/sbin/ip addr show
)
echo "${ADDRESSES}" | grep 192.168.255.25/24
echo "${ADDRESSES}" | grep 2001:db8:dddd:eeee:ffff:1:2:3/32
if ! echo "${ADDRESSES}" | grep 192.168.255.25/24; then
echo "ERROR: invalid IPv4 address assigned"
exit 1
fi
if ! echo "${ADDRESSES}" | grep 2001:db8:dddd:eeee:ffff:1:2:3/32; then
echo "ERROR: invalid IPv6 address assigned"
exit 1
fi

docker network rm test1


docker network create \
--internal \
--driver bridge \
--ipam-driver "${PLUGIN}" \
--ipv6 \
Expand All @@ -51,15 +65,21 @@ docker network create \
test2

ROUTES=$(docker run --rm --network test2 \
debian \
/bin/ip route show
alpine \
/sbin/ip route show
)
echo "${ROUTES}" | grep 192.168.255.254
if ! echo "${ROUTES}" | grep 192.168.255.254; then
echo "ERROR: invalid IPv4 route"
exit 1
fi

ROUTES=$(docker run --rm --network test2 \
debian \
/bin/ip -6 route show
alpine \
/sbin/ip -6 route show
)
echo "${ROUTES}" | grep 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff
if ! echo "${ROUTES}" | grep 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff; then
echo "ERROR: invalid IPv6 route"
exit 1
fi

docker network rm test2