Add kube-cpusets tool to summarize kubernetes container cpusets

This tool gathers cpuset usage information for all kubernetes
containers that are running on the current host.

With kubernetes CPUManager policy:
- 'none' -- the k8s-infra cpuset is used for all pods
- 'static' -- pods get exclusive cpuset for QoS Guaranteed or
  using isolcpus, otherwise pods inherit DefaultCPUSet.

This displays the cpusets per container and the mapping to numa nodes.
This displays the aggregate cpuset usage per system-level groupings
(i.e., platform, isolated, guaranteed, default), per numa-node.

Story: 2006999
Task: 39579

Change-Id: I7f1b12e2bbcf7d0b1606c1c948c545216ec454c5
Signed-off-by: Jim Gauld <james.gauld@windriver.com>
This commit is contained in:
Jim Gauld 2020-04-28 02:03:57 -04:00
parent f7437000c7
commit 1a5e6c4c3d
19 changed files with 1508 additions and 0 deletions

View File

@ -10,3 +10,6 @@ monitor-tools
# vm-topology
vm-topology
# kube-cpusets
kube-cpusets

View File

@ -2,3 +2,4 @@ collectd-extensions
influxdb-extensions
monitor-tools
vm-topology
kube-cpusets

202
kube-cpusets/LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

12
kube-cpusets/PKG-INFO Normal file
View File

@ -0,0 +1,12 @@
Metadata-Version: 1.1
Name: kube-cpusets
Version: 1.0
Summary: Display kubernetes containers cpusets per numa-node
Home-page:
Author: Windriver
Author-email: info@windriver.com
License: Apache-2.0
Description: Display kubernetes containers cpusets per numa-node
Platform: UNKNOWN

View File

@ -0,0 +1,4 @@
PACKAGE_NAME=kube-cpusets
VERSION=1.0
SRC_DIR=$PKG_BASE/$PACKAGE_NAME
TIS_PATCH_VER=PKG_GITREVCOUNT

View File

@ -0,0 +1,67 @@
%global pypi_name kube-cpusets
Summary: Display kubernetes containers cpusets per numa node
Name: kube-cpusets
Version: 1.0
Release: %{tis_patch_ver}%{?_tis_dist}
License: Apache-2.0
Group: base
Packager: Wind River <info@windriver.com>
URL: unknown
Source0: %{pypi_name}-%{version}.tar.gz
BuildArch: noarch
BuildRequires: python
BuildRequires: python-setuptools
BuildRequires: python2-pip
BuildRequires: python2-wheel
Requires: python
Requires: python-devel
Requires: /usr/bin/env
Requires: /bin/bash
%define debug_package %{nil}
%description
Display kubernetes containers cpusets per numa node
%define pythonroot /usr/lib64/python2.7/site-packages
%prep
%autosetup -p 1 -n %{pypi_name}-%{version}
# Remove bundled egg-info
rm -rf %{pypi_name}.egg-info
# Let RPM handle the dependencies
rm -f requirements.txt
rm -f test-requirements.txt
%build
%{__python2} setup.py build
%py2_build_wheel
%install
%{__python2} setup.py install --skip-build --root %{buildroot}
mkdir -p $RPM_BUILD_ROOT/wheels
install -m 644 dist/*.whl $RPM_BUILD_ROOT/wheels/
%files
%defattr(-,root,root,-)
%doc LICENSE
%{_bindir}/kube-cpusets
%{python2_sitelib}/kube_cpusets
%{python2_sitelib}/*.egg-info
%package wheels
Summary: %{name} wheels
%description wheels
Contains python wheels for %{name}
%files wheels
/wheels/*
%clean
rm -rf $RPM_BUILD_ROOT

View File

@ -0,0 +1,7 @@
[run]
branch = True
source = kube_cpusets
omit = kube_cpusets/tests/*
[report]
ignore_errors = True

View File

@ -0,0 +1,2 @@
[DEFAULT]
test_path=kube_cpusets/tests

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,6 @@
"""
Copyright (c) 2020 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""

View File

@ -0,0 +1,474 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""
This tool gathers cpuset usage information for all kubernetes containers
that are running on the current host.
With kubernetes CPUManager policy 'none', the k8s-infra cpuset is used for
all pods. For policy 'static', pods get an exclusive cpuset in the case of
QoS Guaranteed or using isolcpus, otherwise pods inherit DefaultCPUSet.
This displays the cpusets per container and the mapping to numa nodes.
This displays the aggregate cpuset usage per system-level groupings
(i.e., platform, isolated, guaranteed, default), per numa-node.
Usage: kube-cpusets [-h] [--debug]
"""
import argparse
import itertools as it
import json
import logging
import logging.handlers
import os
import re
import socket
import subprocess
import sys
import prettytable
# Constants
statefile = '/var/lib/kubelet/cpu_manager_state'
platformconf = '/etc/platform/platform.conf'
reservedfile = '/etc/platform/worker_reserved.conf'
LOG = logging.getLogger(__name__)
def format_range_set(items):
"""Generate a pretty-printed value of ranges from a set of integers.
e.g., given a set or list of integers, format as '3-6,8-9,12-17'
"""
ranges = []
for _, iterable in it.groupby(enumerate(sorted(items)),
lambda x: x[1] - x[0]):
rng = list(iterable)
if len(rng) == 1:
s = str(rng[0][1])
else:
s = "%s-%s" % (rng[0][1], rng[-1][1])
ranges.append(s)
return ','.join(ranges)
def range_to_list(csv_range=None):
"""Convert a string of comma separate ranges into an expanded list.
e.g., '1-3,8-9,15' is converted to [1,2,3,8,9,15]
"""
if not csv_range:
return []
ranges = [(lambda L: range(L[0], L[-1] + 1))(map(int, r.split('-')))
for r in csv_range.split(',')]
return [y for x in ranges for y in x]
def cpuset_from_cpulist_file(filename):
"""Read cpulist file and convert to set of integers.
File containing comma separated ranges is converted to an expanded set
of integers. e.g., '1-3,8-9,15' is converted to set([1,2,3,8,9,15])
"""
cpuset_str = None
try:
with open(filename, 'r') as f:
cpuset_str = f.readline().strip()
except Exception as e:
LOG.error('Cannot parse file:%s, error=%s', filename, e)
cpuset = set(range_to_list(csv_range=cpuset_str))
return cpuset
def get_isolated_cpuset():
"""Get isolated cpuset from sys devices.
Reads sys devices isolated cpu file containing comma separated ranges
and convert to an expanded set of integers.
"""
filename = '/sys/devices/system/cpu/isolated'
cpuset = cpuset_from_cpulist_file(filename)
return cpuset
def get_online_cpuset():
"""Get online cpuset from sys devices.
Reads sys devices online cpu file containing comma separated ranges
and convert to an expanded set of integers.
"""
filename = '/sys/devices/system/cpu/online'
cpuset = cpuset_from_cpulist_file(filename)
return cpuset
def get_k8sinfra_cpuset():
"""Get cgroup k8s-infra cpuset from sys fs cgroup.
Reads sys fs cgroup k8s-infra cpuset.cpus file containing comma
separated ranges and convert to an expanded set of integers.
"""
filename = '/sys/fs/cgroup/cpuset/k8s-infra/cpuset.cpus'
cpuset = cpuset_from_cpulist_file(filename)
return cpuset
def get_node_cpusets():
"""Get cpusets per numa node from sys devices.
Returns a list of nodes with the set of integers per numa node.
"""
nodepath = '/sys/devices/system/node'
re_node = re.compile(r'^node(\d+)$')
nodes = {}
if os.path.isdir(nodepath):
for d in os.listdir(nodepath):
match = re_node.search(d)
if match:
node = int(match.group(1))
filename = nodepath + '/node' + str(node) + '/cpulist'
cpuset = set()
if os.path.isfile(filename):
cpuset = cpuset_from_cpulist_file(filename)
nodes[node] = cpuset
return nodes
def gather_info_and_display():
"""Gather cpuset information for all kubernetes containers.
Display the cpuset per container and the mapping to numa nodes.
Display the aggregate cpuset usage per system-level groupings,
per-numa node.
"""
hostname = socket.gethostname()
# Read current host cpusets from sysfs
node_cpusets = get_node_cpusets()
isolated_cpuset = get_isolated_cpuset()
online_cpuset = get_online_cpuset()
k8sinfra_cpuset = get_k8sinfra_cpuset()
LOG.debug('node_cpusets = %r', node_cpusets)
LOG.debug('isolated_cpuset = %r', isolated_cpuset)
LOG.debug('online_cpuset = %r', online_cpuset)
LOG.debug('k8sinfra_cpuset = %r', k8sinfra_cpuset)
# Obtain platform node configuration
re_keyval = re.compile(r'^(\S+)\s*=\s*(\S+)')
platconf = {}
try:
with open(platformconf, 'r') as f:
for line in f:
m = re.search(re_keyval, line)
if m:
key = m.group(1)
value = m.group(2)
platconf[key] = value
except Exception as e:
LOG.error('Could not parse: %s, error: %s.', platformconf, e)
return 1
nodetype = platconf.get('nodetype')
subfunction = platconf.get('subfunction')
system_type = platconf.get('system_type')
# Obtain platform cpuset for worker node, as configured by sysinv/puppet.
re_platform = re.compile(r'^PLATFORM_CPU_LIST\s*=\s*\"(\S+)\"')
if 'worker' in subfunction:
cpulist_str = None
try:
with open(reservedfile, 'r') as f:
for line in f:
m = re.search(re_platform, line)
if m:
cpulist_str = m.group(1)
except Exception as e:
LOG.error('Could not parse: %s, error: %s.', reservedfile, e)
platform_cpuset = set(range_to_list(csv_range=cpulist_str))
else:
platform_cpuset = online_cpuset - isolated_cpuset
LOG.debug('platform_cpuset = %r', platform_cpuset)
# Read cpusets from kubelet cpumanager JSON state file dictionary
state = {}
try:
with open(statefile, 'r') as f:
state = json.load(f)
except Exception as e:
LOG.error('Could not load: %s, error: %s.', statefile, e)
return 1
LOG.debug('cpu-manager state = %r', state)
# Obtain cpu-manager policy
policy = str(state['policyName'])
# Print tool header line
LOG.info('host:%s, system_type=%s, nodetype=%s, subfunction=%s, '
'cpumanager_policy=%s',
hostname, system_type, nodetype, subfunction, policy)
# Determine default cpu-manager cpuset
if 'defaultCpuSet' not in state:
LOG.error('Missing defaultCpuSet in %s', statefile)
return 1
default_cpuranges = str(state['defaultCpuSet'])
default_cpuset = set(range_to_list(csv_range=default_cpuranges))
LOG.debug('default_cpuset = %r', default_cpuset)
# Determine aggregate of cpumanager static allocations,
# i.e., this contains: platform, guaranteed, isolated .
static_cpuset = set()
if 'entries' in state:
for _, dcpus in state['entries'].items():
for cpus in [str(i) for i in dcpus.values()]:
cpulist = set(range_to_list(csv_range=cpus))
static_cpuset.update(cpulist)
# Determine guaranteed cpuset
guaranteed_cpuset = static_cpuset - platform_cpuset - isolated_cpuset
LOG.debug('guaranteed_cpuset = %r', guaranteed_cpuset)
# Determine isolated cpuset
isolated_used_cpuset = static_cpuset.intersection(isolated_cpuset)
isolated_free_cpuset = isolated_cpuset - isolated_used_cpuset
# Get list of containers on this host
cmd = ['crictl', 'ps', '--output=json']
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
LOG.debug('command: %s\n%s', ' '.join(cmd), output)
except subprocess.CalledProcessError as e:
LOG.error('Could not list containers, error=%s', e)
return 1
pt = prettytable.PrettyTable(
['namespace',
'pod.name',
'container.name',
'container.id',
'state',
'QoS',
'shares',
'group',
'cpus',
], caching=False)
pt.align = 'l'
pt.align['shares'] = 'r'
# Gather data for each container
J = json.loads(output)
containers = {}
for cont in J['containers']:
containers[cont['id']] = {
'name': cont['metadata']['name'],
'pod.name': cont['labels']['io.kubernetes.pod.name'],
'cont.name': cont['labels']['io.kubernetes.container.name'],
'namespace': cont['labels']['io.kubernetes.pod.namespace'],
'state': cont['state'],
}
for cid, C in sorted(containers.items(),
key=lambda kv: (kv[1]['namespace'],
kv[1]['name'],
kv[1]['cont.name'])):
cid_short = cid[0:13]
pname = C['pod.name']
cname = C['cont.name']
namespace = C['namespace']
cstate = C['state']
# Now that we have the container ids, get more detailed resource info
cmd = ['crictl', 'inspect', '--output=json', cid]
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
LOG.debug('command: %s\n%s', ' '.join(cmd), output)
except subprocess.CalledProcessError as e:
LOG.error('Could not get container %s, error=%s', cid, e)
return 1
inspect = json.loads(output)
linux = inspect['info']['runtimeSpec']['linux']
cgroupsPath = linux['cgroupsPath']
resources = linux['resources']
cpu = resources['cpu']
shares = cpu.get('shares', '-')
cpus = cpu.get('cpus')
cpuset = set(range_to_list(csv_range=cpus))
if not cpuset:
cpuset = k8sinfra_cpuset
# Determine pod QoS
if 'besteffort' in cgroupsPath:
QoS = 'besteffort'
elif 'burstable' in cgroupsPath:
QoS = 'burstable'
else:
QoS = 'guaranteed'
# Determine cpuset group mapping
if cpus is None:
group = 'k8s-infra'
else:
if cpuset.issubset(platform_cpuset):
group = 'platform'
elif cpuset.issubset(isolated_cpuset):
group = 'isolated'
elif cpuset.issubset(guaranteed_cpuset):
group = 'guaranteed'
elif cpuset.issubset(default_cpuset):
group = 'default'
else:
group = 'unknown'
# Determine per-numa node mapping of the container cpuset
per_node = []
for node, node_cpuset in sorted(node_cpusets.items(),
key=lambda kv: kv[0]):
# calculate subset of cpus for the given numa node
n_cpuset = cpuset.intersection(node_cpuset)
if n_cpuset:
cpuranges = format_range_set(n_cpuset)
per_node.append('node {} {}'.format(node, cpuranges))
per_node_cpus = '; '.join(per_node)
pt.add_row(
[namespace,
pname,
cname,
cid_short,
cstate,
QoS,
shares,
group,
per_node_cpus or '-',
])
# Display overall cpusets summary per numa node
pt0 = prettytable.PrettyTable(
['Node',
'Total',
'Platform',
'Isolated_used',
'Isolated_free',
'Guaranteed',
'Default',
], caching=False)
pt0.align = 'r'
# Display total cpu resources per numa node
pt1 = prettytable.PrettyTable(
['Node',
'Total',
'Platform',
'Isolated_used',
'Isolated_free',
'Guaranteed',
'Default',
], caching=False)
pt1.align = 'r'
for node, node_cpuset in sorted(node_cpusets.items(), key=lambda kv: kv[0]):
# calculate subset of cpus for the given numa node
node_platform = platform_cpuset.intersection(node_cpuset)
node_default = default_cpuset.intersection(node_cpuset)
node_guaranteed = guaranteed_cpuset.intersection(node_cpuset)
node_isolated_used = isolated_used_cpuset.intersection(node_cpuset)
node_isolated_free = isolated_free_cpuset.intersection(node_cpuset)
# format cpusets as strings
node_cpuranges = format_range_set(node_cpuset) or '-'
platform_cpuranges = format_range_set(node_platform) or '-'
default_cpuranges = format_range_set(node_default) or '-'
guaranteed_cpuranges = format_range_set(node_guaranteed) or '-'
isolated_used_cpuranges = format_range_set(node_isolated_used) or '-'
isolated_free_cpuranges = format_range_set(node_isolated_free) or '-'
# calculate overall usage and free
node_used = len(node_cpuset)
platform_used = len(node_platform)
default_used = len(node_default)
guaranteed_used = len(node_guaranteed)
isolated_used = len(node_isolated_used)
isolated_free = len(node_isolated_free)
pt0.add_row(
[node,
node_cpuranges,
platform_cpuranges,
isolated_used_cpuranges,
isolated_free_cpuranges,
guaranteed_cpuranges,
default_cpuranges,
])
pt1.add_row(
[node,
node_used,
platform_used,
isolated_used,
isolated_free,
guaranteed_used,
default_used,
])
# Dump the tables out
print('\nPer-container cpusets:')
print(pt)
print('\nLogical cpusets usage per numa node:')
print(pt0)
print('\nLogical cpus usage per numa node:')
print(pt1)
return 0
def main():
"""Main program."""
# Parse command line arguments
parser = argparse.ArgumentParser(
description='Display kubernetes containers cpuset '
'usage per numa-node.')
parser.add_argument('--debug',
action='store_true',
help='display debug info')
args = parser.parse_args()
# Configure logging
if args.debug:
level = logging.DEBUG
else:
level = logging.INFO
out_hdlr = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
'%(asctime)s %(process)s %(levelname)s %(module)s: %(message)s')
out_hdlr.setFormatter(formatter)
out_hdlr.setLevel(level)
LOG.addHandler(out_hdlr)
LOG.setLevel(level)
# Limit access of this tool since some sysfs data requires root.
if os.geteuid() != 0:
LOG.error('Require sudo/root.')
sys.exit(1)
try:
ret = gather_info_and_display()
sys.exit(ret)
except KeyboardInterrupt as e:
LOG.info('caught: %r, shutting down', e)
sys.exit(0)
except IOError:
sys.exit(0)
except Exception as e:
LOG.error('exception: %r', e, exc_info=1)
sys.exit(-4)

View File

@ -0,0 +1,16 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
import testtools
class KubeCpusetsTestCase(testtools.TestCase):
# NOTE(jgauld): Suggest add basic cpuset parsing tests.
# /sys/devices/system/cpu/isolated
def test_isolated_cpusets_parse(self):
pass
# kube_cpusets.kube_cpusets.get_isolated_cpuset()

View File

@ -0,0 +1,346 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# DEPRECATED
include-ids=no
# DEPRECATED
symbols=no
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
# Disable specific warnings:
# W0703: Catching too general exception
# Disable convention warnings:
# C0103: Invalid %s name "%s" : this is anal
# C0111: Missing %s docstring
# C0330: Wrong hanging indentation before block : inconsistent with flake8
# C0325: Unnecessary parens after %r keyword : inconsistent with python 3
# Disable refactor for bad code smell:
# R0912: Too many branches
# R0914: Too many local variables
# R0915: Too many statements
disable=C0103, C0111, C0325, C0330, R0912, R0914, R0915, W0703
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input,file
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=rpm,PKCS1_PSS
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@ -0,0 +1,4 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.

View File

@ -0,0 +1,22 @@
[metadata]
license_files = LICENSE
name = kube_cpusets
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
[files]
packages =
kube_cpusets
[wheel]
universal = 1

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""
Copyright (c) 2020 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
import setuptools
setuptools.setup(
name='kube_cpusets',
version='1.0.0',
description='display kubernetes containers cpusets per numa node',
license='Apache-2.0',
packages=['kube_cpusets'],
entry_points={
'console_scripts': [
'kube-cpusets = kube_cpusets.kube_cpusets:main',
]}
)

View File

@ -0,0 +1,11 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=1.1.0,<=2.0.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
mock>=2.0.0 # BSD
stestr>=1.0.0 # Apache-2.0
testtools>=2.2.0 # MIT

View File

@ -0,0 +1,107 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
[tox]
envlist = flake8,py27,py36,pylint,cover
minversion = 2.3.2
skipsdist = True
stxdir = {toxinidir}/../../..
[testenv]
setenv = VIRTUAL_ENV={envdir}
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
OS_STDERR_CAPTURE=1
OS_STDOUT_CAPTURE=1
OS_TEST_PATH=./kube_cpusets/tests
OS_TEST_TIMEOUT=60
PYTHONDONTWRITEBYTECODE=1
PYTHONHASHSEED=0
PYTHONWARNINGS=default::DeprecationWarning
PIP_DISABLE_PIP_VERSION_CHECK=1
passenv =
XDG_CACHE_HOME
sitepackages = False
install_command = pip install \
-v -v -v \
-c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/stable/stein/upper-constraints.txt} \
{opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
-e{[tox]stxdir}/config/tsconfig/tsconfig
whitelist_externals = find
sh
[testenv:stestr]
commands =
find . -name "*.pyc" -delete
stestr run {posargs}
stestr slowest
[testenv:py27]
basepython = python2.7
commands = {[testenv:stestr]commands}
[testenv:py36]
basepython = python3.6
commands = {[testenv:stestr]commands}
[bandit]
exclude = tests
[testenv:bandit]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
bandit
commands = bandit --ini tox.ini -n 5 -r kube_cpusets
[flake8]
show-source = True
ignore =
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,release-tag-*
# H106: Don't put vim configuration in source files (off by default).
# H203: Use assertIs(Not)None to check for None (off by default).
# enable: H904 Delay string interpolations at logging calls (off by default).
enable-extensions = H106 H203 H904
max-line-length = 120
[testenv:flake8]
basepython = python3
deps = {[testenv]deps}
flake8-bugbear
usedevelop = False
#skip_install = True
commands =
flake8 {posargs} .
[testenv:pylint]
deps = {[testenv]deps}
pylint
basepython = python2.7
sitepackages = False
commands = pylint kube_cpusets --rcfile=./pylint.rc
[testenv:cover]
setenv =
PYTHON=coverage run --parallel-mode
PYTHONDONTWRITEBYTECODE=True
commands = coverage erase
find . -name "*.pyc" -delete
stestr run {posargs}
coverage combine
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report