Skip to content

Commit ddf96bc

Browse files
committed
cpu: interfaces for managing state and governor
This is the first stage of the power management series. In order to be able to switch the CPU state or change the governor, we need a framework to access sysfs. As some bits can be reused, let's create a nova.filesystem helper module that will define read-write mechanisms for accessing sysfs-specific commands. Partially-Implements: blueprint libvirt-cpu-state-mgmt Change-Id: Icb913ed9be8d508de35e755a9c650ba25e45aca2
1 parent 7ea9aac commit ddf96bc

File tree

10 files changed

+466
-0
lines changed

10 files changed

+466
-0
lines changed

mypy-files.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
nova/compute/manager.py
22
nova/compute/pci_placement_translator.py
33
nova/crypto.py
4+
nova/filesystem.py
45
nova/limit/local.py
56
nova/limit/placement.py
67
nova/network/neutron.py
@@ -13,6 +14,9 @@ nova/virt/driver.py
1314
nova/virt/hardware.py
1415
nova/virt/libvirt/machine_type_utils.py
1516
nova/virt/libvirt/__init__.py
17+
nova/virt/libvirt/cpu/__init__.py
18+
nova/virt/libvirt/cpu/api.py
19+
nova/virt/libvirt/cpu/core.py
1620
nova/virt/libvirt/driver.py
1721
nova/virt/libvirt/event.py
1822
nova/virt/libvirt/guest.py

nova/conf/libvirt.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,15 @@
14781478
"""),
14791479
]
14801480

1481+
libvirt_cpu_mgmt_opts = [
1482+
cfg.StrOpt('cpu_power_governor_low',
1483+
default='powersave',
1484+
help='Governor to use in order '
1485+
'to reduce CPU power consumption'),
1486+
cfg.StrOpt('cpu_power_governor_high',
1487+
default='performance',
1488+
help='Governor to use in order to have best CPU performance'),
1489+
]
14811490

14821491
ALL_OPTS = list(itertools.chain(
14831492
libvirt_general_opts,
@@ -1499,6 +1508,7 @@
14991508
libvirt_volume_nvmeof_opts,
15001509
libvirt_pmem_opts,
15011510
libvirt_vtpm_opts,
1511+
libvirt_cpu_mgmt_opts,
15021512
))
15031513

15041514

nova/filesystem.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""Functions to address filesystem calls, particularly sysfs."""
14+
15+
import os
16+
17+
from oslo_log import log as logging
18+
19+
from nova import exception
20+
21+
LOG = logging.getLogger(__name__)
22+
23+
24+
SYS = '/sys'
25+
26+
27+
# NOTE(bauzas): this method is deliberately not wrapped in a privsep entrypoint
28+
def read_sys(path: str) -> str:
29+
"""Reads the content of a file in the sys filesystem.
30+
31+
:param path: relative or absolute. If relative, will be prefixed by /sys.
32+
:returns: contents of that file.
33+
:raises: nova.exception.FileNotFound if we can't read that file.
34+
"""
35+
try:
36+
# The path can be absolute with a /sys prefix but that's fine.
37+
with open(os.path.join(SYS, path), mode='r') as data:
38+
return data.read()
39+
except (OSError, ValueError) as exc:
40+
raise exception.FileNotFound(file_path=path) from exc
41+
42+
43+
# NOTE(bauzas): this method is deliberately not wrapped in a privsep entrypoint
44+
# In order to correctly use it, you need to decorate the caller with a specific
45+
# privsep entrypoint.
46+
def write_sys(path: str, data: str) -> None:
47+
"""Writes the content of a file in the sys filesystem with data.
48+
49+
:param path: relative or absolute. If relative, will be prefixed by /sys.
50+
:param data: the data to write.
51+
:returns: contents of that file.
52+
:raises: nova.exception.FileNotFound if we can't write that file.
53+
"""
54+
try:
55+
# The path can be absolute with a /sys prefix but that's fine.
56+
with open(os.path.join(SYS, path), mode='w') as fd:
57+
fd.write(data)
58+
except (OSError, ValueError) as exc:
59+
raise exception.FileNotFound(file_path=path) from exc

nova/tests/unit/test_filesystem.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
import os
14+
from unittest import mock
15+
16+
from nova import exception
17+
from nova import filesystem
18+
from nova import test
19+
20+
21+
class TestFSCommon(test.NoDBTestCase):
22+
23+
def test_read_sys(self):
24+
open_mock = mock.mock_open(read_data='bar')
25+
with mock.patch('builtins.open', open_mock) as m_open:
26+
self.assertEqual('bar', filesystem.read_sys('foo'))
27+
expected_path = os.path.join(filesystem.SYS, 'foo')
28+
m_open.assert_called_once_with(expected_path, mode='r')
29+
30+
def test_read_sys_error(self):
31+
with mock.patch('builtins.open',
32+
side_effect=OSError('error')) as m_open:
33+
self.assertRaises(exception.FileNotFound,
34+
filesystem.read_sys, 'foo')
35+
expected_path = os.path.join(filesystem.SYS, 'foo')
36+
m_open.assert_called_once_with(expected_path, mode='r')
37+
38+
def test_write_sys(self):
39+
open_mock = mock.mock_open()
40+
with mock.patch('builtins.open', open_mock) as m_open:
41+
self.assertIsNone(filesystem.write_sys('foo', 'bar'))
42+
expected_path = os.path.join(filesystem.SYS, 'foo')
43+
m_open.assert_called_once_with(expected_path, mode='w')
44+
open_mock().write.assert_called_once_with('bar')
45+
46+
def test_write_sys_error(self):
47+
with mock.patch('builtins.open',
48+
side_effect=OSError('fake_error')) as m_open:
49+
self.assertRaises(exception.FileNotFound,
50+
filesystem.write_sys, 'foo', 'bar')
51+
expected_path = os.path.join(filesystem.SYS, 'foo')
52+
m_open.assert_called_once_with(expected_path, mode='w')

nova/tests/unit/virt/libvirt/cpu/__init__.py

Whitespace-only changes.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from unittest import mock
14+
15+
from nova import test
16+
from nova.virt.libvirt.cpu import api
17+
from nova.virt.libvirt.cpu import core
18+
19+
20+
class TestAPI(test.NoDBTestCase):
21+
22+
def setUp(self):
23+
super(TestAPI, self).setUp()
24+
self.core_1 = api.Core(1)
25+
26+
@mock.patch.object(core, 'get_online')
27+
def test_online(self, mock_get_online):
28+
mock_get_online.return_value = True
29+
self.assertTrue(self.core_1.online)
30+
mock_get_online.assert_called_once_with(self.core_1.ident)
31+
32+
@mock.patch.object(core, 'set_online')
33+
def test_set_online(self, mock_set_online):
34+
self.core_1.online = True
35+
mock_set_online.assert_called_once_with(self.core_1.ident)
36+
37+
@mock.patch.object(core, 'set_offline')
38+
def test_set_offline(self, mock_set_offline):
39+
self.core_1.online = False
40+
mock_set_offline.assert_called_once_with(self.core_1.ident)
41+
42+
def test_hash(self):
43+
self.assertEqual(hash(self.core_1.ident), hash(self.core_1))
44+
45+
@mock.patch.object(core, 'get_governor')
46+
def test_governor(self, mock_get_governor):
47+
mock_get_governor.return_value = 'fake_governor'
48+
self.assertEqual('fake_governor', self.core_1.governor)
49+
mock_get_governor.assert_called_once_with(self.core_1.ident)
50+
51+
@mock.patch.object(core, 'set_governor')
52+
def test_set_governor_low(self, mock_set_governor):
53+
self.flags(cpu_power_governor_low='fake_low_gov', group='libvirt')
54+
self.core_1.set_low_governor()
55+
mock_set_governor.assert_called_once_with(self.core_1.ident,
56+
'fake_low_gov')
57+
58+
@mock.patch.object(core, 'set_governor')
59+
def test_set_governor_high(self, mock_set_governor):
60+
self.flags(cpu_power_governor_high='fake_high_gov', group='libvirt')
61+
self.core_1.set_high_governor()
62+
mock_set_governor.assert_called_once_with(self.core_1.ident,
63+
'fake_high_gov')
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from unittest import mock
14+
15+
from nova import exception
16+
from nova import test
17+
from nova.tests import fixtures
18+
from nova.virt.libvirt.cpu import core
19+
20+
21+
class TestCore(test.NoDBTestCase):
22+
23+
@mock.patch.object(core.filesystem, 'read_sys')
24+
@mock.patch.object(core.hardware, 'parse_cpu_spec')
25+
def test_get_available_cores(self, mock_parse_cpu_spec, mock_read_sys):
26+
mock_read_sys.return_value = '1-2'
27+
mock_parse_cpu_spec.return_value = set([1, 2])
28+
self.assertEqual(set([1, 2]), core.get_available_cores())
29+
mock_read_sys.assert_called_once_with(core.AVAILABLE_PATH)
30+
mock_parse_cpu_spec.assert_called_once_with('1-2')
31+
32+
@mock.patch.object(core.filesystem, 'read_sys')
33+
@mock.patch.object(core.hardware, 'parse_cpu_spec')
34+
def test_get_available_cores_none(
35+
self, mock_parse_cpu_spec, mock_read_sys):
36+
mock_read_sys.return_value = ''
37+
self.assertEqual(set(), core.get_available_cores())
38+
mock_parse_cpu_spec.assert_not_called()
39+
40+
@mock.patch.object(core, 'get_available_cores')
41+
def test_exists(self, mock_get_available_cores):
42+
mock_get_available_cores.return_value = set([1])
43+
self.assertTrue(core.exists(1))
44+
mock_get_available_cores.assert_called_once_with()
45+
self.assertFalse(core.exists(2))
46+
47+
@mock.patch.object(
48+
core, 'CPU_PATH_TEMPLATE',
49+
new_callable=mock.PropertyMock(return_value='/sys/blah%(core)s'))
50+
@mock.patch.object(core, 'exists')
51+
def test_gen_cpu_path(self, mock_exists, mock_cpu_path):
52+
mock_exists.return_value = True
53+
self.assertEqual('/sys/blah1', core.gen_cpu_path(1))
54+
mock_exists.assert_called_once_with(1)
55+
56+
@mock.patch.object(core, 'exists')
57+
def test_gen_cpu_path_raises(self, mock_exists):
58+
mock_exists.return_value = False
59+
self.assertRaises(ValueError, core.gen_cpu_path, 1)
60+
self.assertIn('Unable to access CPU: 1', self.stdlog.logger.output)
61+
62+
63+
class TestCoreHelpers(test.NoDBTestCase):
64+
65+
def setUp(self):
66+
super(TestCoreHelpers, self).setUp()
67+
self.useFixture(fixtures.PrivsepFixture())
68+
_p1 = mock.patch.object(core, 'exists', return_value=True)
69+
self.mock_exists = _p1.start()
70+
self.addCleanup(_p1.stop)
71+
72+
_p2 = mock.patch.object(core, 'gen_cpu_path',
73+
side_effect=lambda x: '/fakesys/blah%s' % x)
74+
self.mock_gen_cpu_path = _p2.start()
75+
self.addCleanup(_p2.stop)
76+
77+
@mock.patch.object(core.filesystem, 'read_sys')
78+
def test_get_online(self, mock_read_sys):
79+
mock_read_sys.return_value = '1'
80+
self.assertTrue(core.get_online(1))
81+
mock_read_sys.assert_called_once_with('/fakesys/blah1/online')
82+
83+
@mock.patch.object(core.filesystem, 'read_sys')
84+
def test_get_online_not_exists(self, mock_read_sys):
85+
mock_read_sys.side_effect = exception.FileNotFound(file_path='foo')
86+
self.assertTrue(core.get_online(1))
87+
mock_read_sys.assert_called_once_with('/fakesys/blah1/online')
88+
89+
@mock.patch.object(core.filesystem, 'write_sys')
90+
@mock.patch.object(core, 'get_online')
91+
def test_set_online(self, mock_get_online, mock_write_sys):
92+
mock_get_online.return_value = True
93+
self.assertTrue(core.set_online(1))
94+
mock_write_sys.assert_called_once_with('/fakesys/blah1/online',
95+
data='1')
96+
mock_get_online.assert_called_once_with(1)
97+
98+
@mock.patch.object(core.filesystem, 'write_sys')
99+
@mock.patch.object(core, 'get_online')
100+
def test_set_offline(self, mock_get_online, mock_write_sys):
101+
mock_get_online.return_value = False
102+
self.assertTrue(core.set_offline(1))
103+
mock_write_sys.assert_called_once_with('/fakesys/blah1/online',
104+
data='0')
105+
mock_get_online.assert_called_once_with(1)
106+
107+
@mock.patch.object(core.filesystem, 'read_sys')
108+
def test_get_governor(self, mock_read_sys):
109+
mock_read_sys.return_value = 'fake_gov'
110+
self.assertEqual('fake_gov', core.get_governor(1))
111+
mock_read_sys.assert_called_once_with(
112+
'/fakesys/blah1/cpufreq/scaling_governor')
113+
114+
@mock.patch.object(core, 'get_governor')
115+
@mock.patch.object(core.filesystem, 'write_sys')
116+
def test_set_governor(self, mock_write_sys, mock_get_governor):
117+
mock_get_governor.return_value = 'fake_gov'
118+
self.assertEqual('fake_gov',
119+
core.set_governor(1, 'fake_gov'))
120+
mock_write_sys.assert_called_once_with(
121+
'/fakesys/blah1/cpufreq/scaling_governor', data='fake_gov')
122+
mock_get_governor.assert_called_once_with(1)

nova/virt/libvirt/cpu/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from nova.virt.libvirt.cpu import api
14+
15+
16+
Core = api.Core

0 commit comments

Comments
 (0)