osc-reservations 1.0.3
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +103 -0
- data/Rakefile +7 -0
- data/config/batch.yml +13 -0
- data/config/websvcs08.osc.edu.yml +13 -0
- data/lib/osc/reservations.rb +47 -0
- data/lib/osc/reservations/adapter.rb +38 -0
- data/lib/osc/reservations/adapters/osc_moab.rb +167 -0
- data/lib/osc/reservations/batch.rb +39 -0
- data/lib/osc/reservations/node.rb +42 -0
- data/lib/osc/reservations/query.rb +60 -0
- data/lib/osc/reservations/reservation.rb +53 -0
- data/lib/osc/reservations/version.rb +6 -0
- data/osc-reservations.gemspec +26 -0
- data/test/adapters/test_osc_moab.rb +115 -0
- data/test/files/batch.yml +4 -0
- data/test/files/osc_oakley_job_4691489.oak-batch.osc.edu.yml +47 -0
- data/test/files/osc_oakley_job_4691490.oak-batch.osc.edu.yml +47 -0
- data/test/files/osc_oakley_job_4691491.oak-batch.osc.edu.yml +47 -0
- data/test/files/osc_oakley_node_n0609.yml +14 -0
- data/test/files/osc_oakley_node_n0611.yml +15 -0
- data/test/files/osc_oakley_node_n0612.yml +13 -0
- data/test/files/osc_oakley_node_n0613.yml +13 -0
- data/test/files/osc_rsv.xml +18 -0
- data/test/files/osc_rsvs.xml +97 -0
- data/test/test_adapter.rb +14 -0
- data/test/test_batch.rb +23 -0
- data/test/test_node.rb +21 -0
- data/test/test_query.rb +62 -0
- data/test/test_reservation.rb +33 -0
- data/test/test_reservations.rb +28 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ab5a834ec9a8cff3089cb19f5ad11c3ae15ffc14
|
4
|
+
data.tar.gz: 526dc32a86f095ab039bb7a39f046d8566cdc9cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: acaca8c8eddeb61afb95eb33a70c5f119148a8f2f2496b31dbadf3baad6ba5220ae43353bd11e553c2292beb0e3e1fb36a8a1cb03ea97197e13abcb18b977f62
|
7
|
+
data.tar.gz: 0ef6c89a286cce588c33bd94e97aa2afbd18e03f664a50b92e23ed8412dfe28b2908feec94bda15639f72e79c7c4eb5496ca0174d0fd57325183e17f9c965e2f
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015-2016 Ohio Supercomputer Center
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# OSC::Reservations
|
2
|
+
|
3
|
+
Ruby library that queries OSC systems for active batch reservations of the
|
4
|
+
current user.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'osc-reservations'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle install
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
Currently only `OSC::Reservations::Query` is implemented, meaning you can query
|
19
|
+
for available reservations or for a single reservation. At this time you are
|
20
|
+
unable to submit a reservation to the system. Also it should be noted that the
|
21
|
+
OSC scheduler is set up to display only the reservations owned by the current
|
22
|
+
running user. Viewing reservations that the current user does not have access
|
23
|
+
to is impossible at this time.
|
24
|
+
|
25
|
+
### Simple Example (Oakley)
|
26
|
+
|
27
|
+
This library has a set of OSC clusters pre-programmed into it. Querying a
|
28
|
+
reservation of the Oakley cluster is as simple as
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
q = OSC::Reservations::Query.oakley
|
32
|
+
|
33
|
+
q.reservations
|
34
|
+
```
|
35
|
+
|
36
|
+
This will return an array of immutable `Reservation` objects containing the
|
37
|
+
relevant information for each reservation allocated to the user. The structure
|
38
|
+
of such objects are detailed in the corresponding yardoc pages.
|
39
|
+
|
40
|
+
### Advance Example
|
41
|
+
|
42
|
+
This library comes with a list of adapters to communicate with your
|
43
|
+
corresponding batch scheduler. As of writing this the only adapter implemented
|
44
|
+
is the `Adapters::OSCMoab` adapter. It provides an interface to OSC's Moab
|
45
|
+
batch scheduler.
|
46
|
+
|
47
|
+
Once you have chosen your desired adapter, you will need to define a `Batch`
|
48
|
+
server object that the library will connect to when requesting/submitting the
|
49
|
+
reservations. Certain adapters may have further requirements on the `Batch`
|
50
|
+
object (e.g., `Adapters::OSCMoab` requires a `mrsvctl` property on the `Batch`
|
51
|
+
object).
|
52
|
+
|
53
|
+
A full implementation would look like
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
adapter = OSC::Reservations::Adapters::OSCMoab.new
|
57
|
+
batch = OSC::Reservations::Batch.new(
|
58
|
+
oak-batch.osc.edu,
|
59
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab-6.1.11/lib /usr/local/moab-6.1.11/bin/mrsvctl'
|
60
|
+
)
|
61
|
+
|
62
|
+
q = OSC::Reservations::Query.new adapter, batch
|
63
|
+
q.reservations
|
64
|
+
```
|
65
|
+
|
66
|
+
### Configure your own Batch Schedulers
|
67
|
+
|
68
|
+
Currently the library will read from a defined configuration YAML file located
|
69
|
+
at `conf/batch.yml`. An example entry may look like
|
70
|
+
|
71
|
+
```yaml
|
72
|
+
oakley:
|
73
|
+
adapter: 'OSC::Reservations::Adapters::OSCMoab'
|
74
|
+
server: 'oak-batch.osc.edu'
|
75
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab-6.1.11/lib /usr/local/moab-6.1.11/bin/mrsvctl'
|
76
|
+
```
|
77
|
+
|
78
|
+
The `Query` object will then use these entries when defining its methods. For
|
79
|
+
example, a developer can access this `oakley` batch scheduler through
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
q = OSC::Reservations::Query.oakley
|
83
|
+
```
|
84
|
+
|
85
|
+
To predefine your own batch schedulers you will need to make a local YAML file
|
86
|
+
with the necessary properties. Then you would be able to access and use this
|
87
|
+
YAML file as shown below.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
OSC::Reservations.batch_config_path = "/path/to/config.yml"
|
91
|
+
q = OSC::Reservations::Query.my_batch
|
92
|
+
|
93
|
+
q.reservations
|
94
|
+
```
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
1. Fork it
|
99
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
100
|
+
3. Test your changes (`rake test`)
|
101
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
102
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
103
|
+
6. Create new Pull Request
|
data/Rakefile
ADDED
data/config/batch.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
oakley:
|
2
|
+
adapter: &OSCMoab
|
3
|
+
type: 'OSC::Reservations::Adapters::OSCMoab'
|
4
|
+
batch:
|
5
|
+
server: 'oak-batch.osc.edu'
|
6
|
+
torque_name: 'oakley'
|
7
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab-6.1.11/lib:$LD_LIBRARY_PATH MOABHOMEDIR=/var/spool/batch/moab /usr/local/moab-6.1.11/bin/mrsvctl'
|
8
|
+
ruby:
|
9
|
+
adapter: *OSCMoab
|
10
|
+
batch:
|
11
|
+
server: 'ruby-batch.ten.osc.edu'
|
12
|
+
torque_name: 'ruby'
|
13
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab-6.1.11/lib:$LD_LIBRARY_PATH MOABHOMEDIR=/var/spool/batch/moab /usr/local/moab-6.1.11/bin/mrsvctl'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
oakley:
|
2
|
+
adapter: &OSCMoab
|
3
|
+
type: 'OSC::Reservations::Adapters::OSCMoab'
|
4
|
+
batch:
|
5
|
+
server: 'oak-batch.osc.edu'
|
6
|
+
torque_name: 'oakley'
|
7
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab/8.1.1.2-2015080516-eb28ad0-el6/lib:$LD_LIBRARY_PATH MOABHOMEDIR=/var/spool/batch/moab /usr/local/moab/8.1.1.2-2015080516-eb28ad0-el6/bin/mrsvctl'
|
8
|
+
ruby:
|
9
|
+
adapter: *OSCMoab
|
10
|
+
batch:
|
11
|
+
server: 'ruby-batch.ten.osc.edu'
|
12
|
+
torque_name: 'ruby'
|
13
|
+
mrsvctl: 'LD_LIBRARY_PATH=/usr/local/moab/8.1.1.2-2015080516-eb28ad0-el6/lib:$LD_LIBRARY_PATH MOABHOMEDIR=/var/spool/batch/moab /usr/local/moab/8.1.1.2-2015080516-eb28ad0-el6/bin/mrsvctl'
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
require_relative 'reservations/version'
|
5
|
+
require_relative 'reservations/batch'
|
6
|
+
require_relative 'reservations/reservation'
|
7
|
+
require_relative 'reservations/node'
|
8
|
+
require_relative 'reservations/query'
|
9
|
+
|
10
|
+
# The namespace used for OSC gems.
|
11
|
+
module OSC
|
12
|
+
# The main namespace for OSC::Reservations. Provides the ability to submit
|
13
|
+
# and read back reservations to the local batch scheduler.
|
14
|
+
module Reservations
|
15
|
+
# Path to batch server configuration file.
|
16
|
+
CONFIG_ROOT = File.expand_path("../../../config", __FILE__)
|
17
|
+
|
18
|
+
# Default path to the batch config yaml file.
|
19
|
+
# @return [String] Path to the default batch config yaml file.
|
20
|
+
def self.default_batch_config_path
|
21
|
+
host_config = File.expand_path("#{CONFIG_ROOT}/#{Socket.gethostname}.yml")
|
22
|
+
default_config = File.expand_path("#{CONFIG_ROOT}/batch.yml")
|
23
|
+
File.file?(host_config) ? host_config : default_config
|
24
|
+
end
|
25
|
+
|
26
|
+
# Path to the batch config yaml file describing the batch servers for
|
27
|
+
# local batch schedulers.
|
28
|
+
# @return [String] Path to the batch config yaml file.
|
29
|
+
def self.batch_config_path
|
30
|
+
@batch_config_path ||= default_batch_config_path
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the path to the batch config yaml file.
|
34
|
+
# @param path [String] The path to the batch config yaml file.
|
35
|
+
def self.batch_config_path=(path)
|
36
|
+
@batch_config_path = File.expand_path(path)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Hash generated from reading the batch config yaml file.
|
40
|
+
# @return [Hash] Batch configuration generated from config yaml file.
|
41
|
+
def self.batch_config
|
42
|
+
YAML.load_file(batch_config_path)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require_relative 'reservations/adapters/osc_moab'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module OSC
|
2
|
+
module Reservations
|
3
|
+
# Adapters are the glue between Reservations API and the scheduling
|
4
|
+
# service. This provides a scheduler independent interface.
|
5
|
+
class Adapter
|
6
|
+
# @param opts [Hash] The options to create an adapter with.
|
7
|
+
def initialize(opts = {})
|
8
|
+
end
|
9
|
+
|
10
|
+
# Queries the batch server for a given reservation.
|
11
|
+
# @param batch [Batch] The batch server to query for the reservation.
|
12
|
+
# @param id [String] The ID of the reservation.
|
13
|
+
# @return [Reservation, nil]
|
14
|
+
# @abstract This should be implemented by the adapter.
|
15
|
+
def query_reservation(batch, id)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Queries the batch server for a list of reservations.
|
20
|
+
# @param batch [Batch] The batch server to query for reservations.
|
21
|
+
# @return [Array<Reservation>]
|
22
|
+
# @abstract This should be implemented by the adapter.
|
23
|
+
def query_reservations(batch)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!method submit_reservation(batch, reservation)
|
28
|
+
# Submits a given reservation to the batch server.
|
29
|
+
# @param batch [Batch] The batch server to submit the reservation at.
|
30
|
+
# @param reservation [Reservation] A reservation with necessary information.
|
31
|
+
# @return [Reservation]
|
32
|
+
# @abstract This should be implemented by the adapter.
|
33
|
+
def submit_reservation(batch, reservation)
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open3'
|
3
|
+
require 'pbs'
|
4
|
+
|
5
|
+
require_relative '../adapter'
|
6
|
+
|
7
|
+
module OSC
|
8
|
+
module Reservations
|
9
|
+
# A namespace to hold all subclasses of {Adapter}.
|
10
|
+
module Adapters
|
11
|
+
# This adapter is designed for OSC systems using the Moab scheduler. This
|
12
|
+
# adapter requires the parameters <tt>torque_name</tt> and
|
13
|
+
# <tt>mrsvctl</tt> in the Batch object to work. Only tested on Torque >4.
|
14
|
+
class OSCMoab < Adapter
|
15
|
+
# Queries the batch server for a given reservation.
|
16
|
+
# @param batch [Batch] The batch server to query for the reservation.
|
17
|
+
# @param id [String] The ID of the reservation.
|
18
|
+
# @return [Reservation, nil]
|
19
|
+
# @raise [CommandLineError] if a command run from the shell exits with error
|
20
|
+
def query_reservation(batch, id)
|
21
|
+
cmd = "#{batch.mrsvctl} --host=#{batch.server} -q #{id} --xml"
|
22
|
+
begin
|
23
|
+
xml = get_xml(cmd, batch)
|
24
|
+
rescue CommandLineError
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
r_xml = xml.xpath("//rsv")
|
29
|
+
rsv = parse_rsv(r_xml)
|
30
|
+
query_nodes(rsv, batch)
|
31
|
+
query_node_users(rsv, batch)
|
32
|
+
|
33
|
+
rsv
|
34
|
+
end
|
35
|
+
|
36
|
+
# Queries the batch server for a list of reservations.
|
37
|
+
# @param batch [Batch] The batch server to query for the reservation.
|
38
|
+
# @return [Array<Reservation>]
|
39
|
+
# @raise [CommandLineError] if a command run from the shell exits with error
|
40
|
+
def query_reservations(batch)
|
41
|
+
cmd = "#{batch.mrsvctl} --host=#{batch.server} -q ALL --xml"
|
42
|
+
xml = get_xml(cmd, batch)
|
43
|
+
xml = filter(xml) # filter out "fake" reservations
|
44
|
+
|
45
|
+
rsv_list = []
|
46
|
+
xml.xpath("//rsv").each do |r_xml|
|
47
|
+
rsv_list << parse_rsv(r_xml)
|
48
|
+
end
|
49
|
+
query_nodes(rsv_list, batch)
|
50
|
+
query_node_users(rsv_list, batch)
|
51
|
+
|
52
|
+
rsv_list
|
53
|
+
end
|
54
|
+
|
55
|
+
# Submits a given reservation to the batch server.
|
56
|
+
# @param batch [Batch] The batch server to submit the reservation at.
|
57
|
+
# @param reservation [Reservation] A reservation with necessary information.
|
58
|
+
# @return [Reservation]
|
59
|
+
# @raise [CommandLineError] if a command run from the shell exits with error
|
60
|
+
def submit_reservation(batch, reservation)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Query the nodes from the batch server
|
66
|
+
# @param rsvs [Array<Reservation>] An array of reservations to check.
|
67
|
+
# @param batch [Batch] The batch server object to use.
|
68
|
+
# @return [Array<Reservation>] The array of reservations on this batch server.
|
69
|
+
def query_nodes(rsvs, batch)
|
70
|
+
# make list of unique nodes for all reservations
|
71
|
+
nodes = [*rsvs].map(&:nodes).flatten.uniq
|
72
|
+
|
73
|
+
c = PBS::Conn.batch(batch.torque_name)
|
74
|
+
q = PBS::Query.new(conn: c, type: :node)
|
75
|
+
node_list = {}
|
76
|
+
nodes.each do |n|
|
77
|
+
node_hash = q.find(id: n)[0][:attribs]
|
78
|
+
np = node_hash[:np].to_i
|
79
|
+
props = node_hash[:properties].split(',')
|
80
|
+
jobs = node_hash.fetch(:jobs, '').split(',').map{|j| j.split('/')[1]}
|
81
|
+
users = []
|
82
|
+
|
83
|
+
node_list[n] = Node.new(
|
84
|
+
id: n,
|
85
|
+
np: np,
|
86
|
+
props: props,
|
87
|
+
jobs: jobs,
|
88
|
+
users: users
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
# replace node lists with lists of Node objects
|
93
|
+
[*rsvs].each{|r| r.nodes.map!{|n| node_list[n]}}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Query what users are running jobs on the nodes for list of reservations
|
97
|
+
# @param rsvs [Array<Reservation>] An array of reservations to check.
|
98
|
+
# @param batch [Batch] The batch server object to use.
|
99
|
+
def query_node_users(rsvs, batch)
|
100
|
+
# make list of job ids
|
101
|
+
jobs = [*rsvs].map{|r| r.nodes.map(&:jobs)}.flatten.uniq
|
102
|
+
|
103
|
+
c = PBS::Conn.batch(batch.torque_name)
|
104
|
+
q = PBS::Query.new(conn: c, type: :job)
|
105
|
+
job_list = {}
|
106
|
+
jobs.each do |j|
|
107
|
+
job_list[j] = q.find(id: j)[0][:attribs][:Job_Owner].split('@')[0]
|
108
|
+
end
|
109
|
+
|
110
|
+
[*rsvs].each{|r| r.nodes.each{|n| n.users = n.jobs.map{|j| job_list[j]}.uniq}}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Error when executing from command line
|
114
|
+
class CommandLineError < StandardError; end
|
115
|
+
|
116
|
+
# Get reservation list as xml
|
117
|
+
# @param cmd [String] Command used in terminal to get XML reservation list.
|
118
|
+
# @param batch [Batch] The batch server object to use.
|
119
|
+
# @return [Nokogiri::XML::Document] The XML document corresponding to the queried reservation list.
|
120
|
+
def get_xml(cmd, batch)
|
121
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
122
|
+
exit_status = wait_thr.value
|
123
|
+
unless exit_status.success?
|
124
|
+
raise CommandLineError, "Bad exit status for: #{cmd}"
|
125
|
+
end
|
126
|
+
|
127
|
+
Nokogiri::XML(stdout.read)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Filter out unwanted reservations in a query
|
132
|
+
# @param xml [Nokogiri::XML::Document] The XML document to filter for this adapter.
|
133
|
+
# @return [Nokogiri::XML::Document] The filtered XML document.
|
134
|
+
def filter(xml)
|
135
|
+
# Remove running jobs
|
136
|
+
xml.xpath("//rsv[@SubType='JobReservation']").remove
|
137
|
+
|
138
|
+
# Remove debug queue from Glenn cluster
|
139
|
+
# xml.xpath("//rsv[@SubType='StandingReservation']").remove
|
140
|
+
|
141
|
+
xml
|
142
|
+
end
|
143
|
+
|
144
|
+
# Parse a reservation from a "rsv" element in XML
|
145
|
+
# @param xml [Nokogiri::XML::Element] The XML element corresponding to a reservation.
|
146
|
+
# @return [Reservation] The reservation object.
|
147
|
+
def parse_rsv(xml)
|
148
|
+
id = xml.xpath("@Name").to_s
|
149
|
+
starttime = Time.at(xml.xpath("@starttime").to_s.to_i)
|
150
|
+
endtime = Time.at(xml.xpath("@endtime").to_s.to_i)
|
151
|
+
auser = xml.xpath("@AUser").to_s
|
152
|
+
users = xml.xpath("ACL[@type='USER']/@name").map { |v| v.value }
|
153
|
+
nodes = xml.xpath("@AllocNodeList").to_s.split(',')
|
154
|
+
|
155
|
+
Reservation.new(
|
156
|
+
id: id,
|
157
|
+
starttime: starttime,
|
158
|
+
endtime: endtime,
|
159
|
+
auser: auser,
|
160
|
+
users: users,
|
161
|
+
nodes: nodes
|
162
|
+
)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|