| | |
| | | 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 |
| | | |
| | | 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}" |
| | | done |
| | |
| | | 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 |
| | | |
| | |
| | | VERSIONS: dev |
| | | steps: |
| | | - name: Checkout code |
| | | uses: actions/checkout@v2 |
| | | uses: actions/checkout@v6 |
| | | |
| | | - name: Prepare Docker plugin |
| | | run: | |
| | |
| | | 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" ] |
| | |
| | | |
| | | 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 |
| | |
| | | "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", |
| | |
| | | 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 |
| | | |
| | |
| | | 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) |
| | | |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | #!/bin/bash -x |
| | | #!/bin/bash |
| | | |
| | | set -e -x |
| | | |
| | | NAME=${NAME:-jacekkow/pyipam} |
| | | VERSIONS=${VERSIONS:-latest} |
| | |
| | | sudo mkdir -p rootfs |
| | | 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 |
| | | for VERSION in ${VERSIONS}; do |
| | | sudo docker plugin create "${NAME}:${VERSION}" . |
| | | done |
| | | sudo du -hs rootfs |
| | | for VERSION in ${VERSIONS}; do |
| | | docker plugin enable "${NAME}:${VERSION}" || exit 1 |
| | | break |
| | | done |
| | | docker plugin disable "${NAME}" || true |
| | | docker plugin rm "${NAME}" || true |
| | | sudo chmod 755 rootfs && sudo chmod -R o=g rootfs/usr/src |
| | | if [ `echo ${VERSIONS} | wc -w` -gt 1 ]; then |
| | | for VERSION in ${VERSIONS}; do |
| | | sudo docker plugin create "${NAME}:${VERSION}" . |
| | | docker plugin push "${NAME}:${VERSION}" |
| | | docker plugin rm "${NAME}:${VERSION}" |
| | | done |
| | | else |
| | | sudo docker plugin create "${NAME}:${VERSIONS}" . |
| | | docker plugin enable "${NAME}:${VERSIONS}" |
| | | fi |
| | |
| | | |
| | | import logging |
| | | import os |
| | | import signal |
| | | import sys |
| | | |
| | | import docker_plugin_api.Plugin |
| | | import flask |
| | |
| | | if os.environ.get('ENVIRONMENT', 'dev') == 'dev': |
| | | app.run(debug=True) |
| | | else: |
| | | signal.signal(signal.SIGTERM, lambda: sys.exit(0)) |
| | | waitress.serve(app, unix_socket='/run/docker/plugins/pyipam.sock', threads=1) |
| | |
| | | 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') |
| | |
| | | #!/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 \ |
| | |
| | | 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 \ |
| | |
| | | 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 |