hpc-nodes 1.8.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ include nodes.spec
2
+ include nodes.conf
3
+ include nodes.conf.sample
4
+ include MANIFEST.in
5
+ include nodes.1
6
+ include nodes.conf.5
7
+ include README
8
+ include VERSION.file
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: hpc-nodes
3
+ Version: 1.8.3
4
+ Summary: Handle hierarchical groups of nodes
5
+ Author-email: Kent Engström <kent@nsc.liu.se>
6
+ License-Expression: GPL-2.0
7
+ Project-URL: homepage, http://www.nsc.liu.se/~kent/nodes/
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Topic :: System :: Clustering
12
+ Classifier: Topic :: System :: Systems Administration
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: python-hostlist
hpc_nodes-1.8.3/README ADDED
@@ -0,0 +1 @@
1
+ Handle hierarchical groups of nodes in high performance computing cluster, such as enclosures and racks.
@@ -0,0 +1 @@
1
+ 1.8.3
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: hpc-nodes
3
+ Version: 1.8.3
4
+ Summary: Handle hierarchical groups of nodes
5
+ Author-email: Kent Engström <kent@nsc.liu.se>
6
+ License-Expression: GPL-2.0
7
+ Project-URL: homepage, http://www.nsc.liu.se/~kent/nodes/
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Topic :: System :: Clustering
12
+ Classifier: Topic :: System :: Systems Administration
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: python-hostlist
@@ -0,0 +1,18 @@
1
+ MANIFEST.in
2
+ README
3
+ VERSION.file
4
+ nodes
5
+ nodes.1
6
+ nodes.conf
7
+ nodes.conf.5
8
+ nodes.conf.sample
9
+ nodes.py
10
+ nodes.spec
11
+ pyproject.toml
12
+ setup.py
13
+ hpc_nodes.egg-info/PKG-INFO
14
+ hpc_nodes.egg-info/SOURCES.txt
15
+ hpc_nodes.egg-info/dependency_links.txt
16
+ hpc_nodes.egg-info/requires.txt
17
+ hpc_nodes.egg-info/top_level.txt
18
+ test/test_nodes.py
@@ -0,0 +1 @@
1
+ python-hostlist
@@ -0,0 +1 @@
1
+ nodes
hpc_nodes-1.8.3/nodes ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/python3
2
+ # Version 1.8.3
3
+ import sys
4
+ import optparse
5
+ from nodes import Hierarchy, NodesException, numerically_sorted_entities, collect_name_hostlist
6
+
7
+ # MAIN
8
+
9
+ op = optparse.OptionParser(usage="usage: %prog [OPTION]... NODESPEC")
10
+ op.add_option("--to-nodes",
11
+ action="store_true",
12
+ help="convert NODESPEC to nodes (default)")
13
+ op.add_option("-u", "--up",
14
+ action="store",
15
+ metavar="LEVEL",
16
+ help="list groups where any node in NODESPEC is a member")
17
+ op.add_option("-f", "--fill",
18
+ action="store",
19
+ metavar="LEVEL",
20
+ help="as --up, but then convert to nodes")
21
+ op.add_option("-r", "--representative",
22
+ action="store",
23
+ metavar="LEVEL",
24
+ help="as --up, but then convert to a representative node per group")
25
+ op.add_option("-g", "--gather",
26
+ action="store",
27
+ metavar="LEVEL",
28
+ help="list groups whose members are all in NODESPEC plus the individual nodes that are left over")
29
+ op.add_option("-i", "--index",
30
+ action="store",
31
+ metavar="LEVEL",
32
+ help="list the nodes together with their index in their group at the specified LEVEL (--expand is implied)")
33
+ op.add_option("-I", "--index-separator",
34
+ action="store", type="string", default=": ",
35
+ metavar="SEPARATOR",
36
+ help="separator to use between node and index in --index (default is ': ')")
37
+ op.add_option("--convert-to-clustershell",
38
+ action="store_true",
39
+ help="convert to ClusterShell YAML")
40
+ op.add_option("-m", "--missing",
41
+ action="store",
42
+ metavar="GROUP",
43
+ help="when doing --up, pretend that nodes not in any group belong to GROUP")
44
+ op.add_option("-e", "--expand",
45
+ action="store_true",
46
+ help="expand to list of names instead of collecting a hostlist")
47
+ op.add_option("--separator",
48
+ action="store", type="string", default="\n",
49
+ help="separator to use between nodes when outputting an expanded list (default is newline)")
50
+ op.add_option("--prepend",
51
+ action="store", type="string", default="",
52
+ help="string to prepend to each node when outputting an expanded list")
53
+ op.add_option("--append",
54
+ action="store", type="string", default="",
55
+ help="string to append to each node when outputting an expanded list")
56
+ op.add_option("-n", "--count",
57
+ action="store_true",
58
+ help="output the number of names instead of the hostlist or expanded list")
59
+ op.add_option("-s", "--slurm",
60
+ action="store_true",
61
+ help="create dynamic groups for SLURM jobs and users")
62
+ op.add_option("--config-file",
63
+ action="store",
64
+ default="/etc/nodes.conf",
65
+ metavar="FILE",
66
+ help="use FILE instead of /etc/nodes.conf")
67
+
68
+ (opts, args) = op.parse_args()
69
+
70
+ h = Hierarchy()
71
+ try:
72
+ h.parse_file(opts.config_file)
73
+ except NodesException as e:
74
+ print()
75
+ sys.stderr.write("Config file error: %s\n" % e.msg)
76
+ sys.exit(1)
77
+
78
+ if opts.slurm:
79
+ try:
80
+ h.parse_slurm()
81
+ except NodesException as e:
82
+ print()
83
+ sys.stderr.write("SLURM error: %s\n" % e.msg)
84
+ sys.exit(1)
85
+
86
+
87
+ if not opts.convert_to_clustershell:
88
+ if len(args) != 1:
89
+ sys.exit("Error: You need to provide a single node specifier!\n")
90
+ sys.exit(1)
91
+ else:
92
+ arg = args[0]
93
+
94
+ modes_chosen = int(opts.to_nodes is not None) + \
95
+ int(opts.up is not None) + \
96
+ int(opts.fill is not None) + \
97
+ int(opts.representative is not None) + \
98
+ int(opts.gather is not None) + \
99
+ int(opts.index is not None) + \
100
+ int(opts.convert_to_clustershell is not None)
101
+
102
+ if modes_chosen > 1:
103
+ sys.exit("Error: You cannot choose more than one mode at a time!\n")
104
+ sys.exit(1)
105
+ elif modes_chosen == 0 or opts.to_nodes:
106
+ func = lambda a: h.to_nodes(a)
107
+ else:
108
+ if opts.up:
109
+ func = lambda arg: h.up(opts.up, arg, missing_group = opts.missing)
110
+ elif opts.fill:
111
+ func = lambda arg: h.fill(opts.fill, arg)
112
+ elif opts.representative:
113
+ func = lambda arg: h.representative(opts.representative, arg)
114
+ elif opts.gather:
115
+ func = lambda arg: h.gather(opts.gather, arg)
116
+ elif opts.index:
117
+ func = lambda arg: h.index(opts.index, arg, separator = opts.index_separator)
118
+ elif opts.convert_to_clustershell:
119
+ h.convert_to_clustershell()
120
+ sys.exit(0)
121
+
122
+ try:
123
+ res = func(arg)
124
+ if opts.count:
125
+ print(len(res))
126
+ elif opts.expand or opts.index:
127
+ print(opts.separator.join([opts.prepend + x.name + opts.append for x in numerically_sorted_entities(res)]))
128
+ else:
129
+ print(collect_name_hostlist(res))
130
+
131
+ except NodesException as e:
132
+ print()
133
+ sys.stderr.write("Error: %s\n" % e.msg)
134
+ sys.exit(1)
@@ -0,0 +1,201 @@
1
+ .TH nodes 1 "Version 1.8.3"
2
+ .SH NAME
3
+ nodes \- handle hierarchical groups of nodes
4
+ .SH SYNOPSIS
5
+ .B nodes
6
+ .RI [ OPTION "]... " NODESPEC
7
+ .SH DESCRIPTION
8
+ Expand
9
+ .I NODESPEC
10
+ to a set of nodes and then, if specified by the options,
11
+ collect them into groups.
12
+
13
+ A nodespec follows the same syntax as a LLNL hostlist, but may contain
14
+ names of groups that refer to several nodes. For example, if nodes
15
+ n[1-4] make up enclosure e1, the group name e1 may refer to all
16
+ four nodes n[1-4]. The group hierarchy is defined in the
17
+ .B /etc/nodes.conf
18
+ configuration file.
19
+
20
+ A group is defined to be at a specific level, which has a name like
21
+ "enclosure", "rack" etc. In the options below where a level name is required,
22
+ any unambiguous prefix of the level name may be used.
23
+
24
+ .SH OPTIONS
25
+ .TP
26
+ .B --to-nodes
27
+ Expand
28
+ .I NODESPEC
29
+ to a set of nodes and collect that into a hostlist that will only contain
30
+ node names, not group names. This is the default operation.
31
+ .TP
32
+ .BI "-u " LEVEL ", --up=" LEVEL
33
+ Expand
34
+ .I NODESPEC
35
+ to a set of nodes and then collect the nodes into groups at the
36
+ specified
37
+ .I LEVEL
38
+ and return the resulting nodespec. A group is included in the result
39
+ if any of its members is present in the initial nodespec.
40
+ .TP
41
+ .BI "-f " LEVEL ", --fill=" LEVEL
42
+ Expand
43
+ .I NODESPEC
44
+ to a set of nodes and then collect the nodes into groups at the
45
+ specified
46
+ .I LEVEL,
47
+ just like for
48
+ .B -u/--up
49
+ above. Then expand the resulting nodespec again to a hostlist
50
+ that will only contain node names.
51
+ .TP
52
+ .BI "-r " LEVEL ", --representative=" LEVEL
53
+ Expand
54
+ .I NODESPEC
55
+ to a set of nodes and then collect the nodes into groups at the
56
+ specified
57
+ .I LEVEL,
58
+ just like for
59
+ .B -u/--up
60
+ above. Then expand the resulting nodespec again to a hostlist that
61
+ contains one representative node (the first in lexicographic
62
+ order) from each group.
63
+ .TP
64
+ .BI "-g " LEVEL ", --gather=" LEVEL
65
+ Expand
66
+ .I NODESPEC
67
+ to a set of nodes and then collect the nodes into groups at the
68
+ specified
69
+ .I LEVEL
70
+ and individual nodes, so that the resulting nodespec will
71
+ refer to exactly the same set of nodes as the initial one.
72
+ Groups are only present in the resulting nodespec when all of
73
+ their members are present in the initial nodespec.
74
+ .TP
75
+ .BI "-i " LEVEL ", --index=" LEVEL
76
+ Expand
77
+ .I NODESPEC
78
+ to a set of nodes and then for each node add ": " and the index
79
+ of the node in its group at
80
+ .IR LEVEL .
81
+ The index is 0 for the first node in the group (in lexicographic order),
82
+ 1 for the next one, etc. This option implies
83
+ .BR --expand .
84
+ .TP
85
+ .BI "-I " SEPARATOR ", --index-separator=" SEPARATOR
86
+ Use
87
+ .I SEPARATOR
88
+ as the separator between node and index in the
89
+ .B index
90
+ list. The default is ": ".
91
+ .TP
92
+ .BI "-m " GROUP ", --missing=" GROUP
93
+ When using
94
+ .BR "-u/--up" ,
95
+ pretend that nodes that do not belong to any group at the given LEVEL
96
+ do belong to GROUP. If this option is not used, an error will be
97
+ signaled in that situation.
98
+ .TP
99
+ .B -e, --expand
100
+ Instead of collecting the names (nodes and/or groups) to a hostlist,
101
+ output them as an expanded list of names.
102
+ .TP
103
+ .BI "--separator=" SEPARATOR
104
+ Use
105
+ .I SEPARATOR
106
+ as the separator between the nodes in the expanded list.
107
+ The default is a newline.
108
+ .TP
109
+ .BI "--prepend=" PREPEND
110
+ Output
111
+ .I PREPEND
112
+ before each node in the expanded list.
113
+ The default is to prepend nothing.
114
+ .TP
115
+ .BI "--append=" APPEND
116
+ Output
117
+ .I APPEND
118
+ after each node in the expanded list.
119
+ The default is to append nothing.
120
+ .TP
121
+ .B -n, --count
122
+ Output the number of names (nodes and/or groups) in the result instead of the list itself.
123
+ .TP
124
+ .B -s, --slurm
125
+ Invoke the
126
+ .B /usr/bin/squeue
127
+ command of the SLURM resource manager and use the output to build
128
+ dynamic groups for users and jobs. The user groups use level
129
+ .B user
130
+ and have group names equal to the user names . The job groups use level
131
+ .B job
132
+ and have the job numbers prefixed by "job" as group names.
133
+ .TP
134
+ .BI --config-file= FILE
135
+ Read configuration from
136
+ .I FILE
137
+ instead of
138
+ .B /etc/nodes.conf.
139
+ .TP
140
+ .B --convert-to-clustershell
141
+ Output configuration suitable for a YAML file in
142
+ .B /etc/clustershell/groups.d
143
+ when using ClusterShell 1.7 or later.
144
+ .TP
145
+ .B -h, --help
146
+ Show brief usage information and exit.
147
+ .SH EXAMPLES
148
+ The examples below assume a configuration where each enclosure
149
+ contains four nodes, such as the the example in the
150
+ .I nodes.conf(5)
151
+ man page.
152
+ .TP
153
+ What nodes belong to enclosures e2 and e3?
154
+ .B % nodes e[2-3]
155
+ .br
156
+ n[5-12]
157
+ .TP
158
+ What enclosures contain the nodes n[8-10]?
159
+ .B % nodes --up enclosure n[8-10]
160
+ .br
161
+ e[2-3]
162
+ .TP
163
+ What nodes are in the same enclosures as n[8-10]?
164
+ .B % nodes --fill enclosure n[8-10]
165
+ .br
166
+ n[5-12]
167
+ .TP
168
+ This is to much to type! Can I abbreviate?
169
+ .B % nodes -fe n[8-10]
170
+ .br
171
+ n[5-12]
172
+ .TP
173
+ Can I have a hostlist with one representative node from each enclosure that has nodes from the set n[8-10]?
174
+ .B % nodes --representative enclosure n[8-10]
175
+ .br
176
+ n[5,9]
177
+ .TP
178
+ Express n[1-10] as full enclosures and remaining nodes:
179
+ .B % nodes --gather enclosure n[1-10]
180
+ .br
181
+ e[1-2],n[9-10]
182
+ .SH AUTHOR
183
+ Written by Kent Engström <kent@nsc.liu.se>.
184
+
185
+ The program is published at http://www.nsc.liu.se/~kent/nodes/
186
+ .SH SEE ALSO
187
+ .I nodes.conf(5), hostlist(1)
188
+
189
+ The hostlist expression syntax is used by several programs developed at
190
+ .B LLNL
191
+ (https://computing.llnl.gov/linux/), for example
192
+ .B SLURM
193
+ (https://computing.llnl.gov/linux/slurm/) and
194
+ .B Pdsh
195
+ (https://computing.llnl.gov/linux/pdsh.html).
196
+
197
+ See the
198
+ .B HOSTLIST EXPRESSIONS
199
+ section of the
200
+ .BR pdsh (1)
201
+ manual page for a short introduction to the hostlist syntax.
@@ -0,0 +1,28 @@
1
+ # Sample config file
2
+
3
+ # We have to define nodes first, so that misspelled groups won't be
4
+ # treaded as node names:
5
+
6
+ nodes: n[1-64]
7
+
8
+ # Now, we define groups
9
+
10
+ enclosure e1: n[1-4]
11
+ enclosure e2: n[5-8]
12
+ enclosure e3: n[9-12]
13
+ enclosure e4: n[13-16]
14
+ enclosure e5: n[17-20]
15
+ enclosure e6: n[21-24]
16
+ enclosure e7: n[25-28]
17
+ enclosure e8: n[29-32]
18
+ enclosure e9: n[33-36]
19
+ enclosure e10: n[37-40]
20
+ enclosure e11: n[41-44]
21
+ enclosure e12: n[45-48]
22
+ enclosure e13: n[49-52]
23
+ enclosure e14: n[53-56]
24
+ enclosure e15: n[57-60]
25
+ enclosure e16: n[61-64]
26
+
27
+ rack rack1: e[1-8]
28
+ rack rack2: e[9-16]
@@ -0,0 +1,51 @@
1
+ .TH nodes.conf 5 "Version 1.8.3"
2
+ .SH NAME
3
+ nodes.conf \- definition of nodes and groups for the
4
+ .I nodes(1)
5
+ command
6
+ .SH SYNOPSIS
7
+ .B /etc/nodes.conf
8
+ .SH DESCRIPTION
9
+ This file defines the nodes and groups known to the
10
+ .I nodes(1)
11
+ command.
12
+
13
+ Definitions have to start at the beginning of a line, with no
14
+ prepended whitespace. An indented line following a definition is
15
+ assumed to contain a nodespec. The nodes it refers to are added to
16
+ the group being defined.
17
+
18
+ Blank lines are ignored. A # sign starts a comment that
19
+ causes the rest of the line to be ignored.
20
+
21
+ Before groups are defined, valid node names have to be defined
22
+ with a line that starts with "nodes:" and then contains a hostlist
23
+ listing all valid node names.
24
+
25
+ A group is defined by a line containing the level name, whitespace,
26
+ the group name, colon, whitespace and then a nodespec listing the
27
+ members of the group.
28
+
29
+ A group name has to be unique. The same group name cannot be used at
30
+ several levels. A nodespec can only refer to nodes that have already
31
+ been defined.
32
+ .SH EXAMPLES
33
+ The example below defines sixteen nodes n[1-16] belonging
34
+ to four enclosures e[1-4]. Nodes n[1-8] belong to rack r1,
35
+ while nodes n[9-12] belong to rack r2. We define enclosure e4
36
+ in an odd way just to show how multiple nodespecs can be used.
37
+ .nf
38
+ \fB
39
+ nodes: n[1-16]
40
+ enclosure e1: n[1-4]
41
+ enclosure e2: n[5-8]
42
+ enclosure e3: n[9-12]
43
+ enclosure e4: n13
44
+ n14
45
+ n15,n16
46
+ rack r1: e[1-2]
47
+ rack r2: e[3-4]
48
+ \fR
49
+ .fi
50
+ .SH SEE ALSO
51
+ nodes(1)
@@ -0,0 +1,28 @@
1
+ # Sample config file
2
+
3
+ # We have to define nodes first, so that misspelled groups won't be
4
+ # treaded as node names:
5
+
6
+ nodes: n[1-64]
7
+
8
+ # Now, we define groups
9
+
10
+ enclosure e1: n[1-4]
11
+ enclosure e2: n[5-8]
12
+ enclosure e3: n[9-12]
13
+ enclosure e4: n[13-16]
14
+ enclosure e5: n[17-20]
15
+ enclosure e6: n[21-24]
16
+ enclosure e7: n[25-28]
17
+ enclosure e8: n[29-32]
18
+ enclosure e9: n[33-36]
19
+ enclosure e10: n[37-40]
20
+ enclosure e11: n[41-44]
21
+ enclosure e12: n[45-48]
22
+ enclosure e13: n[49-52]
23
+ enclosure e14: n[53-56]
24
+ enclosure e15: n[57-60]
25
+ enclosure e16: n[61-64]
26
+
27
+ rack rack1: e[1-8]
28
+ rack rack2: e[9-16]
@@ -0,0 +1,376 @@
1
+ # Version 1.8.3
2
+
3
+ import re
4
+ import sys
5
+ from hostlist import expand_hostlist, collect_hostlist
6
+ import subprocess
7
+
8
+ # Exceptions
9
+
10
+ class NodesException(Exception):
11
+ def __init__(self, msg):
12
+ self.msg = msg
13
+ class UnknownName(NodesException): pass
14
+ class DuplicatedGroup(NodesException): pass
15
+ class BadConfigSyntax(NodesException): pass
16
+ class MissingGroup(NodesException): pass
17
+ class DuplicateMembership(NodesException): pass
18
+ class UnknownLevel(NodesException): pass
19
+ class AmbiguousLevel(NodesException): pass
20
+ class FailedSLURM(NodesException): pass
21
+
22
+ # Helper functions
23
+
24
+ def collect_name_hostlist(node_iterable):
25
+ return collect_hostlist([node.name for node in node_iterable])
26
+
27
+ # Sort a list of entities numerically
28
+
29
+ def numerically_sorted_entities (l):
30
+ """Sort a list of entities numerically.
31
+
32
+ E.g. sorted order should be n1, n2, n10; not n1, n10, n2.
33
+ """
34
+
35
+ return sorted(l, key=entity_numeric_sort_key)
36
+
37
+ nsk_re = re.compile("([0-9]+)|([^0-9]+)")
38
+ def entity_numeric_sort_key(x):
39
+ return [handle_int_nonint(i_ni) for i_ni in nsk_re.findall(x.name)]
40
+
41
+ def handle_int_nonint(int_nonint_tuple):
42
+ if int_nonint_tuple[0]:
43
+ return int(int_nonint_tuple[0])
44
+ else:
45
+ return int_nonint_tuple[1]
46
+
47
+
48
+ # Classes
49
+
50
+ class Entity:
51
+ pass
52
+
53
+ class Node(Entity):
54
+ def __init__(self, name):
55
+ self.name = name
56
+ self.part_of = {} # level_name -> Group
57
+
58
+ def __repr__(self):
59
+ return "<Node: %s>" % (self.name)
60
+
61
+ def expand_to_node_set(self):
62
+ return {self}
63
+
64
+ def add_to_group(self, group):
65
+ if group.level in self.part_of:
66
+ raise DuplicateMembership("%s cannot belong to %s %s and %s at the same time" % \
67
+ (self.name,
68
+ group.level,
69
+ self.part_of[group.level].name,
70
+ group.name))
71
+
72
+ self.part_of[group.level] = group
73
+
74
+ def get_group(self, level):
75
+ return self.part_of.get(level)
76
+
77
+ def get_clustershell_name(self):
78
+ return self.name
79
+
80
+ class Group(Entity):
81
+ def __init__(self, hierarchy, level, name, all_nodes_group = False):
82
+ self.hierarchy = hierarchy
83
+ self.level = level
84
+ self.name = name
85
+ self.node_set = set() # Node set
86
+ self.representative = None
87
+ self.sorted_nodes = None
88
+ self.all_nodes_group = all_nodes_group
89
+ self.nodespecs = [] # Raw list of all nodespecs added
90
+
91
+ def __repr__(self):
92
+ return f"<Group {self.level}: {self.name}>"
93
+
94
+ def expand_to_node_set(self):
95
+ return self.node_set
96
+
97
+ def get_sorted_nodes(self):
98
+ if self.sorted_nodes is None:
99
+ self.sorted_nodes = numerically_sorted_entities(self.node_set)
100
+ return self.sorted_nodes
101
+
102
+ def get_representative(self):
103
+ if self.representative is None:
104
+ self.representative = self.get_sorted_nodes()[0]
105
+ return self.representative
106
+
107
+ def get_index_of(self, node):
108
+ return self.get_sorted_nodes().index(node)
109
+
110
+ def add_nodespec(self, nodespec):
111
+ #print " NODES", self.name, "-->", nodespec
112
+ self.nodespecs.append(nodespec)
113
+ node_set = set()
114
+ for name in expand_hostlist(nodespec):
115
+ entity = self.hierarchy.get_entity(name, auto_add_node = self.all_nodes_group)
116
+ nodes = entity.expand_to_node_set()
117
+ node_set |= nodes
118
+ #print " NODE_SET", node_set
119
+ self.node_set |= node_set
120
+ for node in node_set:
121
+ node.add_to_group(self)
122
+ self.representative = None
123
+ return self
124
+
125
+ def get_clustershell_name(self):
126
+ return f"@{self.level}:{self.name}"
127
+
128
+ def get_clustershell_definition(self):
129
+ res = []
130
+ for ns in self.nodespecs:
131
+ converted_set = set()
132
+ for e in set(expand_hostlist(ns)):
133
+ entity = self.hierarchy.get_entity(e)
134
+ converted_set.add(entity.get_clustershell_name())
135
+ part = collect_hostlist(converted_set)
136
+
137
+ res.append(part)
138
+ return ",".join(res)
139
+
140
+ class FakedNode(Entity):
141
+ """Use to fake nodes with indexes for --index"""
142
+ def __init__(self, nodename, index, separator):
143
+ self.name = "%s%s%d" %(nodename, separator, index)
144
+
145
+ class Hierarchy:
146
+ def __init__(self):
147
+ self.names = {} # name -> Node or Group
148
+ self.level_names = set() # level names used for abbreviation lookup
149
+
150
+ self.level_names_list = [] # level names in the order mentioned in the file
151
+ self.level_groups = {} # level name -> list of Groups
152
+
153
+ def create_group(self, level, name, all_nodes_group = False):
154
+ #print "GROUP", level, name
155
+ if not level in self.level_names:
156
+ self.level_names_list.append(level)
157
+ self.level_groups[level] = []
158
+ self.level_names.add(level)
159
+
160
+ if name in self.names:
161
+ raise DuplicatedGroup("group %s seen twice" % name)
162
+ group = Group(self, level, name, all_nodes_group)
163
+ self.names[name] = group
164
+ self.level_groups[level].append(group)
165
+ return group
166
+
167
+ def create_or_get_group(self, level, name):
168
+ if name in self.names:
169
+ return self.names[name]
170
+ else:
171
+ return self.create_group(level, name)
172
+
173
+ def get_entity(self, name, auto_add_node = False):
174
+ if name not in self.names:
175
+ if auto_add_node:
176
+ self.names[name] = Node(name)
177
+ else:
178
+ raise UnknownName("unknown name " + name)
179
+ return self.names[name]
180
+
181
+ def parse_file(self, file_or_filename):
182
+ if isinstance(file_or_filename, str):
183
+ f = open(file_or_filename)
184
+ else:
185
+ f = file_or_filename
186
+ current_group = None
187
+ for line in f:
188
+ line = re.sub(r' *#.*', '', line)
189
+ line = line.rstrip()
190
+ if line.strip() == "": continue
191
+
192
+ # nodes: <nodes>
193
+ m = re.match(r'^nodes:\s*(.*)', line)
194
+ if m:
195
+ rest = m.group(1)
196
+ current_group = self.create_group("all", "nodes", all_nodes_group = True)
197
+ if rest:
198
+ current_group.add_nodespec(rest)
199
+ continue
200
+
201
+ # <level> <name>: <parts>
202
+ m = re.match(r'^([a-z]+)\s+([a-z0-9]+):\s*(.*)', line)
203
+ if m:
204
+ (level, name, rest) = m.group(1, 2, 3)
205
+ current_group = self.create_group(level, name)
206
+ if rest:
207
+ current_group.add_nodespec(rest)
208
+ continue
209
+
210
+ # <parts> (indented)
211
+ if current_group is not None:
212
+ m = re.match(r'^\s+(.*)', line)
213
+ if m:
214
+ rest = m.group(1)
215
+ current_group.add_nodespec(rest)
216
+ continue
217
+
218
+ # Fail
219
+ raise BadConfigSyntax(line)
220
+ return self
221
+
222
+ def parse_slurm(self):
223
+ rc, slurm_data = subprocess.getstatusoutput('/usr/bin/squeue -aho "%i %u %N"')
224
+ if rc != 0:
225
+ raise FailedSLURM("Failed to get data from SLURM")
226
+
227
+ for line in slurm_data.split("\n"):
228
+ fields = line.split()
229
+ if len(fields) != 3:
230
+ continue
231
+ job, user, nodes = fields
232
+
233
+ gj = self.create_group("job", "job" + job)
234
+ gj.add_nodespec(nodes)
235
+
236
+ gu = self.create_or_get_group("user", user)
237
+ gu.add_nodespec(nodes)
238
+ return self
239
+
240
+ def expand_abbreviated_level(self, level):
241
+ matching = [l for l in self.level_names if l.startswith(level)]
242
+ if len(matching) == 1:
243
+ return matching[0]
244
+ elif len(matching) == 0:
245
+ raise UnknownLevel("unknown level %s" % level)
246
+ else:
247
+ raise AmbiguousLevel("ambiguous level {} matching {}".format(level,
248
+ ", ".join(matching)))
249
+
250
+ def expand_to_node_set(self, nodespec):
251
+ res = set()
252
+ for name in expand_hostlist(nodespec):
253
+ entity = self.get_entity(name)
254
+ res |= entity.expand_to_node_set()
255
+ return res
256
+
257
+ # This is the method invoked by "nodes --to-nodes" (default)
258
+ def to_nodes(self, nodespec):
259
+ return self.expand_to_node_set(nodespec)
260
+
261
+ # This is the central logic behind up, fill and gather
262
+ # Returns a tuple of sets (groups, leftovers, missing):
263
+ # groups: groups that nodes in nodespec belong to
264
+ # leftovers: nodes that did not fill whole groups (or empty if fill is True)
265
+ # missing: nodes that did not belong to any group
266
+
267
+ def up_set(self, level, nodespec, fill = True, missing_group = None):
268
+ groups = set()
269
+ leftovers = set()
270
+ missing = set()
271
+ node_set = self.expand_to_node_set(nodespec)
272
+ for node in node_set:
273
+ group = node.get_group(level)
274
+ if group is None:
275
+ if missing_group is None:
276
+ missing.add(node)
277
+ else:
278
+ groups.add(self.create_or_get_group(level, missing_group))
279
+ else:
280
+ if fill or group.expand_to_node_set() <= node_set:
281
+ groups.add(group)
282
+ else:
283
+ leftovers.add(node)
284
+
285
+ return groups, leftovers, missing
286
+
287
+ # This is the method invoked by "nodes --up"
288
+ def up(self, level, nodespec, missing_group = None):
289
+ level = self.expand_abbreviated_level(level)
290
+ groups, leftovers, missing = self.up_set(level, nodespec,
291
+ missing_group = missing_group)
292
+
293
+ assert len(leftovers) == 0
294
+ if missing:
295
+ raise MissingGroup("missing %s for %s" % \
296
+ (level,
297
+ collect_name_hostlist(missing)))
298
+
299
+ return groups
300
+
301
+
302
+ # This is the method invoked by "nodes --fill"
303
+ def fill(self, level, nodespec):
304
+ level = self.expand_abbreviated_level(level)
305
+ groups, leftovers, missing = self.up_set(level, nodespec)
306
+
307
+ assert len(leftovers) == 0
308
+ if missing:
309
+ sys.stderr.write("Warning: missing %s for %s\n" % \
310
+ (level,
311
+ collect_name_hostlist(missing)))
312
+
313
+ # FIXME: Check if it makes more sense to signal error above?
314
+ res = missing
315
+
316
+ for group in groups:
317
+ res |= group.expand_to_node_set()
318
+ return res
319
+
320
+ # This is the method invoked by "nodes --representative"
321
+ def representative(self, level, nodespec):
322
+ level = self.expand_abbreviated_level(level)
323
+ groups, leftovers, missing = self.up_set(level, nodespec)
324
+
325
+ assert len(leftovers) == 0
326
+ if missing:
327
+ sys.stderr.write("Warning: missing %s for %s\n" % \
328
+ (level,
329
+ collect_name_hostlist(missing)))
330
+
331
+ # FIXME: Check if it makes more sense to signal error above?
332
+ res = missing
333
+
334
+ for group in groups:
335
+ res.add(group.get_representative())
336
+ return res
337
+
338
+ # This is the method invoked by "nodes --gather"
339
+ def gather(self, level, nodespec):
340
+ level = self.expand_abbreviated_level(level)
341
+ groups, leftovers, missing = self.up_set(level, nodespec, fill=False)
342
+
343
+ if missing:
344
+ sys.stderr.write("Warning: missing %s for %s\n" % \
345
+ (level,
346
+ collect_name_hostlist(missing)))
347
+
348
+ return groups|leftovers|missing
349
+
350
+ # This is the method invoked by "nodes --index"
351
+ def index(self, level, nodespec, separator = ": "):
352
+ node_set = self.expand_to_node_set(nodespec)
353
+ level = self.expand_abbreviated_level(level)
354
+ res = []
355
+ missing = set()
356
+ for node in node_set:
357
+ group = node.get_group(level)
358
+ if group is None:
359
+ missing.add(node)
360
+ else:
361
+ index = group.get_index_of(node)
362
+ res.append(FakedNode(node.name, index, separator))
363
+ if missing:
364
+ sys.stderr.write("Warning: missing %s for %s\n" % \
365
+ (level,
366
+ collect_name_hostlist(missing)))
367
+ return res
368
+
369
+ # Convert to ClusterShell YAML definition
370
+ def convert_to_clustershell(self):
371
+ for level in self.level_names_list:
372
+ if level == "all": continue
373
+ print("%s:" % level)
374
+ for group in self.level_groups[level]:
375
+ print(f" {group.name}: '{group.get_clustershell_definition()}'")
376
+ print()
@@ -0,0 +1,99 @@
1
+ Name: nodes
2
+ Version: %(cat VERSION.file)
3
+ Release: 1%{?dist}
4
+ Summary: Handle hierarchical groups of nodes
5
+ Vendor: NSC
6
+
7
+ Group: Development/Languages
8
+ License: GPLv2+
9
+ URL: http://www.nsc.liu.se/~kent/nodes/
10
+ Source0: http://www.nsc.liu.se/~kent/nodes/%{name}-%{version}.tar.gz
11
+ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
12
+
13
+ BuildArch: noarch
14
+ BuildRequires: python3-devel
15
+ Requires: python3-hostlist
16
+
17
+
18
+ # The default value is '-s' (a subset of -I).
19
+ %global py3_shbang_opts -I
20
+
21
+
22
+ %description
23
+ Handle hierarchical groups of nodes, such as enclosures and racks.
24
+
25
+
26
+ %prep
27
+ %setup -q
28
+
29
+
30
+ %build
31
+ %py3_build
32
+
33
+
34
+ %install
35
+ %py3_install -- --prefix /usr
36
+
37
+
38
+ %files
39
+ %defattr(-,root,root,-)
40
+ %{python3_sitelib}/nodes.py
41
+ %{python3_sitelib}/nodes-*.egg-info
42
+ %{python3_sitelib}/__pycache__/*
43
+ /usr/bin/nodes
44
+ %config(noreplace) /etc/nodes.conf
45
+ /usr/share/man/man1/nodes.1.gz
46
+ /usr/share/man/man5/nodes.conf.5.gz
47
+ %doc nodes.conf.sample
48
+
49
+
50
+ %changelog
51
+ * Tue Mar 04 2026 Christian Luckey <rovanion@nsc.liu.se> - 1.8.3-1
52
+ - Move project declaration to pyproject.toml
53
+ - setup.py: Remove indicators of Python 2 support
54
+ - Remove #RELEASE# from nodes and nodes.py
55
+
56
+ * Fri Feb 27 2026 Christian Luckey <rovanion@nsc.liu.se> - 1.8.2-1
57
+ - Return self from self-modifying methods of Group and Hierarchy
58
+
59
+ * Tue Nov 15 2022 Torbjörn Lönnemark <ketl@nsc.liu.se> - 1.8.1-1
60
+ - Let RPM handle Python interpreter line flags
61
+
62
+ * Thu Oct 27 2022 Torbjörn Lönnemark <ketl@nsc.liu.se> - 1.8.0-1
63
+ - Port to Python 3
64
+
65
+ * Wed Jun 2 2021 Torbjörn Lönnemark <ketl@nsc.liu.se> - 1.7-1
66
+ - Specify Python version explicitly in shebang
67
+ - Explicitly use Python 2 in Makefile
68
+ - Fix spec file for el8
69
+ - Remove unneeded python_sitelib fallback
70
+ - Update dependency hostlist to use new package name
71
+ - Include dist tag in Release in RPMs
72
+ - Add --convert-to-clustershell option.
73
+
74
+ * Fri Nov 2 2012 Kent Engström <kent@nsc.liu.se> - 1.6-1
75
+ - Add --count, --index and --index-separator.
76
+
77
+ * Mon Aug 23 2010 Kent Engström <kent@nsc.liu.se> - 1.5-1
78
+ - Add --expand together with --separator, --prepend and --append.
79
+
80
+ * Tue Mar 9 2010 Kent Engström <kent@nsc.liu.se> - 1.4-1
81
+ - Add --representative operation.
82
+ - Add --missing option for the --up operation.
83
+
84
+ * Sun Feb 21 2010 Kent Engström <kent@nsc.liu.se> - 1.3-1
85
+ - Add man pages nodes(1) and nodes.conf(5).
86
+ - Change gather behaviour when nodes do not belong to a group.
87
+ - Refactor and rename internal methods.
88
+ - Add unit tests.
89
+
90
+ * Tue Feb 09 2010 Kent Engström <kent@nsc.liu.se> - 1.2-1
91
+ - Add dynamic SLURM groups for users and jobs.
92
+
93
+ * Mon Nov 02 2009 Kent Engström <kent@nsc.liu.se> - 1.1-1
94
+ - Add one-letter options for --up, --fill and --gather.
95
+ - Add --config-file option.
96
+ - Allow abbreviations of level names as long as they are not ambiguous.
97
+
98
+ * Fri Oct 09 2009 Kent Engström <kent@nsc.liu.se> - 1.0-1
99
+ - Package as RPM.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hpc-nodes"
7
+ description = "Handle hierarchical groups of nodes"
8
+ authors = [
9
+ {name = "Kent Engström", email = "kent@nsc.liu.se"},
10
+ ]
11
+ requires-python = ">=3.9"
12
+ license = "GPL-2.0"
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Science/Research",
16
+ "Intended Audience :: System Administrators",
17
+ "Topic :: System :: Clustering",
18
+ "Topic :: System :: Systems Administration",
19
+ "Programming Language :: Python :: 3",
20
+ ]
21
+ dynamic = [ "version" ]
22
+ dependencies = [
23
+ "python-hostlist",
24
+ ]
25
+
26
+ [project.urls]
27
+ homepage = "http://www.nsc.liu.se/~kent/nodes/"
28
+
29
+ [tool.setuptools.dynamic]
30
+ version = { file = "VERSION.file" }
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "ruff",
35
+ "twine",
36
+ ]
37
+
38
+ [tool.ruff.lint]
39
+ ignore = [ "E731", "E701" ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from setuptools import setup
2
+
3
+ setup(name = "hpc-nodes",
4
+ py_modules = ["nodes"],
5
+ scripts = ["nodes"],
6
+ data_files = [("/etc", ["nodes.conf"]),
7
+ ("share/man/man1", ["nodes.1"]),
8
+ ("share/man/man5", ["nodes.conf.5"]),
9
+ ],
10
+ )
@@ -0,0 +1,152 @@
1
+ import nodes
2
+ import unittest
3
+ import io
4
+
5
+ class Test1(unittest.TestCase):
6
+
7
+ def setUp(self):
8
+ self.h = nodes.Hierarchy()
9
+ f = io.StringIO("""
10
+ nodes: n[1-99]
11
+ enclosure e1: n[1-4]
12
+ enclosure e2: n[5-8]
13
+ enclosure e3: n[9-12]
14
+ enclosure e4: n[13-16]
15
+ rack r1: e[1-2]
16
+ rack r2: e[3-4]
17
+ oddball o1: n[6-10]
18
+ """)
19
+ self.h.parse_file(f)
20
+ f.close()
21
+
22
+ def expands_to(self, nodespec, hostlist):
23
+ self.assertEqual(nodes.collect_name_hostlist(self.h.to_nodes(nodespec)), hostlist)
24
+
25
+ def expands_bad(self, nodespec):
26
+ self.assertRaises(nodes.NodesException,
27
+ self.h.to_nodes, nodespec)
28
+
29
+ def test_expand(self):
30
+ self.expands_to("", "")
31
+ self.expands_to("n1", "n1")
32
+ self.expands_to("n99", "n99")
33
+ self.expands_to("e1", "n[1-4]")
34
+ self.expands_to("e1,e1", "n[1-4]")
35
+ self.expands_to("e[1-2]", "n[1-8]")
36
+ self.expands_to("e[1,3]", "n[1-4,9-12]")
37
+ self.expands_to("e1,n1", "n[1-4]")
38
+ self.expands_to("e1,n5", "n[1-5]")
39
+ self.expands_to("n6,e1", "n[1-4,6]")
40
+ self.expands_to("r1", "n[1-8]")
41
+ self.expands_to("r1,e3", "n[1-12]")
42
+ self.expands_to("r1,e4", "n[1-8,13-16]")
43
+ self.expands_to("o1", "n[6-10]")
44
+ self.expands_to("o1,e1", "n[1-4,6-10]")
45
+ self.expands_to("o1,e2", "n[5-10]")
46
+ self.expands_to("o1,e3", "n[6-12]")
47
+
48
+ self.expands_bad("n0")
49
+ self.expands_bad("n100")
50
+
51
+ def ufrg(self, level, nodespec, up, fill, representative, gather):
52
+ if up is not None:
53
+ self.assertEqual(nodes.collect_name_hostlist(self.h.up(level, nodespec)), up)
54
+ else:
55
+ self.assertRaises(nodes.NodesException,
56
+ self.h.up, level, nodespec)
57
+
58
+ if fill is not None:
59
+ self.assertEqual(nodes.collect_name_hostlist(self.h.fill(level, nodespec)), fill)
60
+ else:
61
+ self.assertRaises(nodes.NodesException,
62
+ self.h.fill, level, nodespec)
63
+
64
+ if representative is not None:
65
+ self.assertEqual(nodes.collect_name_hostlist(self.h.representative(level, nodespec)), representative)
66
+ else:
67
+ self.assertRaises(nodes.NodesException,
68
+ self.h.representative, level, nodespec)
69
+
70
+ if gather is not None:
71
+ self.assertEqual(nodes.collect_name_hostlist(self.h.gather(level, nodespec)), gather)
72
+ else:
73
+ self.assertRaises(nodes.NodesException,
74
+ self.h.gather, level, nodespec)
75
+
76
+ def ufrg_e(self, nodespec, up, fill, representative, gather):
77
+ self.ufrg("enclosure", nodespec, up, fill, representative, gather)
78
+
79
+ def ufrg_r(self, nodespec, up, fill, representative, gather):
80
+ self.ufrg("rack", nodespec, up, fill, representative, gather)
81
+
82
+ def test_up_fill_representative_gather(self):
83
+ # NODESPEC UP FILL REPRESENTATIVE GATHER
84
+ # -----------------------------------------------------------------------------------
85
+ self.ufrg_e("", "", "", "", "")
86
+ self.ufrg_e("e1", "e1", "n[1-4]", "n1", "e1")
87
+ self.ufrg_e("n1", "e1", "n[1-4]", "n1", "n1")
88
+ self.ufrg_e("n2", "e1", "n[1-4]", "n1", "n2")
89
+ self.ufrg_e("n3", "e1", "n[1-4]", "n1", "n3")
90
+ self.ufrg_e("n4", "e1", "n[1-4]", "n1", "n4")
91
+ self.ufrg_e("n5", "e2", "n[5-8]", "n5", "n5")
92
+ self.ufrg_e("n[1-2]", "e1", "n[1-4]", "n1", "n[1-2]")
93
+ self.ufrg_e("n[4-5]", "e[1-2]", "n[1-8]", "n[1,5]", "n[4-5]")
94
+ self.ufrg_e("n[1-5,14]", "e[1-2,4]", "n[1-8,13-16]", "n[1,5,13]", "e1,n[5,14]")
95
+ self.ufrg_e("n[1-8,14]", "e[1-2,4]", "n[1-8,13-16]", "n[1,5,13]", "e[1-2],n14")
96
+ self.ufrg_e("n[1-9,14]", "e[1-4]", "n[1-16]", "n[1,5,9,13]", "e[1-2],n[9,14]")
97
+
98
+ self.ufrg_e("r1", "e[1-2]", "n[1-8]", "n[1,5]", "e[1-2]")
99
+ self.ufrg_e("r1,e3", "e[1-3]", "n[1-12]", "n[1,5,9]", "e[1-3]")
100
+ self.ufrg_e("r1,e4", "e[1-2,4]", "n[1-8,13-16]", "n[1,5,13]", "e[1-2,4]")
101
+ self.ufrg_e("r1,n16", "e[1-2,4]", "n[1-8,13-16]", "n[1,5,13]", "e[1-2],n16")
102
+ self.ufrg_e("r[1-2]", "e[1-4]", "n[1-16]", "n[1,5,9,13]", "e[1-4]")
103
+
104
+ self.ufrg_e("o1", "e[2-3]", "n[5-12]", "n[5,9]", "n[6-10]")
105
+
106
+ self.ufrg_e("e5", None, None, None, None)
107
+ self.ufrg_e("n17", None, "n17", "n17", "n17")
108
+ self.ufrg_e("n[16-17]", None, "n[13-17]", "n[13,17]", "n[16-17]") # representative?
109
+
110
+ self.ufrg_r("r1", "r1", "n[1-8]", "n1", "r1")
111
+ self.ufrg_r("e1", "r1", "n[1-8]", "n1", "n[1-4]")
112
+ self.ufrg_r("n1", "r1", "n[1-8]", "n1", "n1")
113
+ self.ufrg_r("e[1-2]", "r1", "n[1-8]", "n1", "r1")
114
+ self.ufrg_r("e[1-3]", "r[1-2]", "n[1-16]", "n[1,9]", "n[9-12],r1")
115
+ self.ufrg_r("e[1-4]", "r[1-2]", "n[1-16]", "n[1,9]", "r[1-2]")
116
+ self.ufrg_r("r1,e4", "r[1-2]", "n[1-16]", "n[1,9]", "n[13-16],r1")
117
+ self.ufrg_r("r1,n16", "r[1-2]", "n[1-16]", "n[1,9]", "n16,r1")
118
+
119
+ self.ufrg_r("o1", "r[1-2]", "n[1-16]", "n[1,9]", "n[6-10]")
120
+
121
+ self.ufrg_r("r3", None, None, None, None)
122
+ self.ufrg_r("n17", None, "n17", "n17", "n17")
123
+ self.ufrg_r("n[16-17]", None, "n[9-17]", "n[9,17]", "n[16-17]") # representative?
124
+
125
+ def um(self, level, nodespec, up, up_missing):
126
+ if up is not None:
127
+ self.assertEqual(nodes.collect_name_hostlist(self.h.up(level, nodespec)), up)
128
+ else:
129
+ self.assertRaises(nodes.NodesException,
130
+ self.h.up, level, nodespec)
131
+ if up_missing is not None:
132
+ self.assertEqual(nodes.collect_name_hostlist(self.h.up(level, nodespec, "M")), up_missing)
133
+ else:
134
+ self.assertRaises(nodes.NodesException,
135
+ self.h.up, level, nodespec, "M")
136
+
137
+ def um_e(self, nodespec, up, up_missing):
138
+ self.um("enclosure", nodespec, up, up_missing)
139
+
140
+ def test_up_missing(self):
141
+ # NODESPEC UP UP_MISSING
142
+ # ---------------------------------------------
143
+ self.um_e("n1", "e1", "e1")
144
+ self.um_e("n17", None, "M")
145
+ self.um_e("n[16-17]", None, "M,e4")
146
+ self.um_e("n[1-20]", None, "M,e[1-4]")
147
+
148
+
149
+ if __name__ == '__main__':
150
+ unittest.main()
151
+
152
+