[lxc-devel] [nova-lxd/master] Disk and NIC quotas

peter-slovak on Github lxc-bot at linuxcontainers.org
Fri May 13 17:14:12 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 760 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160513/2560fa2e/attachment.bin>
-------------- next part --------------
From fdfcbd43fe45eb92224bbab47cdae2fc78d91f70 Mon Sep 17 00:00:00 2001
From: Peter Slovak <peter.slovak at websupport.sk>
Date: Fri, 13 May 2016 15:35:41 +0200
Subject: [PATCH 1/3] respect disk and network quotas defined in
 OS::Compute::Quota MD namespace

---
 nova/virt/lxd/config.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 72 insertions(+), 2 deletions(-)

diff --git a/nova/virt/lxd/config.py b/nova/virt/lxd/config.py
index 555f5cb..9c31807 100644
--- a/nova/virt/lxd/config.py
+++ b/nova/virt/lxd/config.py
@@ -22,7 +22,7 @@
 from nova.virt import configdrive
 
 from oslo_log import log as logging
-from oslo_utils import excutils
+from oslo_utils import excutils, units
 
 from nova.virt.lxd import session
 from nova.virt.lxd import utils as container_dir
@@ -128,7 +128,7 @@ def create_config(self, instance_name, instance):
         try:
             config = {}
 
-            # Update continaer options
+            # Update container options
             config.update(self.config_instance_options(config, instance))
 
             # Set the instance memory limit
@@ -188,6 +188,10 @@ def configure_container_root(self, instance):
             else:
                 config['root'] = {'path': '/',
                                   'type': 'disk'}
+
+            # Set disk quotas
+            config['root'].update(self.create_disk_quota_config(instance))
+
             return config
         except Exception as ex:
             with excutils.save_and_reraise_exception():
@@ -196,6 +200,41 @@ def configure_container_root(self, instance):
                           {'instance': instance.name, 'ex': ex},
                           instance=instance)
 
+    def create_disk_quota_config(self, instance):
+        extra_specs = instance.flavor.extra_specs
+        disk_config = {}
+
+        # Get disk quotas from flavor metadata
+        disk_read_iops_sec = extra_specs.get('quota:disk_read_iops_sec')
+        disk_read_bytes_sec = extra_specs.get('quota:disk_read_bytes_sec')
+        disk_write_iops_sec = extra_specs.get('quota:disk_write_iops_sec')
+        disk_write_bytes_sec = extra_specs.get('quota:disk_write_bytes_sec')
+        disk_total_iops_sec = extra_specs.get('quota:disk_total_iops_sec')
+        disk_total_bytes_sec = extra_specs.get('quota:disk_total_bytes_sec')
+
+        # Bytes and IOps are not separate config options in a container
+        # profile - we let Bytes take priority over IOps if both are set.
+        # Align all limits to MiB/s, which should be a sensible middle road.
+        if disk_read_iops_sec:
+            disk_config['limits.read'] = str(disk_read_iops_sec) + 'iops'
+
+        if disk_read_bytes_sec:
+            disk_config['limits.read'] = str(disk_read_bytes_sec / units.Mi) + 'MB'
+
+        if disk_write_iops_sec:
+            disk_config['limits.write'] = str(disk_write_iops_sec) + 'iops'
+
+        if disk_write_bytes_sec:
+            disk_config['limits.write'] = str(disk_write_bytes_sec / units.Mi) + 'MB'
+
+        if disk_total_iops_sec:
+            disk_config['limits.max'] = str(disk_total_iops_sec) + 'iops'
+
+        if disk_total_bytes_sec:
+            disk_config['limits.max'] = str(disk_total_bytes_sec / units.Mi) + 'MB'
+
+        return disk_config
+
     def create_network(self, instance_name, instance, network_info):
         """Create the LXD container network on the host
 
@@ -218,6 +257,11 @@ def create_network(self, instance_name, instance, network_info):
                      'hwaddr': str(cfg['mac_address']),
                      'parent': str(cfg['bridge']),
                      'type': 'nic'}
+
+                # Set network device quotas
+                network_devices[str(cfg['bridge'])].update(
+                    self.create_network_quota_config(instance)
+                )
                 return network_devices
         except Exception as ex:
             with excutils.save_and_reraise_exception():
@@ -225,6 +269,32 @@ def create_network(self, instance_name, instance, network_info):
                     _LE('Fail to configure network for %(instance)s: %(ex)s'),
                     {'instance': instance_name, 'ex': ex}, instance=instance)
 
+    def create_network_quota_config(self, instance):
+        extra_specs = instance.flavor.extra_specs
+        network_config = {}
+
+        # Get network quotas from flavor metadata
+        vif_inbound_average = extra_specs.get('quota:vif_inbound_average')
+        vif_inbound_burst = extra_specs.get('quota:vif_inbound_burst')
+        vif_inbound_peak = extra_specs.get('quota:vif_inbound_peak')
+        vif_outbound_average = extra_specs.get('quota:vif_outbound_average')
+        vif_outbound_burst = extra_specs.get('quota:vif_outbound_burst')
+        vif_outbound_peak = extra_specs.get('quota:vif_outbound_peak')
+
+        # Since LXD does not implement average NIC IO and number of burst
+        # bytes, we take the max(vif_*_average, vif_*_peak) to set the peak
+        # network IO and simply ignore the burst bytes.
+        # Align values to MB/s (powers of 1000 in this case)
+        vif_inbound_limit = max(vif_inbound_average, vif_inbound_peak)
+        if vif_inbound_limit:
+            network_config['limits.ingress'] = str(vif_inbound_limit / units.M) + 'Mbit'
+
+        vif_outbound_limit = max(vif_outbound_average, vif_outbound_peak)
+        if vif_outbound_limit:
+            network_config['limits.egress'] = str(vif_outbound_limit / units.M) + 'Mbit'
+
+        return network_config
+
     def get_container_source(self, instance):
         """Set the LXD container image for the instance.
 

From b6359441b3113c2ffc85a62738e77718fc56758c Mon Sep 17 00:00:00 2001
From: Peter Slovak <peter.slovak at websupport.sk>
Date: Fri, 13 May 2016 15:43:36 +0200
Subject: [PATCH 2/3] skip defining 'limits.max' if a more concrete quota has
 been defined

---
 nova/virt/lxd/config.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/nova/virt/lxd/config.py b/nova/virt/lxd/config.py
index 9c31807..836ec33 100644
--- a/nova/virt/lxd/config.py
+++ b/nova/virt/lxd/config.py
@@ -227,10 +227,15 @@ def create_disk_quota_config(self, instance):
         if disk_write_bytes_sec:
             disk_config['limits.write'] = str(disk_write_bytes_sec / units.Mi) + 'MB'
 
-        if disk_total_iops_sec:
+        # If at least one of the above limits has been defined, do not set
+        # the "max" quota (which would apply to both read and write)
+        minor_quota_defined = (disk_read_iops_sec or disk_write_iops_sec or
+                               disk_read_bytes_sec or disk_write_bytes_sec)
+
+        if disk_total_iops_sec and not minor_quota_defined:
             disk_config['limits.max'] = str(disk_total_iops_sec) + 'iops'
 
-        if disk_total_bytes_sec:
+        if disk_total_bytes_sec and not minor_quota_defined:
             disk_config['limits.max'] = str(disk_total_bytes_sec / units.Mi) + 'MB'
 
         return disk_config

From b8062285ad279e1eb7f1a2ffa471006c1da7c815 Mon Sep 17 00:00:00 2001
From: Peter Slovak <peter.slovak at websupport.sk>
Date: Fri, 13 May 2016 19:06:15 +0200
Subject: [PATCH 3/3] tests for disk and nic quotas

---
 nova/tests/unit/virt/lxd/test_config.py | 118 ++++++++++++++++++++++++++++++++
 nova/virt/lxd/config.py                 |  26 ++++---
 2 files changed, 133 insertions(+), 11 deletions(-)

diff --git a/nova/tests/unit/virt/lxd/test_config.py b/nova/tests/unit/virt/lxd/test_config.py
index 19b658d..27eda7c 100644
--- a/nova/tests/unit/virt/lxd/test_config.py
+++ b/nova/tests/unit/virt/lxd/test_config.py
@@ -22,6 +22,8 @@
 from nova.virt.lxd import config
 from nova.virt.lxd import session
 from nova.virt.lxd import utils as container_dir
+from oslo_utils import units
+
 import stubs
 
 
@@ -132,3 +134,119 @@ def test_container_privileged_container(self):
         config = self.config.config_instance_options({}, instance)
         self.assertEqual({'security.privileged': 'True',
                           'boot.autostart': 'True'}, config)
+
+    @mock.patch.object(session.LXDAPISession, 'get_host_config',
+                       mock.Mock(return_value={'storage': 'zfs'}))
+    def test_disk_quota_rw_iops(self):
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {'quota:disk_read_iops_sec': 10000,
+                                       'quota:disk_write_iops_sec': 10000}
+        config = self.config.configure_container_root(instance)
+        self.assertEqual({'root': {'path': '/',
+                                   'type': 'disk',
+                                   'size': '10GB',
+                                   'limits.read': '10000iops',
+                                   'limits.write': '10000iops'}}, config)
+
+    @mock.patch.object(session.LXDAPISession, 'get_host_config',
+                       mock.Mock(return_value={'storage': 'zfs'}))
+    def test_disk_quota_rw_iops_and_bytes(self):
+        # Byte values should take precedence
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:disk_read_iops_sec': 10000,
+            'quota:disk_write_iops_sec': 10000,
+            'quota:disk_read_bytes_sec': 13 * units.Mi,
+            'quota:disk_write_bytes_sec': 5 * units.Mi
+        }
+        config = self.config.configure_container_root(instance)
+        self.assertEqual({'root': {'path': '/',
+                                   'type': 'disk',
+                                   'size': '10GB',
+                                   'limits.read': '13MB',
+                                   'limits.write': '5MB'}}, config)
+
+    @mock.patch.object(session.LXDAPISession, 'get_host_config',
+                       mock.Mock(return_value={'storage': 'zfs'}))
+    def test_disk_quota_total_iops(self):
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:disk_total_iops_sec': 10000
+        }
+        config = self.config.configure_container_root(instance)
+        self.assertEqual({'root': {'path': '/',
+                                   'type': 'disk',
+                                   'size': '10GB',
+                                   'limits.max': '10000iops'}}, config)
+
+    @mock.patch.object(session.LXDAPISession, 'get_host_config',
+                       mock.Mock(return_value={'storage': 'zfs'}))
+    def test_disk_quota_total_iops_and_bytes(self):
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:disk_total_iops_sec': 10000,
+            'quota:disk_total_bytes_sec': 11 * units.Mi
+        }
+        config = self.config.configure_container_root(instance)
+        self.assertEqual({'root': {'path': '/',
+                                   'type': 'disk',
+                                   'size': '10GB',
+                                   'limits.max': '11MB'}}, config)
+
+    @mock.patch.object(session.LXDAPISession, 'get_host_config',
+                       mock.Mock(return_value={'storage': 'zfs'}))
+    def test_disk_quota_rw_and_total_iops_and_bytes(self):
+        # More granular quotas should be set only, moreover
+        # in MBytes, not iops
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:disk_read_iops_sec': 10000,
+            'quota:disk_write_iops_sec': 10000,
+            'quota:disk_read_bytes_sec': 13 * units.Mi,
+            'quota:disk_write_bytes_sec': 5 * units.Mi,
+            'quota:disk_total_iops_sec': 10000,
+            'quota:disk_total_bytes_sec': 11 * units.Mi
+        }
+        config = self.config.configure_container_root(instance)
+        self.assertEqual({'root': {'path': '/',
+                                   'type': 'disk',
+                                   'size': '10GB',
+                                   'limits.read': '13MB',
+                                   'limits.write': '5MB'}}, config)
+
+    def test_network_in_out_average(self):
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:vif_inbound_average': 20 * units.M,
+            'quota:vif_outbound_average': 8 * units.M
+        }
+        instance_name = 'fake_instance'
+        network_info = fake_network.fake_get_instance_nw_info(self)
+        config = self.config.create_network(instance_name, instance,
+                                            network_info)
+        self.assertEqual({'fake_br1': {'hwaddr': 'DE:AD:BE:EF:00:01',
+                                       'nictype': 'bridged',
+                                       'parent': 'fake_br1',
+                                       'type': 'nic',
+                                       'limits.ingress': '20Mbit',
+                                       'limits.egress': '8Mbit'}}, config)
+
+    def test_network_in_out_average_and_peak(self):
+        # Max of the two values should take precedence
+        instance = stubs._fake_instance()
+        instance.flavor.extra_specs = {
+            'quota:vif_inbound_average': 20 * units.M,
+            'quota:vif_outbound_average': 9 * units.M,
+            'quota:vif_inbound_peak': 21 * units.M,
+            'quota:vif_outbound_peak': 8 * units.M,
+        }
+        instance_name = 'fake_instance'
+        network_info = fake_network.fake_get_instance_nw_info(self)
+        config = self.config.create_network(instance_name, instance,
+                                            network_info)
+        self.assertEqual({'fake_br1': {'hwaddr': 'DE:AD:BE:EF:00:01',
+                                       'nictype': 'bridged',
+                                       'parent': 'fake_br1',
+                                       'type': 'nic',
+                                       'limits.ingress': '21Mbit',
+                                       'limits.egress': '9Mbit'}}, config)
diff --git a/nova/virt/lxd/config.py b/nova/virt/lxd/config.py
index 836ec33..a29f387 100644
--- a/nova/virt/lxd/config.py
+++ b/nova/virt/lxd/config.py
@@ -22,7 +22,8 @@
 from nova.virt import configdrive
 
 from oslo_log import log as logging
-from oslo_utils import excutils, units
+from oslo_utils import excutils
+from oslo_utils import units
 
 from nova.virt.lxd import session
 from nova.virt.lxd import utils as container_dir
@@ -216,16 +217,18 @@ def create_disk_quota_config(self, instance):
         # profile - we let Bytes take priority over IOps if both are set.
         # Align all limits to MiB/s, which should be a sensible middle road.
         if disk_read_iops_sec:
-            disk_config['limits.read'] = str(disk_read_iops_sec) + 'iops'
+            disk_config['limits.read'] = disk_read_iops_sec + 'iops'
 
         if disk_read_bytes_sec:
-            disk_config['limits.read'] = str(disk_read_bytes_sec / units.Mi) + 'MB'
+            disk_config['limits.read'] = \
+                str(int(disk_read_bytes_sec) / units.Mi) + 'MB'
 
         if disk_write_iops_sec:
-            disk_config['limits.write'] = str(disk_write_iops_sec) + 'iops'
+            disk_config['limits.write'] = disk_write_iops_sec + 'iops'
 
         if disk_write_bytes_sec:
-            disk_config['limits.write'] = str(disk_write_bytes_sec / units.Mi) + 'MB'
+            disk_config['limits.write'] = \
+                str(int(disk_write_bytes_sec) / units.Mi) + 'MB'
 
         # If at least one of the above limits has been defined, do not set
         # the "max" quota (which would apply to both read and write)
@@ -233,10 +236,11 @@ def create_disk_quota_config(self, instance):
                                disk_read_bytes_sec or disk_write_bytes_sec)
 
         if disk_total_iops_sec and not minor_quota_defined:
-            disk_config['limits.max'] = str(disk_total_iops_sec) + 'iops'
+            disk_config['limits.max'] = disk_total_iops_sec + 'iops'
 
         if disk_total_bytes_sec and not minor_quota_defined:
-            disk_config['limits.max'] = str(disk_total_bytes_sec / units.Mi) + 'MB'
+            disk_config['limits.max'] = \
+                str(int(disk_total_bytes_sec) / units.Mi) + 'MB'
 
         return disk_config
 
@@ -280,10 +284,8 @@ def create_network_quota_config(self, instance):
 
         # Get network quotas from flavor metadata
         vif_inbound_average = extra_specs.get('quota:vif_inbound_average')
-        vif_inbound_burst = extra_specs.get('quota:vif_inbound_burst')
         vif_inbound_peak = extra_specs.get('quota:vif_inbound_peak')
         vif_outbound_average = extra_specs.get('quota:vif_outbound_average')
-        vif_outbound_burst = extra_specs.get('quota:vif_outbound_burst')
         vif_outbound_peak = extra_specs.get('quota:vif_outbound_peak')
 
         # Since LXD does not implement average NIC IO and number of burst
@@ -292,11 +294,13 @@ def create_network_quota_config(self, instance):
         # Align values to MB/s (powers of 1000 in this case)
         vif_inbound_limit = max(vif_inbound_average, vif_inbound_peak)
         if vif_inbound_limit:
-            network_config['limits.ingress'] = str(vif_inbound_limit / units.M) + 'Mbit'
+            network_config['limits.ingress'] = \
+                str(int(vif_inbound_limit) / units.M) + 'Mbit'
 
         vif_outbound_limit = max(vif_outbound_average, vif_outbound_peak)
         if vif_outbound_limit:
-            network_config['limits.egress'] = str(vif_outbound_limit / units.M) + 'Mbit'
+            network_config['limits.egress'] = \
+                str(int(vif_outbound_limit) / units.M) + 'Mbit'
 
         return network_config
 


More information about the lxc-devel mailing list