Repository: docker/docker-py

Issue:
No option to initialize a swarm with a default-addr-pool
The docker cli has options (available since API v1.39) to initialize a swarm with --default-addr-pool and --default-addr-pool-mask-length.  This project does not, yet.


Discussion:

Resolving patch:
diff --git a/docker/api/swarm.py b/docker/api/swarm.py
--- a/docker/api/swarm.py
+++ b/docker/api/swarm.py
@@ -1,5 +1,6 @@
 import logging
 from six.moves import http_client
+from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
 from .. import errors
 from .. import types
 from .. import utils
@@ -82,7 +83,8 @@ def get_unlock_key(self):
 
     @utils.minimum_version('1.24')
     def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
-                   force_new_cluster=False, swarm_spec=None):
+                   force_new_cluster=False, swarm_spec=None,
+                   default_addr_pool=None, subnet_size=None):
         """
         Initialize a new Swarm using the current connected engine as the first
         node.
@@ -107,6 +109,12 @@ def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
             swarm_spec (dict): Configuration settings of the new Swarm. Use
                 ``APIClient.create_swarm_spec`` to generate a valid
                 configuration. Default: None
+            default_addr_pool (list of strings): Default Address Pool specifies
+                default subnet pools for global scope networks. Each pool
+                should be specified as a CIDR block, like '10.0.0.0/8'.
+                Default: None
+            subnet_size (int): SubnetSize specifies the subnet size of the
+                networks created from the default subnet pool. Default: None
 
         Returns:
             ``True`` if successful.
@@ -119,9 +127,30 @@ def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
         url = self._url('/swarm/init')
         if swarm_spec is not None and not isinstance(swarm_spec, dict):
             raise TypeError('swarm_spec must be a dictionary')
+
+        if default_addr_pool is not None:
+            if utils.version_lt(self._version, '1.39'):
+                raise errors.InvalidVersion(
+                    'Address pool is only available for API version >= 1.39'
+                )
+            # subnet_size becomes 0 if not set with default_addr_pool
+            if subnet_size is None:
+                subnet_size = DEFAULT_SWARM_SUBNET_SIZE
+
+        if subnet_size is not None:
+            if utils.version_lt(self._version, '1.39'):
+                raise errors.InvalidVersion(
+                    'Subnet size is only available for API version >= 1.39'
+                )
+            # subnet_size is ignored if set without default_addr_pool
+            if default_addr_pool is None:
+                default_addr_pool = DEFAULT_SWARM_ADDR_POOL
+
         data = {
             'AdvertiseAddr': advertise_addr,
             'ListenAddr': listen_addr,
+            'DefaultAddrPool': default_addr_pool,
+            'SubnetSize': subnet_size,
             'ForceNewCluster': force_new_cluster,
             'Spec': swarm_spec,
         }
diff --git a/docker/constants.py b/docker/constants.py
--- a/docker/constants.py
+++ b/docker/constants.py
@@ -25,3 +25,6 @@
 DEFAULT_NUM_POOLS_SSH = 9
 
 DEFAULT_DATA_CHUNK_SIZE = 1024 * 2048
+
+DEFAULT_SWARM_ADDR_POOL = ['10.0.0.0/8']
+DEFAULT_SWARM_SUBNET_SIZE = 24
diff --git a/docker/models/swarm.py b/docker/models/swarm.py
--- a/docker/models/swarm.py
+++ b/docker/models/swarm.py
@@ -34,7 +34,8 @@ def get_unlock_key(self):
     get_unlock_key.__doc__ = APIClient.get_unlock_key.__doc__
 
     def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
-             force_new_cluster=False, **kwargs):
+             force_new_cluster=False, default_addr_pool=None,
+             subnet_size=None, **kwargs):
         """
         Initialize a new swarm on this Engine.
 
@@ -56,6 +57,12 @@ def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
                 is used. Default: ``0.0.0.0:2377``
             force_new_cluster (bool): Force creating a new Swarm, even if
                 already part of one. Default: False
+            default_addr_pool (list of str): Default Address Pool specifies
+                default subnet pools for global scope networks. Each pool
+                should be specified as a CIDR block, like '10.0.0.0/8'.
+                Default: None
+            subnet_size (int): SubnetSize specifies the subnet size of the
+                networks created from the default subnet pool. Default: None
             task_history_retention_limit (int): Maximum number of tasks
                 history stored.
             snapshot_interval (int): Number of logs entries between snapshot.
@@ -99,7 +106,8 @@ def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
 
             >>> client.swarm.init(
                 advertise_addr='eth0', listen_addr='0.0.0.0:5000',
-                force_new_cluster=False, snapshot_interval=5000,
+                force_new_cluster=False, default_addr_pool=['10.20.0.0/16],
+                subnet_size=24, snapshot_interval=5000,
                 log_entries_for_slow_followers=1200
             )
 
@@ -107,7 +115,9 @@ def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
         init_kwargs = {
             'advertise_addr': advertise_addr,
             'listen_addr': listen_addr,
-            'force_new_cluster': force_new_cluster
+            'force_new_cluster': force_new_cluster,
+            'default_addr_pool': default_addr_pool,
+            'subnet_size': subnet_size
         }
         init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs)
         self.client.api.init_swarm(**init_kwargs)


Test patch:
diff --git a/tests/integration/api_swarm_test.py b/tests/integration/api_swarm_test.py
--- a/tests/integration/api_swarm_test.py
+++ b/tests/integration/api_swarm_test.py
@@ -35,6 +35,35 @@ def test_init_swarm_force_new_cluster(self):
         version_2 = self.client.inspect_swarm()['Version']['Index']
         assert version_2 != version_1
 
+    @requires_api_version('1.39')
+    def test_init_swarm_custom_addr_pool_defaults(self):
+        assert self.init_swarm()
+        results = self.client.inspect_swarm()
+        assert set(results['DefaultAddrPool']) == {'10.0.0.0/8'}
+        assert results['SubnetSize'] == 24
+
+    @requires_api_version('1.39')
+    def test_init_swarm_custom_addr_pool_only_pool(self):
+        assert self.init_swarm(default_addr_pool=['2.0.0.0/16'])
+        results = self.client.inspect_swarm()
+        assert set(results['DefaultAddrPool']) == {'2.0.0.0/16'}
+        assert results['SubnetSize'] == 24
+
+    @requires_api_version('1.39')
+    def test_init_swarm_custom_addr_pool_only_subnet_size(self):
+        assert self.init_swarm(subnet_size=26)
+        results = self.client.inspect_swarm()
+        assert set(results['DefaultAddrPool']) == {'10.0.0.0/8'}
+        assert results['SubnetSize'] == 26
+
+    @requires_api_version('1.39')
+    def test_init_swarm_custom_addr_pool_both_args(self):
+        assert self.init_swarm(default_addr_pool=['2.0.0.0/16', '3.0.0.0/16'],
+                               subnet_size=28)
+        results = self.client.inspect_swarm()
+        assert set(results['DefaultAddrPool']) == {'2.0.0.0/16', '3.0.0.0/16'}
+        assert results['SubnetSize'] == 28
+
     @requires_api_version('1.24')
     def test_init_already_in_cluster(self):
         assert self.init_swarm()