ood_appkit 0.3.6 → 1.0.0
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 +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +5 -105
- data/lib/ood_appkit/configuration.rb +15 -13
- data/lib/ood_appkit/version.rb +1 -1
- data/lib/ood_appkit.rb +0 -9
- metadata +7 -24
- data/lib/ood_appkit/cluster_decorator.rb +0 -46
- data/lib/ood_appkit/clusters.rb +0 -24
- data/lib/ood_appkit/config_parser.rb +0 -45
- data/lib/ood_appkit/validator.rb +0 -25
- data/lib/ood_appkit/validators/groups.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 639be3508b4fec095c35a5d4975b786d1ec8b4dd
|
4
|
+
data.tar.gz: 772b0ddc945ae9a38247c68fde45a611627b08f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00d0a0e9c72158520b4a7fa8bf7350134e321fb32fb3baea420d74ec5b912b32988e052c37231aec5bed0585ce8a1d0337e8bf46855982c69dd698b52eff40f2
|
7
|
+
data.tar.gz: 62d4891cac215848b39c1668582360ebe2749edfa224db14453c139d85bf0db61d2cf5b57f0013fbad2fe6130895df5952c146ee0b54d22fe86f418d34d4b880
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -525,11 +525,7 @@ host is done through:
|
|
525
525
|
```ruby
|
526
526
|
# An enumerable list of clusters
|
527
527
|
OodAppkit.clusters
|
528
|
-
#=> #<
|
529
|
-
|
530
|
-
# Create list of cluster titles
|
531
|
-
OodAppkit.clusters.map(&:title)
|
532
|
-
#=> ["My Cluster", "Tiny Cluster", "Big Cluster"]
|
528
|
+
#=> #<OodCore::Clusters>
|
533
529
|
|
534
530
|
# Count number of clusters available
|
535
531
|
OodAppkit.clusters.count
|
@@ -545,114 +541,18 @@ You can access a given cluster with id `my_cluster` by:
|
|
545
541
|
```ruby
|
546
542
|
# Get object describing my HPC center's `my_cluster`
|
547
543
|
OodAppkit.clusters[:my_cluster]
|
548
|
-
#=> #<
|
544
|
+
#=> #<OodCore::Cluster>
|
549
545
|
|
550
546
|
# or...
|
551
547
|
OodAppkit.clusters["my_cluster"]
|
552
|
-
#=> #<
|
548
|
+
#=> #<OodCore::Cluster>
|
553
549
|
|
554
550
|
# Trying to access a non-existant cluster
|
555
551
|
OodAppkit.clusters[:invalid_cluster]
|
556
552
|
#=> nil
|
557
553
|
```
|
558
554
|
|
559
|
-
|
560
|
-
the cluster that meets your needs:
|
561
|
-
|
562
|
-
```ruby
|
563
|
-
# Get the cluster of our choosing
|
564
|
-
my_cluster = OodAppkit.clusters[:my_cluster]
|
565
|
-
|
566
|
-
# Is this cluster valid (can I the user access the servers provided by it?)
|
567
|
-
my_cluster.valid?
|
568
|
-
#=> true
|
569
|
-
|
570
|
-
# Is this cluster considered a High Performance Computing (hpc) cluster?
|
571
|
-
# NB: A low performance computing cluster expects jobs that request a single
|
572
|
-
# core and use minimal resources (e.g., a desktop for file browsing/editing,
|
573
|
-
# a web server that submits jobs to an HPC cluster, visualization software)
|
574
|
-
my_cluster.hpc_cluster?
|
575
|
-
#=> false
|
576
|
-
|
577
|
-
# ID of cluster object to find again in OodAppkit.clusters
|
578
|
-
my_cluster.id #=> :my_cluster
|
579
|
-
|
580
|
-
# URL of cluster
|
581
|
-
my_cluster.url #=> "https://hpc.center.edu/clusters/my_cluster"
|
582
|
-
|
583
|
-
# Check if it has a login server
|
584
|
-
my_cluster.login_server? #=> true
|
585
|
-
|
586
|
-
# Access login server object
|
587
|
-
my_cluster.login_server
|
588
|
-
#=> #<OodCluster::Servers::Ssh>
|
589
|
-
```
|
590
|
-
|
591
|
-
As this object is an `Enumerable` you can create subsets of clusters that your
|
592
|
-
app only cares for in an initializer:
|
593
|
-
|
594
|
-
```ruby
|
595
|
-
# I only want clusters that are valid for the currently running user
|
596
|
-
valid_clusters = OodAppkit::Clusters.new(
|
597
|
-
OodAppkit.clusters.select(&:valid?)
|
598
|
-
)
|
599
|
-
#=> #<OodAppkit::Clusters>
|
600
|
-
|
601
|
-
# Create list of cluster titles from these valid clusters
|
602
|
-
valid_clusters.map(&:title)
|
603
|
-
#=> ["My Cluster"]
|
604
|
-
|
605
|
-
# I only want HPC clusters that I can submit solver jobs to
|
606
|
-
hpc_clusters = OodAppkit::Clusters.new(
|
607
|
-
OodAppkit.clusters.select(&:hpc_cluster?)
|
608
|
-
)
|
609
|
-
```
|
610
|
-
|
611
|
-
Depending on the type of server chosen, different helper methods will be
|
612
|
-
available to the developer. You can find more details on this at
|
613
|
-
https://github.com/OSC/ood_cluster.
|
614
|
-
|
615
|
-
#### Validations
|
616
|
-
|
617
|
-
A cluster may support more validations than whether the current user can access
|
618
|
-
it. The extra validations are defined in the configuration YAML file as such:
|
619
|
-
|
620
|
-
```yaml
|
621
|
-
validators:
|
622
|
-
cluster:
|
623
|
-
- type: "OodAppkit::Validators::Groups"
|
624
|
-
data:
|
625
|
-
groups:
|
626
|
-
- "ruby"
|
627
|
-
allow: true
|
628
|
-
rsv_query:
|
629
|
-
- type: "OodAppkit::Validators::Groups"
|
630
|
-
data:
|
631
|
-
groups:
|
632
|
-
- "sysp"
|
633
|
-
- "hpcsoft"
|
634
|
-
allow: false
|
635
|
-
```
|
636
|
-
|
637
|
-
where the key is used in the `OodAppkit::ClusterDecorator#valid?` method.
|
638
|
-
|
639
|
-
One such validation is whether the current user can query their reservation
|
640
|
-
information for the given cluster:
|
641
|
-
|
642
|
-
```ruby
|
643
|
-
# Check if reservation query object is valid (am I allowed to use it)
|
644
|
-
# NB: Definitely use this as some users may be privileged and have access to
|
645
|
-
# view ALL reservations. This can cause the app to hang.
|
646
|
-
my_cluster.valid?(:rsv_query) #=> true
|
647
|
-
|
648
|
-
# I am allowed to use it so let's query for user's reservations
|
649
|
-
require 'ood_reservations'
|
650
|
-
my_rsv_query = OodReservations::Query.build(cluster: my_cluster).reservations
|
651
|
-
#=> [ #<OodReservations::Reservation>, ... ]
|
652
|
-
```
|
653
|
-
|
654
|
-
You can learn more about the reservation query object by visiting
|
655
|
-
https://github.com/OSC/ood_reservations.
|
555
|
+
You can read more about this cluster object at https://github.com/OSC/ood_core.
|
656
556
|
|
657
557
|
#### Configuration
|
658
558
|
|
@@ -663,7 +563,7 @@ different config file through the environment variable `OOD_CLUSTERS`
|
|
663
563
|
OOD_CLUSTERS="/path/to/my/config.yml"
|
664
564
|
```
|
665
565
|
|
666
|
-
Or a directory with
|
566
|
+
Or a directory with cluster named configuration files (name of file is id of
|
667
567
|
cluster):
|
668
568
|
|
669
569
|
```sh
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'redcarpet'
|
2
2
|
require 'ostruct'
|
3
|
+
require 'ood_core'
|
3
4
|
|
4
5
|
module OodAppkit
|
5
6
|
# An object that stores and adds configuration options.
|
@@ -12,8 +13,12 @@ module OodAppkit
|
|
12
13
|
attr_writer :dataroot
|
13
14
|
|
14
15
|
# Cluster information for local HPC center
|
15
|
-
# @return [
|
16
|
-
|
16
|
+
# @return [OodCore::Clusters] hash of available clusters
|
17
|
+
def clusters
|
18
|
+
@clusters ||= parse_clusters(ENV['OOD_CLUSTERS'])
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_writer :clusters
|
17
22
|
|
18
23
|
# A markdown renderer used when rendering `*.md` or `*.markdown` views
|
19
24
|
# @return [Redcarpet::Markdown] the markdown renderer used
|
@@ -65,17 +70,6 @@ module OodAppkit
|
|
65
70
|
self.dataroot = ENV['OOD_DATAROOT'] || ENV['RAILS_DATAROOT']
|
66
71
|
self.dataroot ||= "~/#{ENV['OOD_PORTAL'] || "ondemand"}/data/#{ENV['APP_TOKEN']}" if ENV['APP_TOKEN']
|
67
72
|
|
68
|
-
# Initialize list of available clusters
|
69
|
-
c_config = Pathname.new(ENV['OOD_CLUSTERS'] || '/etc/ood/config/clusters.d')
|
70
|
-
self.clusters = Clusters.new(
|
71
|
-
begin
|
72
|
-
ConfigParser.parse(config: c_config).map { |k, v| ClusterDecorator.new(id: k.to_sym, **v) }
|
73
|
-
rescue ConfigParser::InvalidConfigPath
|
74
|
-
STDERR.puts "Invalid cluster config specified: #{c_config}"
|
75
|
-
[]
|
76
|
-
end
|
77
|
-
)
|
78
|
-
|
79
73
|
# Add markdown template support
|
80
74
|
self.markdown = Redcarpet::Markdown.new(
|
81
75
|
Redcarpet::Render::HTML,
|
@@ -127,5 +121,13 @@ module OodAppkit
|
|
127
121
|
|
128
122
|
self.enable_log_formatter = ::Rails.env.production?
|
129
123
|
end
|
124
|
+
|
125
|
+
private
|
126
|
+
# Read in cluster config and parse it
|
127
|
+
def parse_clusters(config)
|
128
|
+
OodCore::Clusters.load_file(config || '/etc/ood/config/clusters.d')
|
129
|
+
rescue OodCore::ConfigurationNotFound
|
130
|
+
OodCore::Clusters.new([])
|
131
|
+
end
|
130
132
|
end
|
131
133
|
end
|
data/lib/ood_appkit/version.rb
CHANGED
data/lib/ood_appkit.rb
CHANGED
@@ -4,10 +4,6 @@ require 'ood_appkit/url'
|
|
4
4
|
require 'ood_appkit/files_rack_app'
|
5
5
|
require 'ood_appkit/markdown_template_handler'
|
6
6
|
require 'ood_appkit/log_formatter'
|
7
|
-
require 'ood_appkit/config_parser'
|
8
|
-
require 'ood_appkit/cluster_decorator'
|
9
|
-
require 'ood_appkit/clusters'
|
10
|
-
require 'ood_appkit/validator'
|
11
7
|
|
12
8
|
# The main namespace for OodAppkit. Provides a global configuration.
|
13
9
|
module OodAppkit
|
@@ -22,9 +18,4 @@ module OodAppkit
|
|
22
18
|
require 'ood_appkit/urls/files'
|
23
19
|
require 'ood_appkit/urls/editor'
|
24
20
|
end
|
25
|
-
|
26
|
-
# A namespace for validators used to validate a cluster
|
27
|
-
module Validators
|
28
|
-
require 'ood_appkit/validators/groups'
|
29
|
-
end
|
30
21
|
end
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ood_appkit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Franz
|
8
|
+
- Jeremy Nicklas
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2017-04-
|
12
|
+
date: 2017-04-17 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rails
|
@@ -31,33 +32,19 @@ dependencies:
|
|
31
32
|
- !ruby/object:Gem::Version
|
32
33
|
version: 4.0.7
|
33
34
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
35
|
+
name: ood_core
|
35
36
|
requirement: !ruby/object:Gem::Requirement
|
36
37
|
requirements:
|
37
38
|
- - "~>"
|
38
39
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
40
|
+
version: 0.0.1
|
40
41
|
type: :runtime
|
41
42
|
prerelease: false
|
42
43
|
version_requirements: !ruby/object:Gem::Requirement
|
43
44
|
requirements:
|
44
45
|
- - "~>"
|
45
46
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
47
|
-
- !ruby/object:Gem::Dependency
|
48
|
-
name: ood_cluster
|
49
|
-
requirement: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - "~>"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0.0'
|
54
|
-
type: :runtime
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - "~>"
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '0.0'
|
47
|
+
version: 0.0.1
|
61
48
|
- !ruby/object:Gem::Dependency
|
62
49
|
name: addressable
|
63
50
|
requirement: !ruby/object:Gem::Requirement
|
@@ -119,6 +106,7 @@ description: Provides an interface to working with other Open OnDemand (OOD) app
|
|
119
106
|
objects for providing branding and documentation within the apps.
|
120
107
|
email:
|
121
108
|
- efranz@osc.edu
|
109
|
+
- jnicklas@osc.edu
|
122
110
|
executables: []
|
123
111
|
extensions: []
|
124
112
|
extra_rdoc_files: []
|
@@ -136,9 +124,6 @@ files:
|
|
136
124
|
- app/views/ood_appkit/wiki/show.html.erb
|
137
125
|
- config/routes.rb
|
138
126
|
- lib/ood_appkit.rb
|
139
|
-
- lib/ood_appkit/cluster_decorator.rb
|
140
|
-
- lib/ood_appkit/clusters.rb
|
141
|
-
- lib/ood_appkit/config_parser.rb
|
142
127
|
- lib/ood_appkit/configuration.rb
|
143
128
|
- lib/ood_appkit/engine.rb
|
144
129
|
- lib/ood_appkit/files_rack_app.rb
|
@@ -150,8 +135,6 @@ files:
|
|
150
135
|
- lib/ood_appkit/urls/files.rb
|
151
136
|
- lib/ood_appkit/urls/public.rb
|
152
137
|
- lib/ood_appkit/urls/shell.rb
|
153
|
-
- lib/ood_appkit/validator.rb
|
154
|
-
- lib/ood_appkit/validators/groups.rb
|
155
138
|
- lib/ood_appkit/version.rb
|
156
139
|
- lib/tasks/ood_appkit_tasks.rake
|
157
140
|
- test/dummy/README.rdoc
|
@@ -1,46 +0,0 @@
|
|
1
|
-
require 'ood_cluster'
|
2
|
-
|
3
|
-
module OodAppkit
|
4
|
-
# A decorator that adds a presentation layer to the {OodCluster::Cluster} object
|
5
|
-
class ClusterDecorator < SimpleDelegator
|
6
|
-
# The identifier used as the key in the OodAppkit clusters hash (used for
|
7
|
-
# reverse searching)
|
8
|
-
# @return [Symbol] the clusters hash key for this object
|
9
|
-
attr_reader :id
|
10
|
-
|
11
|
-
# The title used to describe this cluster to users
|
12
|
-
# @return [String] the cluster title
|
13
|
-
attr_reader :title
|
14
|
-
|
15
|
-
# The URL for this cluster that users can use to view more information
|
16
|
-
# @return [String] the cluster url
|
17
|
-
attr_reader :url
|
18
|
-
|
19
|
-
# @param cluster [OodCluster::Cluster] cluster object
|
20
|
-
# @param id [#to_sym] id used in clusters hash of OodAppkit
|
21
|
-
# @param title [#to_s] title of cluster
|
22
|
-
# @param url [#to_s] url of cluster
|
23
|
-
# @param validators [Hash{#to_sym=>Array<Validator>}] hash of validators
|
24
|
-
def initialize(cluster:, id:, title: "", url: "", validators: {}, **_)
|
25
|
-
super(cluster)
|
26
|
-
@id = id.to_sym
|
27
|
-
@title = title.to_s
|
28
|
-
@url = url.to_s
|
29
|
-
@validators = validators.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Whether the given method is valid (i.e., passes all supplied validators)
|
33
|
-
# @param method [#to_sym] method to check if valid
|
34
|
-
# @return [Boolean] whether this method is valid
|
35
|
-
def valid?(method = :cluster)
|
36
|
-
@validators.fetch(method.to_sym, []).all? { |v| v.success? }
|
37
|
-
end
|
38
|
-
|
39
|
-
# The comparison operator
|
40
|
-
# @param other [#to_sym] object to compare against
|
41
|
-
# @return [Boolean] whether objects are equivalent
|
42
|
-
def ==(other)
|
43
|
-
id == other.to_sym
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
data/lib/ood_appkit/clusters.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module OodAppkit
|
2
|
-
# An enumerable that holds a list of {ClusterDecorator} objects
|
3
|
-
class Clusters
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
# @param clusters [Array<ClusterDecorator>] list of cluster decorator objects
|
7
|
-
def initialize(clusters = [])
|
8
|
-
@clusters = clusters
|
9
|
-
end
|
10
|
-
|
11
|
-
# Find object in list from object's id
|
12
|
-
# @param id [Object] id of cluster decorator object
|
13
|
-
# @return [ClusterDecorator, nil] object if found
|
14
|
-
def [](id)
|
15
|
-
@clusters.detect { |v| v == id }
|
16
|
-
end
|
17
|
-
|
18
|
-
# Iterate over each of the clusters
|
19
|
-
# @yield [obj] cluster decorator objects
|
20
|
-
def each(&block)
|
21
|
-
@clusters.each(&block)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
require 'json'
|
3
|
-
|
4
|
-
module OodAppkit
|
5
|
-
# Helper methods for parsing/deserializing yml configuration files
|
6
|
-
module ConfigParser
|
7
|
-
# Specific version hash in the yaml file to parse
|
8
|
-
YAML_VERSION = 'v1'
|
9
|
-
|
10
|
-
# Identifier used to distinguish class names when deserializing
|
11
|
-
CLASS_ID = 'type'
|
12
|
-
|
13
|
-
# Exception raise when unable to access config file or directory
|
14
|
-
class InvalidConfigPath < StandardError; end
|
15
|
-
|
16
|
-
# Parse/deserialize a configuration file or a set of configuration files
|
17
|
-
# @param config [#to_s] configuration file or directory
|
18
|
-
# @raise [InvalidConfigPath] if config path is inaccessible
|
19
|
-
# @return [Hash] hash of deserialized config file
|
20
|
-
def self.parse(config:)
|
21
|
-
# use 'type' to distinguish class name in yaml file
|
22
|
-
JSON.create_id = CLASS_ID
|
23
|
-
|
24
|
-
config = Pathname.new(config.to_s).expand_path
|
25
|
-
if config.file?
|
26
|
-
parse_file config
|
27
|
-
elsif config.directory?
|
28
|
-
config.children.each_with_object({}) do |f, h|
|
29
|
-
/^(.+)\.yml$/.match(f.basename.to_s) do
|
30
|
-
hsh = parse_file(f)
|
31
|
-
h[$1.to_sym] = hsh unless hsh.empty?
|
32
|
-
end
|
33
|
-
end
|
34
|
-
else
|
35
|
-
raise InvalidConfigPath, "invalid config path: #{config}"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
# Parse a single yaml file
|
41
|
-
def self.parse_file(file)
|
42
|
-
JSON.load(JSON.dump(YAML.load(File.read(file.to_s)).fetch(YAML_VERSION, {}))).deep_symbolize_keys
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
data/lib/ood_appkit/validator.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
module OodAppkit
|
2
|
-
class Validator
|
3
|
-
# Deserialize a validator object from JSON
|
4
|
-
# @param object [Hash{#to_sym=>Object}] hash used defining context
|
5
|
-
# @return [self] deserialized object
|
6
|
-
def self.json_create(object)
|
7
|
-
new object["data"].each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
8
|
-
end
|
9
|
-
|
10
|
-
def initialize(**_)
|
11
|
-
end
|
12
|
-
|
13
|
-
# Whether this validation was successful
|
14
|
-
# @return [Boolean] whether successful
|
15
|
-
def success?
|
16
|
-
true
|
17
|
-
end
|
18
|
-
|
19
|
-
# Whether this validation was a failure
|
20
|
-
# @return [Boolean] whether failure
|
21
|
-
def failure?
|
22
|
-
!success?
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'ood_support'
|
2
|
-
|
3
|
-
module OodAppkit
|
4
|
-
module Validators
|
5
|
-
# Class used to determine if user is in valid list of groups
|
6
|
-
class Groups < Validator
|
7
|
-
# @param groups [Array<#to_s>] list of groups
|
8
|
-
# @param allow [Boolean] whether these groups are allowed access
|
9
|
-
def initialize(groups: [], allow: true, **kwargs)
|
10
|
-
super(kwargs)
|
11
|
-
@groups = groups.map(&:to_s)
|
12
|
-
@allow = allow
|
13
|
-
end
|
14
|
-
|
15
|
-
# Whether this validation was successful
|
16
|
-
# @return [Boolean] whether successful
|
17
|
-
def success?
|
18
|
-
@allow ? in_user_groups?(@groups) : not_in_user_groups?(@groups)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
# List of groups user is in
|
23
|
-
def user_groups
|
24
|
-
OodSupport::User.new.groups.map(&:to_s)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Whether any groups match user's groups
|
28
|
-
def in_user_groups?(groups)
|
29
|
-
!(groups & user_groups).empty?
|
30
|
-
end
|
31
|
-
|
32
|
-
# Whether groups don't correspond with any user group
|
33
|
-
def not_in_user_groups?(groups)
|
34
|
-
!in_user_groups?(groups)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|