2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2019 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 Special agent for monitoring Kubernetes clusters.
30 from __future__
import (
37 from collections
import OrderedDict
, Sequence
46 from typing
import ( # pylint: disable=unused-import
47 Any
, Dict
, Generic
, List
, Mapping
, Optional
, TypeVar
, Union
,
50 from kubernetes
import client
51 from kubernetes
.client
.rest
import ApiException
53 import cmk
.utils
.profile
54 import cmk
.utils
.password_store
57 class PathPrefixAction(argparse
.Action
):
58 def __call__(self
, parser
, namespace
, values
, option_string
=None):
61 path_prefix
= '/' + values
.strip('/')
62 setattr(namespace
, self
.dest
, path_prefix
)
66 # type: (List[str]) -> argparse.Namespace
67 p
= argparse
.ArgumentParser(description
=__doc__
)
68 p
.add_argument('--debug', action
='store_true', help='Debug mode: raise Python exceptions')
74 help='Verbose mode (for even more output use -vvv)')
75 p
.add_argument('host', metavar
='HOST', help='Kubernetes host to connect to')
76 p
.add_argument('--port', type=int, default
=443, help='Port to connect to')
77 p
.add_argument('--token', required
=True, help='Token for that user')
78 p
.add_argument('--url-prefix', help='Custom URL prefix for Kubernetes API calls')
82 action
=PathPrefixAction
,
83 help='Optional URL path prefix to prepend to Kubernetes API calls')
84 p
.add_argument('--no-cert-check', action
='store_true', help='Disable certificate verification')
88 help='Profile the performance of the agent and write the output to a file')
90 arguments
= p
.parse_args(args
)
94 def setup_logging(verbosity
):
97 fmt
= '%(levelname)s: %(name)s: %(filename)s: %(lineno)s: %(message)s'
100 fmt
= '%(levelname)s: %(filename)s: %(lineno)s: %(message)s'
103 fmt
= '%(levelname)s: %(funcName)s: %(message)s'
106 fmt
= '%(levelname)s: %(message)s'
107 lvl
= logging
.WARNING
108 logging
.basicConfig(level
=lvl
, format
=fmt
)
111 def parse_frac_prefix(value
):
112 # type: (str) -> float
113 if value
.endswith('m'):
114 return 0.001 * float(value
[:-1])
118 def parse_memory(value
):
119 # type: (str) -> float
120 if value
.endswith('Ki'):
121 return 1024**1 * float(value
[:-2])
122 if value
.endswith('Mi'):
123 return 1024**2 * float(value
[:-2])
124 if value
.endswith('Gi'):
125 return 1024**3 * float(value
[:-2])
126 if value
.endswith('Ti'):
127 return 1024**4 * float(value
[:-2])
128 if value
.endswith('Pi'):
129 return 1024**5 * float(value
[:-2])
130 if value
.endswith('Ei'):
131 return 1024**6 * float(value
[:-2])
133 if value
.endswith('K'):
134 return 1e3
* float(value
[:-1])
135 if value
.endswith('M'):
136 return 1e6
* float(value
[:-1])
137 if value
.endswith('G'):
138 return 1e9
* float(value
[:-1])
139 if value
.endswith('T'):
140 return 1e12
* float(value
[:-1])
141 if value
.endswith('P'):
142 return 1e15
* float(value
[:-1])
143 if value
.endswith('E'):
144 return 1e18
* float(value
[:-1])
149 def left_join_dicts(initial
, new
, operation
):
151 for key
, value
in initial
.iteritems():
152 if isinstance(value
, dict):
153 d
[key
] = left_join_dicts(value
, new
.get(key
, {}), operation
)
156 d
[key
] = operation(value
, new
[key
])
162 class Metadata(object):
163 def __init__(self
, metadata
):
164 # type: (Optional[client.V1ObjectMeta]) -> None
166 self
.name
= metadata
.name
167 self
.namespace
= metadata
.namespace
168 self
.creation_timestamp
= (time
.mktime(metadata
.creation_timestamp
.timetuple())
169 if metadata
.creation_timestamp
else None)
172 self
.namespace
= None
173 self
.creation_timestamp
= None
176 class Node(Metadata
):
177 def __init__(self
, node
, stats
):
178 # type: (client.V1Node, str) -> None
179 super(Node
, self
).__init
__(node
.metadata
)
180 self
._status
= node
.status
181 # kubelet replies statistics for the last 2 minutes with 10s
182 # intervals. We only need the latest state.
183 self
.stats
= eval(stats
)['stats'][-1]
186 def conditions(self
):
187 # type: () -> Optional[Dict[str, str]]
190 conditions
= self
._status
.conditions
193 return {c
.type: c
.status
for c
in conditions
}
196 def zero_resources():
212 # type: () -> Dict[str, Dict[str, float]]
213 view
= self
.zero_resources()
216 capacity
, allocatable
= self
._status
.capacity
, self
._status
.allocatable
218 view
['capacity']['cpu'] += parse_frac_prefix(capacity
.get('cpu', '0.0'))
219 view
['capacity']['memory'] += parse_memory(capacity
.get('memory', '0.0'))
220 view
['capacity']['pods'] += int(capacity
.get('pods', '0'))
222 view
['allocatable']['cpu'] += parse_frac_prefix(allocatable
.get('cpu', '0.0'))
223 view
['allocatable']['memory'] += parse_memory(allocatable
.get('memory', '0.0'))
224 view
['allocatable']['pods'] += int(allocatable
.get('pods', '0'))
228 class ComponentStatus(Metadata
):
229 def __init__(self
, status
):
230 # type: (client.V1ComponentStatus) -> None
231 super(ComponentStatus
, self
).__init
__(status
.metadata
)
232 self
._conditions
= status
.conditions
235 def conditions(self
):
236 # type: () -> List[Dict[str, str]]
237 if not self
._conditions
:
239 return [{'type': c
.type, 'status': c
.status
} for c
in self
._conditions
]
243 def __init__(self
, pod
):
244 # type: (client.V1Pod) -> None
245 super(Pod
, self
).__init
__(pod
.metadata
)
247 self
.node
= spec
.node_name
if spec
else None
248 self
.containers
= spec
.containers
if spec
else []
251 def zero_resources():
265 view
= self
.zero_resources()
266 for container
in self
.containers
:
267 resources
= container
.resources
270 limits
= resources
.limits
272 view
['limits']['cpu'] += parse_frac_prefix(limits
.get('cpu', 'inf'))
273 view
['limits']['memory'] += parse_memory(limits
.get('memory', 'inf'))
275 view
['limits']['cpu'] += float('inf')
276 view
['limits']['memory'] += float('inf')
277 requests
= resources
.requests
279 view
['requests']['cpu'] += parse_frac_prefix(requests
.get('cpu', '0.0'))
280 view
['requests']['memory'] += parse_memory(requests
.get('memory', '0.0'))
284 class Namespace(Metadata
):
285 # TODO: namespaces may have resource quotas and limits
286 # https://kubernetes.io/docs/tasks/administer-cluster/namespaces/
287 def __init__(self
, namespace
):
288 # type: (client.V1Namespace) -> None
289 super(Namespace
, self
).__init
__(namespace
.metadata
)
290 self
._status
= namespace
.status
294 # type: () -> Optional[str]
296 return self
._status
.phase
300 class PersistentVolume(Metadata
):
301 def __init__(self
, pv
):
302 # type: (client.V1PersistentVolume) -> None
303 super(PersistentVolume
, self
).__init
__(pv
.metadata
)
304 self
._status
= pv
.status
308 def access_modes(self
):
309 # type: () -> Optional[List[str]]
311 return self
._spec
.access_modes
316 # type: () -> Optional[float]
317 if not self
._spec
or not self
._spec
.capacity
:
319 storage
= self
._spec
.capacity
.get('storage')
321 return parse_memory(storage
)
326 # type: () -> Optional[str]
328 return self
._status
.phase
332 class PersistentVolumeClaim(Metadata
):
333 def __init__(self
, pvc
):
334 # type: (client.V1PersistentVolumeClaim) -> None
335 super(PersistentVolumeClaim
, self
).__init
__(pvc
.metadata
)
336 self
._status
= pvc
.status
337 self
._spec
= pvc
.spec
340 def conditions(self
):
341 # type: () -> Optional[client.V1PersistentVolumeClaimCondition]
342 # TODO: don't return client specific object
344 return self
._status
.conditions
349 # type: () -> Optional[str]
351 return self
._status
.phase
355 def volume_name(self
):
356 # type: () -> Optional[str]
358 return self
._spec
.volume_name
362 class StorageClass(Metadata
):
363 def __init__(self
, storage_class
):
364 # type: (client.V1StorageClass) -> None
365 super(StorageClass
, self
).__init
__(storage_class
.metadata
)
366 self
.provisioner
= storage_class
.provisioner
367 self
.reclaim_policy
= storage_class
.reclaim_policy
370 class Role(Metadata
):
371 def __init__(self
, role
):
372 # type: (Union[client.V1Role, client.V1ClusterRole]) -> None
373 super(Role
, self
).__init
__(role
.metadata
)
376 ListElem
= TypeVar('ListElem')
379 class ListLike(Generic
[ListElem
], Sequence
):
380 def __init__(self
, elements
):
381 # type: (List[ListElem]) -> None
382 super(ListLike
, self
).__init
__()
383 self
.elements
= elements
385 def __getitem__(self
, index
):
386 return self
.elements
[index
]
390 return len(self
.elements
)
393 class NodeList(ListLike
[Node
]):
394 def list_nodes(self
):
395 # type: () -> Dict[str, List[str]]
396 return {'nodes': [node
.name
for node
in self
if node
.name
]}
398 def conditions(self
):
399 # type: () -> Dict[str, Dict[str, str]]
400 return {node
.name
: node
.conditions
for node
in self
if node
.name
and node
.conditions
}
403 # type: () -> Dict[str, Dict[str, Dict[str, Optional[float]]]]
404 return {node
.name
: node
.resources
for node
in self
if node
.name
}
407 return {node
.name
: node
.stats
for node
in self
if node
.name
}
409 def cluster_resources(self
):
410 merge
= functools
.partial(left_join_dicts
, operation
=operator
.add
)
411 return reduce(merge
, self
.resources().itervalues())
413 def cluster_stats(self
):
414 merge
= functools
.partial(left_join_dicts
, operation
=operator
.add
)
415 return reduce(merge
, self
.stats().itervalues())
418 class ComponentStatusList(ListLike
[ComponentStatus
]):
419 def list_statuses(self
):
420 # type: () -> Dict[str, List[Dict[str, str]]]
421 return {status
.name
: status
.conditions
for status
in self
if status
.name
}
424 class PodList(ListLike
[Pod
]):
425 def pods_per_node(self
):
426 # type: () -> Dict[str, Dict[str, Dict[str, int]]]
427 pods_sorted
= sorted(self
, key
=lambda pod
: pod
.node
)
428 by_node
= itertools
.groupby(pods_sorted
, lambda pod
: pod
.node
)
429 return {node
: {'requests': {'pods': len(list(pods
))}} for node
, pods
in by_node
}
431 def pods_in_cluster(self
):
432 return {'requests': {'pods': len(self
)}}
434 def resources_per_node(self
):
435 # type: () -> Dict[str, Dict[str, Dict[str, float]]]
437 Returns the limits and requests of all containers grouped by node. If at least
438 one container does not specify a limit, infinity is returned as the container
439 may consume any amount of resources.
442 pods_sorted
= sorted(self
, key
=lambda pod
: pod
.node
)
443 by_node
= itertools
.groupby(pods_sorted
, lambda pod
: pod
.node
)
444 merge
= functools
.partial(left_join_dicts
, operation
=operator
.add
)
446 node
: reduce(merge
, [p
.resources
for p
in pods
447 ], Pod
.zero_resources()) for node
, pods
in by_node
450 def cluster_resources(self
):
451 merge
= functools
.partial(left_join_dicts
, operation
=operator
.add
)
452 return reduce(merge
, [p
.resources
for p
in self
], Pod
.zero_resources())
455 class NamespaceList(ListLike
[Namespace
]):
456 def list_namespaces(self
):
457 # type: () -> Dict[str, Dict[str, Dict[str, Optional[str]]]]
461 'phase': namespace
.phase
,
463 } for namespace
in self
if namespace
.name
467 class PersistentVolumeList(ListLike
[PersistentVolume
]):
468 def list_volumes(self
):
469 # type: () -> Dict[str, Dict[str, Union[Optional[List[str]], Optional[float], Dict[str, Optional[str]]]]]
470 # TODO: Output details of the different types of volumes
473 'access': pv
.access_modes
,
474 'capacity': pv
.capacity
,
478 } for pv
in self
if pv
.name
482 class PersistentVolumeClaimList(ListLike
[PersistentVolumeClaim
]):
483 def list_volume_claims(self
):
484 # type: () -> Dict[str, Dict[str, Any]]
488 'namespace': pvc
.namespace
,
489 'condition': pvc
.conditions
,
491 'volume': pvc
.volume_name
,
492 } for pvc
in self
if pvc
.name
496 class StorageClassList(ListLike
[StorageClass
]):
497 def list_storage_classes(self
):
498 # type: () -> Dict[Any, Dict[str, Any]]
499 # TODO: should be Dict[str, Dict[str, Optional[str]]]
501 storage_class
.name
: {
502 'provisioner': storage_class
.provisioner
,
503 'reclaim_policy': storage_class
.reclaim_policy
504 } for storage_class
in self
if storage_class
.name
508 class RoleList(ListLike
[Role
]):
509 def list_roles(self
):
512 'namespace': role
.namespace
,
513 'creation_timestamp': role
.creation_timestamp
514 } for role
in self
if role
.name
]
517 class Metric(object):
518 def __init__(self
, metric
):
519 self
.from_object
= metric
['describedObject']
520 self
.metrics
= {metric
['metricName']: metric
.get('value')}
522 def __add__(self
, other
):
523 assert self
.from_object
== other
.from_object
524 self
.metrics
.update(other
.metrics
)
528 return str(self
.__dict
__)
531 return str(self
.__dict
__)
534 class MetricList(ListLike
[Metric
]):
535 def __add__(self
, other
):
536 return MetricList([a
+ b
for a
, b
in zip(self
, other
)])
538 def list_metrics(self
):
539 return [item
.__dict
__ for item
in self
]
544 A group of elements where an element is e.g. a piggyback host.
549 super(Group
, self
).__init
__()
550 self
.elements
= OrderedDict() # type: OrderedDict[str, Element]
552 def get(self
, element_name
):
553 # type: (str) -> Element
554 if element_name
not in self
.elements
:
555 self
.elements
[element_name
] = Element()
556 return self
.elements
[element_name
]
558 def join(self
, section_name
, pairs
):
559 # type: (str, Mapping[str, Dict[str, Any]]) -> Group
560 for element_name
, data
in pairs
.iteritems():
561 section
= self
.get(element_name
).get(section_name
)
566 # type: () -> List[str]
568 for name
, element
in self
.elements
.iteritems():
569 data
.append('<<<<%s>>>>' % name
)
570 data
.extend(element
.output())
571 data
.append('<<<<>>>>')
575 class Element(object):
577 An element that bundles a collection of sections.
582 super(Element
, self
).__init
__()
583 self
.sections
= OrderedDict() # type: OrderedDict[str, Section]
585 def get(self
, section_name
):
586 # type: (str) -> Section
587 if section_name
not in self
.sections
:
588 self
.sections
[section_name
] = Section()
589 return self
.sections
[section_name
]
592 # type: () -> List[str]
594 for name
, section
in self
.sections
.iteritems():
595 data
.append('<<<%s:sep(0)>>>' % name
)
596 data
.append(section
.output())
600 class Section(object):
607 super(Section
, self
).__init
__()
608 self
.content
= OrderedDict() # type: OrderedDict[str, Dict[str, Any]]
610 def insert(self
, data
):
611 # type: (Dict[str, Any]) -> None
612 for key
, value
in data
.iteritems():
613 if key
not in self
.content
:
614 self
.content
[key
] = value
616 if isinstance(value
, dict):
617 self
.content
[key
].update(value
)
619 raise ValueError('Key %s is already present and cannot be merged' % key
)
623 return json
.dumps(self
.content
)
626 class ApiData(object):
628 Contains the collected API data.
631 def __init__(self
, api_client
):
632 # type: (client.ApiClient) -> None
633 super(ApiData
, self
).__init
__()
634 logging
.info('Collecting API data')
636 logging
.debug('Constructing API client wrappers')
637 core_api
= client
.CoreV1Api(api_client
)
638 storage_api
= client
.StorageV1Api(api_client
)
639 rbac_authorization_api
= client
.RbacAuthorizationV1Api(api_client
)
641 self
.custom_api
= client
.CustomObjectsApi(api_client
)
643 logging
.debug('Retrieving data')
644 storage_classes
= storage_api
.list_storage_class()
645 namespaces
= core_api
.list_namespace()
646 roles
= rbac_authorization_api
.list_role_for_all_namespaces()
647 cluster_roles
= rbac_authorization_api
.list_cluster_role()
648 component_statuses
= core_api
.list_component_status()
649 nodes
= core_api
.list_node()
650 # Try to make it a post, when client api support sending post data
651 # include {"num_stats": 1} to get the latest only and use less bandwidth
653 core_api
.connect_get_node_proxy_with_path(node
.metadata
.name
, "stats")
654 for node
in nodes
.items
656 pvs
= core_api
.list_persistent_volume()
657 pvcs
= core_api
.list_persistent_volume_claim_for_all_namespaces()
658 pods
= core_api
.list_pod_for_all_namespaces()
660 logging
.debug('Assigning collected data')
661 self
.storage_classes
= StorageClassList(map(StorageClass
, storage_classes
.items
))
662 self
.namespaces
= NamespaceList(map(Namespace
, namespaces
.items
))
663 self
.roles
= RoleList(map(Role
, roles
.items
))
664 self
.cluster_roles
= RoleList(map(Role
, cluster_roles
.items
))
665 self
.component_statuses
= ComponentStatusList(
666 map(ComponentStatus
, component_statuses
.items
))
667 self
.nodes
= NodeList(map(Node
, nodes
.items
, nodes_stats
))
668 self
.persistent_volumes
= PersistentVolumeList(map(PersistentVolume
, pvs
.items
))
669 self
.persistent_volume_claims
= PersistentVolumeClaimList(
670 map(PersistentVolumeClaim
, pvcs
.items
))
671 self
.pods
= PodList(map(Pod
, pods
.items
))
673 pods_custom_metrics
= {
674 "memory": ['memory_rss', 'memory_swap', 'memory_usage_bytes', 'memory_max_usage_bytes'],
675 "fs": ['fs_inodes', 'fs_reads', 'fs_writes', 'fs_limit_bytes', 'fs_usage_bytes'],
676 "cpu": ['cpu_system', 'cpu_user', 'cpu_usage']
679 self
.pods_Metrics
= dict() # type: Dict[str, Dict[str, List]]
680 for metric_group
, metrics
in pods_custom_metrics
.items():
681 self
.pods_Metrics
[metric_group
] = self
.get_namespaced_group_metric(metrics
)
683 def get_namespaced_group_metric(self
, metrics
):
684 # type: (List[str]) -> Dict[str, List]
685 queries
= [self
.get_namespaced_custom_pod_metric(metric
) for metric
in metrics
]
687 grouped_metrics
= {} # type: Dict[str, List]
688 for response
in queries
:
689 for namespace
in response
:
690 grouped_metrics
.setdefault(namespace
, []).append(response
[namespace
])
692 for namespace
in grouped_metrics
:
693 grouped_metrics
[namespace
] = reduce(operator
.add
,
694 grouped_metrics
[namespace
]).list_metrics()
696 return grouped_metrics
698 def get_namespaced_custom_pod_metric(self
, metric
):
699 # type: (str) -> Dict
701 logging
.debug('Query Custom Metrics Endpoint: %s', metric
)
703 for namespace
in self
.namespaces
:
707 self
.custom_api
.get_namespaced_custom_object(
708 'custom.metrics.k8s.io',
714 custom_metric
[namespace
.name
] = MetricList(data
)
715 except ApiException
as err
:
716 if err
.status
== 404:
717 logging
.info('Data unavailable. No pods in namespace %s', namespace
.name
)
718 elif err
.status
== 500:
719 logging
.info('Data unavailable. %s', err
)
725 def cluster_sections(self
):
727 logging
.info('Output cluster sections')
729 e
.get('k8s_nodes').insert(self
.nodes
.list_nodes())
730 e
.get('k8s_namespaces').insert(self
.namespaces
.list_namespaces())
731 e
.get('k8s_persistent_volumes').insert(self
.persistent_volumes
.list_volumes())
732 e
.get('k8s_component_statuses').insert(self
.component_statuses
.list_statuses())
733 e
.get('k8s_persistent_volume_claims').insert(
734 self
.persistent_volume_claims
.list_volume_claims())
735 e
.get('k8s_storage_classes').insert(self
.storage_classes
.list_storage_classes())
736 e
.get('k8s_roles').insert({'roles': self
.roles
.list_roles()})
737 e
.get('k8s_roles').insert({'cluster_roles': self
.cluster_roles
.list_roles()})
738 e
.get('k8s_resources').insert(self
.nodes
.cluster_resources())
739 e
.get('k8s_resources').insert(self
.pods
.cluster_resources())
740 e
.get('k8s_resources').insert(self
.pods
.pods_in_cluster())
741 e
.get('k8s_stats').insert(self
.nodes
.cluster_stats())
742 return '\n'.join(e
.output())
744 def node_sections(self
):
746 logging
.info('Output node sections')
748 g
.join('k8s_resources', self
.nodes
.resources())
749 g
.join('k8s_resources', self
.pods
.resources_per_node())
750 g
.join('k8s_resources', self
.pods
.pods_per_node())
751 g
.join('k8s_stats', self
.nodes
.stats())
752 g
.join('k8s_conditions', self
.nodes
.conditions())
753 return '\n'.join(g
.output())
755 def custom_metrics_section(self
):
757 logging
.info('Output pods custom metrics')
759 for c_metric
in self
.pods_Metrics
:
760 e
.get('k8s_pods_%s' % c_metric
).insert(self
.pods_Metrics
[c_metric
])
761 return '\n'.join(e
.output())
764 def get_api_client(arguments
):
765 # type: (argparse.Namespace) -> client.ApiClient
766 logging
.info('Constructing API client')
768 config
= client
.Configuration()
769 if arguments
.url_prefix
:
770 config
.host
= '%s:%s%s' % (arguments
.url_prefix
, arguments
.port
, arguments
.path_prefix
)
772 config
.host
= 'https://%s:%s%s' % (arguments
.host
, arguments
.port
, arguments
.path_prefix
)
774 config
.api_key_prefix
['authorization'] = 'Bearer'
775 config
.api_key
['authorization'] = arguments
.token
777 if arguments
.no_cert_check
:
778 logging
.warn('Disabling SSL certificate verification')
779 config
.verify_ssl
= False
781 config
.ssl_ca_cert
= os
.environ
.get('REQUESTS_CA_BUNDLE')
783 return client
.ApiClient(config
)
787 # type: (Optional[List[str]]) -> int
789 cmk
.utils
.password_store
.replace_passwords()
791 arguments
= parse(args
)
794 setup_logging(arguments
.verbose
)
795 logging
.debug('parsed arguments: %s\n', arguments
)
797 with cmk
.utils
.profile
.Profile(
798 enabled
=bool(arguments
.profile
), profile_file
=arguments
.profile
):
799 api_client
= get_api_client(arguments
)
800 api_data
= ApiData(api_client
)
801 print(api_data
.cluster_sections())
802 print(api_data
.node_sections())
803 print(api_data
.custom_metrics_section())
804 except Exception as e
:
807 print("%s" % e
, file=sys
.stderr
)