Skip to content

Commit 38592c2

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "cpu: interfaces for managing state and governor"
2 parents 1330b28 + ddf96bc commit 38592c2

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)