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.
- hpc_nodes-1.8.3/MANIFEST.in +8 -0
- hpc_nodes-1.8.3/PKG-INFO +15 -0
- hpc_nodes-1.8.3/README +1 -0
- hpc_nodes-1.8.3/VERSION.file +1 -0
- hpc_nodes-1.8.3/hpc_nodes.egg-info/PKG-INFO +15 -0
- hpc_nodes-1.8.3/hpc_nodes.egg-info/SOURCES.txt +18 -0
- hpc_nodes-1.8.3/hpc_nodes.egg-info/dependency_links.txt +1 -0
- hpc_nodes-1.8.3/hpc_nodes.egg-info/requires.txt +1 -0
- hpc_nodes-1.8.3/hpc_nodes.egg-info/top_level.txt +1 -0
- hpc_nodes-1.8.3/nodes +134 -0
- hpc_nodes-1.8.3/nodes.1 +201 -0
- hpc_nodes-1.8.3/nodes.conf +28 -0
- hpc_nodes-1.8.3/nodes.conf.5 +51 -0
- hpc_nodes-1.8.3/nodes.conf.sample +28 -0
- hpc_nodes-1.8.3/nodes.py +376 -0
- hpc_nodes-1.8.3/nodes.spec +99 -0
- hpc_nodes-1.8.3/pyproject.toml +39 -0
- hpc_nodes-1.8.3/setup.cfg +4 -0
- hpc_nodes-1.8.3/setup.py +10 -0
- hpc_nodes-1.8.3/test/test_nodes.py +152 -0
hpc_nodes-1.8.3/PKG-INFO
ADDED
|
@@ -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
|
+
|
|
@@ -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)
|
hpc_nodes-1.8.3/nodes.1
ADDED
|
@@ -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]
|
hpc_nodes-1.8.3/nodes.py
ADDED
|
@@ -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" ]
|
hpc_nodes-1.8.3/setup.py
ADDED
|
@@ -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
|
+
|