Merge pull request #121 from asadoughi/allocation_pools
Allocation pools
This commit is contained in:
@@ -16,13 +16,9 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from neutron.api import extensions
|
from neutron.api import extensions
|
||||||
from neutron.api.v2 import attributes
|
|
||||||
|
|
||||||
EXTENDED_ATTRIBUTES_2_0 = {
|
EXTENDED_ATTRIBUTES_2_0 = {
|
||||||
'subnets': {
|
'subnets': {
|
||||||
"allocation_pools": {'allow_post': True, 'allow_put': True,
|
|
||||||
'default': attributes.ATTR_NOT_SPECIFIED,
|
|
||||||
'is_visible': False},
|
|
||||||
"enable_dhcp": {'allow_post': False, 'allow_put': False,
|
"enable_dhcp": {'allow_post': False, 'allow_put': False,
|
||||||
'default': False,
|
'default': False,
|
||||||
'is_visible': True},
|
'is_visible': True},
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ LOG = logging.getLogger("neutron")
|
|||||||
|
|
||||||
|
|
||||||
class QuarkIpam(object):
|
class QuarkIpam(object):
|
||||||
def _get_ip_policy_rule_set(self, subnet):
|
@staticmethod
|
||||||
|
def get_ip_policy_rule_set(subnet):
|
||||||
ip_policy = subnet["ip_policy"] or \
|
ip_policy = subnet["ip_policy"] or \
|
||||||
subnet["network"]["ip_policy"] or \
|
subnet["network"]["ip_policy"] or \
|
||||||
dict()
|
dict()
|
||||||
@@ -38,7 +39,7 @@ class QuarkIpam(object):
|
|||||||
ip_policy_rules = netaddr.IPSet(
|
ip_policy_rules = netaddr.IPSet(
|
||||||
[netaddr.IPNetwork((int(ippr["address"]), ippr["prefix"]))
|
[netaddr.IPNetwork((int(ippr["address"]), ippr["prefix"]))
|
||||||
for ippr in ip_policy_rules])
|
for ippr in ip_policy_rules])
|
||||||
subnet_set = netaddr.IPSet(netaddr.IPNetwork(subnet["cidr"]))
|
subnet_set = netaddr.IPSet([netaddr.IPNetwork(subnet["cidr"])])
|
||||||
ip_policy_rules = subnet_set & ip_policy_rules
|
ip_policy_rules = subnet_set & ip_policy_rules
|
||||||
return ip_policy_rules
|
return ip_policy_rules
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ class QuarkIpam(object):
|
|||||||
|
|
||||||
ip_policy_rules = None
|
ip_policy_rules = None
|
||||||
if not ip_address:
|
if not ip_address:
|
||||||
ip_policy_rules = self._get_ip_policy_rule_set(subnet)
|
ip_policy_rules = self.get_ip_policy_rule_set(subnet)
|
||||||
policy_size = ip_policy_rules.size if ip_policy_rules else 0
|
policy_size = ip_policy_rules.size if ip_policy_rules else 0
|
||||||
if ipnet.size > (ips_in_subnet + policy_size):
|
if ipnet.size > (ips_in_subnet + policy_size):
|
||||||
return subnet
|
return subnet
|
||||||
@@ -117,7 +118,7 @@ class QuarkIpam(object):
|
|||||||
|
|
||||||
subnet = self._choose_available_subnet(
|
subnet = self._choose_available_subnet(
|
||||||
elevated, net_id, ip_address=ip_address, version=version)
|
elevated, net_id, ip_address=ip_address, version=version)
|
||||||
ip_policy_rules = self._get_ip_policy_rule_set(subnet)
|
ip_policy_rules = self.get_ip_policy_rule_set(subnet)
|
||||||
|
|
||||||
# Creating this IP for the first time
|
# Creating this IP for the first time
|
||||||
next_ip = None
|
next_ip = None
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2,
|
|||||||
gateway_ip = _pop_param(sub_attrs, "gateway_ip", str(cidr[1]))
|
gateway_ip = _pop_param(sub_attrs, "gateway_ip", str(cidr[1]))
|
||||||
dns_ips = _pop_param(sub_attrs, "dns_nameservers", [])
|
dns_ips = _pop_param(sub_attrs, "dns_nameservers", [])
|
||||||
routes = _pop_param(sub_attrs, "host_routes", [])
|
routes = _pop_param(sub_attrs, "host_routes", [])
|
||||||
|
allocation_pools = _pop_param(sub_attrs, "allocation_pools", [])
|
||||||
|
|
||||||
new_subnet = db_api.subnet_create(context, **sub_attrs)
|
new_subnet = db_api.subnet_create(context, **sub_attrs)
|
||||||
|
|
||||||
@@ -270,6 +271,16 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2,
|
|||||||
new_subnet["dns_nameservers"].append(db_api.dns_create(
|
new_subnet["dns_nameservers"].append(db_api.dns_create(
|
||||||
context, ip=netaddr.IPAddress(dns_ip)))
|
context, ip=netaddr.IPAddress(dns_ip)))
|
||||||
|
|
||||||
|
if allocation_pools:
|
||||||
|
exclude = netaddr.IPSet([cidr])
|
||||||
|
for p in allocation_pools:
|
||||||
|
x = netaddr.IPSet(netaddr.IPRange(p["start"], p["end"]))
|
||||||
|
exclude = exclude - x
|
||||||
|
new_subnet["ip_policy"] = db_api.ip_policy_create(context,
|
||||||
|
exclude=exclude)
|
||||||
|
# HACK(amir): force backref for ip_policy
|
||||||
|
if not new_subnet["network"]:
|
||||||
|
new_subnet["network"] = net
|
||||||
subnet_dict = v._make_subnet_dict(new_subnet,
|
subnet_dict = v._make_subnet_dict(new_subnet,
|
||||||
default_route=DEFAULT_ROUTE)
|
default_route=DEFAULT_ROUTE)
|
||||||
subnet_dict["gateway_ip"] = gateway_ip
|
subnet_dict["gateway_ip"] = gateway_ip
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ View Helpers for Quark Plugin
|
|||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
|
from quark.ipam import QuarkIpam
|
||||||
from quark import network_strategy
|
from quark import network_strategy
|
||||||
STRATEGY = network_strategy.STRATEGY
|
STRATEGY = network_strategy.STRATEGY
|
||||||
|
|
||||||
@@ -42,12 +43,38 @@ def _make_subnet_dict(subnet, default_route=None, fields=None):
|
|||||||
dns_nameservers = [str(netaddr.IPAddress(dns["ip"]))
|
dns_nameservers = [str(netaddr.IPAddress(dns["ip"]))
|
||||||
for dns in subnet.get("dns_nameservers")]
|
for dns in subnet.get("dns_nameservers")]
|
||||||
net_id = STRATEGY.get_parent_network(subnet["network_id"])
|
net_id = STRATEGY.get_parent_network(subnet["network_id"])
|
||||||
|
|
||||||
|
def _allocation_pools(subnet):
|
||||||
|
ip_policy_rules = QuarkIpam.get_ip_policy_rule_set(subnet)
|
||||||
|
cidr = netaddr.IPSet([netaddr.IPNetwork(subnet["cidr"])])
|
||||||
|
allocatable = cidr - ip_policy_rules
|
||||||
|
|
||||||
|
cidrs = allocatable.iter_cidrs()
|
||||||
|
if len(cidrs) == 0:
|
||||||
|
return []
|
||||||
|
if len(cidrs) == 1:
|
||||||
|
return [dict(start=str(cidrs[0][0]),
|
||||||
|
end=str(cidrs[0][-1]))]
|
||||||
|
|
||||||
|
pool_start = cidrs[0][0]
|
||||||
|
prev_cidr_end = cidrs[0][-1]
|
||||||
|
pools = []
|
||||||
|
for cidr in cidrs[1:]:
|
||||||
|
cidr_start = cidr[0]
|
||||||
|
if prev_cidr_end + 1 != cidr_start:
|
||||||
|
pools.append(dict(start=str(pool_start),
|
||||||
|
end=str(prev_cidr_end)))
|
||||||
|
pool_start = cidr_start
|
||||||
|
prev_cidr_end = cidr[-1]
|
||||||
|
pools.append(dict(start=str(pool_start), end=str(prev_cidr_end)))
|
||||||
|
return pools
|
||||||
|
|
||||||
res = {"id": subnet.get('id'),
|
res = {"id": subnet.get('id'),
|
||||||
"name": subnet.get('name'),
|
"name": subnet.get('name'),
|
||||||
"tenant_id": subnet.get('tenant_id'),
|
"tenant_id": subnet.get('tenant_id'),
|
||||||
"network_id": net_id,
|
"network_id": net_id,
|
||||||
"ip_version": subnet.get('ip_version'),
|
"ip_version": subnet.get('ip_version'),
|
||||||
"allocation_pools": [],
|
"allocation_pools": _allocation_pools(subnet),
|
||||||
"dns_nameservers": dns_nameservers or [],
|
"dns_nameservers": dns_nameservers or [],
|
||||||
"cidr": subnet.get('cidr'),
|
"cidr": subnet.get('cidr'),
|
||||||
"enable_dhcp": None}
|
"enable_dhcp": None}
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ class TestQuarkGetSubnets(TestQuarkPlugin):
|
|||||||
for subnet in subnets:
|
for subnet in subnets:
|
||||||
s_dict = subnet.copy()
|
s_dict = subnet.copy()
|
||||||
s_dict["routes"] = route_models
|
s_dict["routes"] = route_models
|
||||||
s = models.Subnet()
|
s = models.Subnet(network=models.Network())
|
||||||
s.update(s_dict)
|
s.update(s_dict)
|
||||||
subnet_models.append(s)
|
subnet_models.append(s)
|
||||||
elif subnets:
|
elif subnets:
|
||||||
mod = models.Subnet()
|
mod = models.Subnet(network=models.Network())
|
||||||
mod.update(subnets)
|
mod.update(subnets)
|
||||||
mod["routes"] = route_models
|
mod["routes"] = route_models
|
||||||
subnet_models = mod
|
subnet_models = mod
|
||||||
@@ -111,7 +111,7 @@ class TestQuarkGetSubnets(TestQuarkPlugin):
|
|||||||
subnet = dict(id=subnet_id, network_id=1, name=subnet_id,
|
subnet = dict(id=subnet_id, network_id=1, name=subnet_id,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="192.168.0.0/24", gateway_ip="192.168.0.1",
|
cidr="192.168.0.0/24", gateway_ip="192.168.0.1",
|
||||||
allocation_pools=[], dns_nameservers=[],
|
dns_nameservers=[],
|
||||||
enable_dhcp=None)
|
enable_dhcp=None)
|
||||||
expected_route = dict(destination=route["cidr"],
|
expected_route = dict(destination=route["cidr"],
|
||||||
nexthop=route["gateway"])
|
nexthop=route["gateway"])
|
||||||
@@ -141,7 +141,7 @@ class TestQuarkGetSubnets(TestQuarkPlugin):
|
|||||||
subnet = dict(id=subnet_id, network_id=1, name=subnet_id,
|
subnet = dict(id=subnet_id, network_id=1, name=subnet_id,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="192.168.0.0/24", gateway_ip="192.168.0.1",
|
cidr="192.168.0.0/24", gateway_ip="192.168.0.1",
|
||||||
allocation_pools=[], dns_nameservers=[],
|
dns_nameservers=[],
|
||||||
enable_dhcp=None)
|
enable_dhcp=None)
|
||||||
|
|
||||||
with self._stubs(subnets=subnet, routes=[route]):
|
with self._stubs(subnets=subnet, routes=[route]):
|
||||||
@@ -174,6 +174,9 @@ class TestQuarkCreateSubnetOverlapping(TestQuarkPlugin):
|
|||||||
) as (net_find, subnet_find, subnet_create):
|
) as (net_find, subnet_find, subnet_create):
|
||||||
net_find.return_value = network
|
net_find.return_value = network
|
||||||
subnet_find.return_value = subnet_models
|
subnet_find.return_value = subnet_models
|
||||||
|
subnet_create.return_value = models.Subnet(
|
||||||
|
network=models.Network(),
|
||||||
|
cidr="192.168.1.1/24")
|
||||||
yield subnet_create
|
yield subnet_create
|
||||||
|
|
||||||
def test_create_subnet_overlapping_true(self):
|
def test_create_subnet_overlapping_true(self):
|
||||||
@@ -207,13 +210,63 @@ class TestQuarkCreateSubnetOverlapping(TestQuarkPlugin):
|
|||||||
self.plugin.create_subnet(self.context, s)
|
self.plugin.create_subnet(self.context, s)
|
||||||
|
|
||||||
|
|
||||||
|
class TestQuarkCreateSubnetAllocationPools(TestQuarkPlugin):
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _stubs(self, subnet):
|
||||||
|
s = models.Subnet(network=models.Network(id=1, subnets=[]))
|
||||||
|
s.update(subnet)
|
||||||
|
|
||||||
|
with contextlib.nested(
|
||||||
|
mock.patch("quark.db.api.network_find"),
|
||||||
|
mock.patch("quark.db.api.subnet_find"),
|
||||||
|
mock.patch("quark.db.api.subnet_create")
|
||||||
|
) as (net_find, subnet_find, subnet_create):
|
||||||
|
net_find.return_value = s["network"]
|
||||||
|
subnet_find.return_value = []
|
||||||
|
subnet_create.return_value = s
|
||||||
|
yield subnet_create
|
||||||
|
|
||||||
|
def test_create_subnet_allocation_pools_zero(self):
|
||||||
|
s = dict(subnet=dict(
|
||||||
|
cidr="192.168.1.1/24",
|
||||||
|
network_id=1))
|
||||||
|
with self._stubs(s["subnet"]) as subnet_create:
|
||||||
|
resp = self.plugin.create_subnet(self.context, s)
|
||||||
|
self.assertEqual(subnet_create.call_count, 1)
|
||||||
|
self.assertEqual(resp["allocation_pools"],
|
||||||
|
[dict(start="192.168.1.0", end="192.168.1.255")])
|
||||||
|
|
||||||
|
def test_create_subnet_allocation_pools_one(self):
|
||||||
|
pools = [dict(start="192.168.1.10", end="192.168.1.20")]
|
||||||
|
s = dict(subnet=dict(
|
||||||
|
allocation_pools=pools,
|
||||||
|
cidr="192.168.1.1/24",
|
||||||
|
network_id=1))
|
||||||
|
with self._stubs(s["subnet"]) as subnet_create:
|
||||||
|
resp = self.plugin.create_subnet(self.context, s)
|
||||||
|
self.assertEqual(subnet_create.call_count, 1)
|
||||||
|
self.assertEqual(resp["allocation_pools"], pools)
|
||||||
|
|
||||||
|
def test_create_subnet_allocation_pools_two(self):
|
||||||
|
pools = [dict(start="192.168.1.10", end="192.168.1.20"),
|
||||||
|
dict(start="192.168.1.40", end="192.168.1.50")]
|
||||||
|
s = dict(subnet=dict(
|
||||||
|
allocation_pools=pools,
|
||||||
|
cidr="192.168.1.1/24",
|
||||||
|
network_id=1))
|
||||||
|
with self._stubs(s["subnet"]) as subnet_create:
|
||||||
|
resp = self.plugin.create_subnet(self.context, s)
|
||||||
|
self.assertEqual(subnet_create.call_count, 1)
|
||||||
|
self.assertEqual(resp["allocation_pools"], pools)
|
||||||
|
|
||||||
|
|
||||||
# TODO(amir): Refactor the tests to test individual subnet attributes.
|
# TODO(amir): Refactor the tests to test individual subnet attributes.
|
||||||
# * copy.deepcopy was necessary to maintain tests on keys, which is a bit ugly.
|
# * copy.deepcopy was necessary to maintain tests on keys, which is a bit ugly.
|
||||||
# * workaround is also in place for lame ATTR_NOT_SPECIFIED object()
|
# * workaround is also in place for lame ATTR_NOT_SPECIFIED object()
|
||||||
class TestQuarkCreateSubnet(TestQuarkPlugin):
|
class TestQuarkCreateSubnet(TestQuarkPlugin):
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _stubs(self, subnet=None, network=None, routes=None, dns=None):
|
def _stubs(self, subnet=None, network=None, routes=None, dns=None):
|
||||||
subnet_mod = models.Subnet()
|
subnet_mod = models.Subnet(network=models.Network())
|
||||||
dns_ips = subnet.pop("dns_nameservers", [])
|
dns_ips = subnet.pop("dns_nameservers", [])
|
||||||
host_routes = subnet.pop("host_routes", [])
|
host_routes = subnet.pop("host_routes", [])
|
||||||
subnet_mod.update(subnet)
|
subnet_mod.update(subnet)
|
||||||
@@ -242,7 +295,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
subnet=dict(network_id=1,
|
subnet=dict(network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
||||||
allocation_pools=[],
|
|
||||||
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
host_routes=neutron_attrs.ATTR_NOT_SPECIFIED,
|
host_routes=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
enable_dhcp=None))
|
enable_dhcp=None))
|
||||||
@@ -281,7 +333,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
subnet=dict(network_id=1,
|
subnet=dict(network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24",
|
cidr="172.16.0.0/24",
|
||||||
allocation_pools=[],
|
|
||||||
gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED,
|
gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
enable_dhcp=None))
|
enable_dhcp=None))
|
||||||
@@ -316,7 +367,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
subnet=dict(network_id=1,
|
subnet=dict(network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
||||||
allocation_pools=[],
|
|
||||||
dns_nameservers=["4.2.2.1", "4.2.2.2"],
|
dns_nameservers=["4.2.2.1", "4.2.2.2"],
|
||||||
enable_dhcp=None))
|
enable_dhcp=None))
|
||||||
network = dict(network_id=1)
|
network = dict(network_id=1)
|
||||||
@@ -345,7 +395,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
subnet=dict(network_id=1,
|
subnet=dict(network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
cidr="172.16.0.0/24", gateway_ip="0.0.0.0",
|
||||||
allocation_pools=[],
|
|
||||||
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
host_routes=[{"destination": "1.1.1.1/8",
|
host_routes=[{"destination": "1.1.1.1/8",
|
||||||
"nexthop": "172.16.0.4"}],
|
"nexthop": "172.16.0.4"}],
|
||||||
@@ -379,7 +428,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
subnet=dict(network_id=1,
|
subnet=dict(network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24",
|
cidr="172.16.0.0/24",
|
||||||
allocation_pools=[],
|
|
||||||
gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED,
|
gateway_ip=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
host_routes=[{"destination": "0.0.0.0/0",
|
host_routes=[{"destination": "0.0.0.0/0",
|
||||||
@@ -420,7 +468,6 @@ class TestQuarkCreateSubnet(TestQuarkPlugin):
|
|||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24",
|
cidr="172.16.0.0/24",
|
||||||
gateway_ip="172.16.0.3",
|
gateway_ip="172.16.0.3",
|
||||||
allocation_pools=[],
|
|
||||||
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
dns_nameservers=neutron_attrs.ATTR_NOT_SPECIFIED,
|
||||||
host_routes=[{"destination": "0.0.0.0/0",
|
host_routes=[{"destination": "0.0.0.0/0",
|
||||||
"nexthop": "172.16.0.4"}],
|
"nexthop": "172.16.0.4"}],
|
||||||
@@ -473,7 +520,6 @@ class TestQuarkUpdateSubnet(TestQuarkPlugin):
|
|||||||
network_id=1,
|
network_id=1,
|
||||||
tenant_id=self.context.tenant_id, ip_version=4,
|
tenant_id=self.context.tenant_id, ip_version=4,
|
||||||
cidr="172.16.0.0/24",
|
cidr="172.16.0.0/24",
|
||||||
allocation_pools=[],
|
|
||||||
host_routes=host_routes,
|
host_routes=host_routes,
|
||||||
dns_nameservers=["4.2.2.1", "4.2.2.2"],
|
dns_nameservers=["4.2.2.1", "4.2.2.2"],
|
||||||
enable_dhcp=None)
|
enable_dhcp=None)
|
||||||
@@ -503,7 +549,8 @@ class TestQuarkUpdateSubnet(TestQuarkPlugin):
|
|||||||
subnet_find.return_value = subnet_mod
|
subnet_find.return_value = subnet_mod
|
||||||
route_find.return_value = subnet_mod["routes"][0] \
|
route_find.return_value = subnet_mod["routes"][0] \
|
||||||
if subnet_mod["routes"] and find_routes else None
|
if subnet_mod["routes"] and find_routes else None
|
||||||
new_subnet_mod = copy.deepcopy(subnet_mod)
|
new_subnet_mod = models.Subnet(network=models.Network())
|
||||||
|
new_subnet_mod.update(subnet_mod)
|
||||||
if new_routes:
|
if new_routes:
|
||||||
new_subnet_mod["routes"] = new_routes
|
new_subnet_mod["routes"] = new_routes
|
||||||
if new_dns_servers:
|
if new_dns_servers:
|
||||||
|
|||||||
Reference in New Issue
Block a user