train 2.1.7 → 2.1.13
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/lib/train.rb +20 -20
- data/lib/train/errors.rb +1 -1
- data/lib/train/extras.rb +2 -2
- data/lib/train/extras/command_wrapper.rb +24 -24
- data/lib/train/extras/stat.rb +27 -27
- data/lib/train/file.rb +30 -30
- data/lib/train/file/local.rb +8 -8
- data/lib/train/file/local/unix.rb +5 -5
- data/lib/train/file/local/windows.rb +1 -1
- data/lib/train/file/remote.rb +8 -8
- data/lib/train/file/remote/aix.rb +1 -1
- data/lib/train/file/remote/linux.rb +2 -2
- data/lib/train/file/remote/qnx.rb +8 -8
- data/lib/train/file/remote/unix.rb +10 -14
- data/lib/train/file/remote/windows.rb +5 -5
- data/lib/train/globals.rb +1 -1
- data/lib/train/options.rb +8 -8
- data/lib/train/platforms.rb +8 -8
- data/lib/train/platforms/common.rb +1 -1
- data/lib/train/platforms/detect/helpers/os_common.rb +36 -32
- data/lib/train/platforms/detect/helpers/os_linux.rb +12 -12
- data/lib/train/platforms/detect/helpers/os_windows.rb +27 -29
- data/lib/train/platforms/detect/scanner.rb +4 -4
- data/lib/train/platforms/detect/specifications/api.rb +8 -8
- data/lib/train/platforms/detect/specifications/os.rb +252 -252
- data/lib/train/platforms/detect/uuid.rb +5 -7
- data/lib/train/platforms/platform.rb +9 -5
- data/lib/train/plugin_test_helper.rb +12 -12
- data/lib/train/plugins.rb +5 -5
- data/lib/train/plugins/base_connection.rb +13 -13
- data/lib/train/plugins/transport.rb +7 -7
- data/lib/train/transports/azure.rb +23 -23
- data/lib/train/transports/cisco_ios_connection.rb +20 -20
- data/lib/train/transports/clients/azure/graph_rbac.rb +2 -2
- data/lib/train/transports/clients/azure/vault.rb +4 -4
- data/lib/train/transports/docker.rb +4 -10
- data/lib/train/transports/gcp.rb +23 -23
- data/lib/train/transports/helpers/azure/file_credentials.rb +8 -8
- data/lib/train/transports/helpers/azure/file_parser.rb +1 -1
- data/lib/train/transports/helpers/azure/subscription_number_file_parser.rb +1 -1
- data/lib/train/transports/local.rb +22 -22
- data/lib/train/transports/mock.rb +33 -35
- data/lib/train/transports/ssh.rb +47 -47
- data/lib/train/transports/ssh_connection.rb +28 -28
- data/lib/train/transports/vmware.rb +32 -34
- data/lib/train/transports/winrm.rb +37 -37
- data/lib/train/transports/winrm_connection.rb +12 -12
- data/lib/train/version.rb +1 -1
- metadata +2 -2
@@ -1,8 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require 'securerandom'
|
5
|
-
require 'json'
|
1
|
+
require "digest/sha1"
|
2
|
+
require "securerandom"
|
3
|
+
require "json"
|
6
4
|
|
7
5
|
module Train::Platforms::Detect
|
8
6
|
class UUID
|
@@ -24,10 +22,10 @@ module Train::Platforms::Detect
|
|
24
22
|
else
|
25
23
|
if @platform[:uuid_command]
|
26
24
|
result = @backend.run_command(@platform[:uuid_command])
|
27
|
-
return uuid_from_string(result.stdout.chomp) if result.exit_status
|
25
|
+
return uuid_from_string(result.stdout.chomp) if result.exit_status == 0 && !result.stdout.empty?
|
28
26
|
end
|
29
27
|
|
30
|
-
raise
|
28
|
+
raise "Could not find platform uuid! Please set a uuid_command for your platform."
|
31
29
|
end
|
32
30
|
end
|
33
31
|
end
|
@@ -44,7 +44,7 @@ module Train::Platforms
|
|
44
44
|
@cleaned_name = nil if force
|
45
45
|
@cleaned_name ||= begin
|
46
46
|
name = (@platform[:name] || @name)
|
47
|
-
name.downcase!.tr!(
|
47
|
+
name.downcase!.tr!(" ", "_") if name =~ /[A-Z ]/
|
48
48
|
name
|
49
49
|
end
|
50
50
|
end
|
@@ -59,7 +59,7 @@ module Train::Platforms
|
|
59
59
|
if respond_to?(name)
|
60
60
|
send(name)
|
61
61
|
else
|
62
|
-
|
62
|
+
"unknown"
|
63
63
|
end
|
64
64
|
end
|
65
65
|
|
@@ -73,6 +73,10 @@ module Train::Platforms
|
|
73
73
|
@platform
|
74
74
|
end
|
75
75
|
|
76
|
+
def cisco_ios? # TODO: kinda a hack. needed to prevent tests from corrupting.
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
76
80
|
# Add generic family? and platform methods to an existing platform
|
77
81
|
#
|
78
82
|
# This is done later to add any custom
|
@@ -84,8 +88,8 @@ module Train::Platforms
|
|
84
88
|
# Add in family methods
|
85
89
|
family_list = Train::Platforms.families
|
86
90
|
family_list.each_value do |k|
|
87
|
-
next if respond_to?(k.name +
|
88
|
-
define_singleton_method(k.name +
|
91
|
+
next if respond_to?(k.name + "?")
|
92
|
+
define_singleton_method(k.name + "?") do
|
89
93
|
family_hierarchy.include?(k.name)
|
90
94
|
end
|
91
95
|
end
|
@@ -99,7 +103,7 @@ module Train::Platforms
|
|
99
103
|
end
|
100
104
|
|
101
105
|
# Create method for name if its not already true
|
102
|
-
m = name +
|
106
|
+
m = name + "?"
|
103
107
|
return if respond_to?(m)
|
104
108
|
define_singleton_method(m) do
|
105
109
|
true
|
@@ -3,27 +3,27 @@
|
|
3
3
|
|
4
4
|
# Load Train. We certainly need the plugin system, and also several other parts
|
5
5
|
# that are tightly coupled. Train itself is fairly light, and non-invasive.
|
6
|
-
require
|
6
|
+
require "train"
|
7
7
|
|
8
8
|
# You can select from a number of test harnesses. Since Train is closely related
|
9
9
|
# to InSpec, and InSpec uses Spec-style controls in profile code, you will
|
10
10
|
# probably want to use something like minitest/spec, which provides Spec-style
|
11
11
|
# tests.
|
12
|
-
require
|
13
|
-
require
|
12
|
+
require "minitest/spec"
|
13
|
+
require "minitest/autorun"
|
14
14
|
|
15
15
|
# Data formats commonly used in testing
|
16
|
-
require
|
17
|
-
require
|
16
|
+
require "json"
|
17
|
+
require "ostruct"
|
18
18
|
|
19
19
|
# Utilities often needed
|
20
|
-
require
|
21
|
-
require
|
22
|
-
require
|
20
|
+
require "fileutils"
|
21
|
+
require "tmpdir"
|
22
|
+
require "pathname"
|
23
23
|
|
24
24
|
# You might want to put some debugging tools here. We run tests to find bugs,
|
25
25
|
# after all.
|
26
|
-
require
|
26
|
+
require "byebug"
|
27
27
|
|
28
28
|
# Configure MiniTest to expose things like `let`
|
29
29
|
class Module
|
@@ -38,11 +38,11 @@ module TrainPluginBaseHelper
|
|
38
38
|
plugin_test_helper_path = Pathname.new(caller_locations(4, 1).first.absolute_path)
|
39
39
|
plugin_src_root = plugin_test_helper_path.parent.parent
|
40
40
|
base.let(:plugin_src_path) { plugin_src_root }
|
41
|
-
base.let(:plugin_fixtures_path) { File.join(plugin_src_root,
|
41
|
+
base.let(:plugin_fixtures_path) { File.join(plugin_src_root, "test", "fixtures") }
|
42
42
|
end
|
43
43
|
|
44
|
-
let(:train_src_path) { File.expand_path(File.join(__FILE__,
|
45
|
-
let(:train_fixtures_path) { File.join(train_src_path,
|
44
|
+
let(:train_src_path) { File.expand_path(File.join(__FILE__, "..", "..")) }
|
45
|
+
let(:train_fixtures_path) { File.join(train_src_path, "test", "fixtures") }
|
46
46
|
let(:registry) { Train::Plugins.registry }
|
47
47
|
end
|
48
48
|
|
data/lib/train/plugins.rb
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
4
|
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
5
|
|
6
|
-
require
|
6
|
+
require "train/errors"
|
7
7
|
|
8
8
|
module Train
|
9
9
|
class Plugins
|
10
|
-
require
|
10
|
+
require "train/plugins/transport"
|
11
11
|
|
12
12
|
class << self
|
13
13
|
# Retrieve the current plugin registry, containing all plugin names
|
@@ -30,10 +30,10 @@ module Train
|
|
30
30
|
# @return [Transport] the versioned transport base class
|
31
31
|
def self.plugin(version = 1)
|
32
32
|
if version != 1
|
33
|
-
|
34
|
-
|
33
|
+
raise ClientError,
|
34
|
+
"Only understand train plugin version 1. You are trying to "\
|
35
35
|
"initialize a train plugin #{version}, which is not supported "\
|
36
|
-
|
36
|
+
"in the current release of train."
|
37
37
|
end
|
38
38
|
::Train::Plugins::Transport
|
39
39
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "train/errors"
|
4
|
+
require "train/extras"
|
5
|
+
require "train/file"
|
6
|
+
require "logger"
|
7
7
|
|
8
8
|
class Train::Plugins::Transport
|
9
9
|
# A Connection instance can be generated and re-generated, given new
|
@@ -21,7 +21,7 @@ class Train::Plugins::Transport
|
|
21
21
|
# @yield [self] yields itself for block-style invocation
|
22
22
|
def initialize(options = nil)
|
23
23
|
@options = options || {}
|
24
|
-
@logger = @options.delete(:logger) || Logger.new(
|
24
|
+
@logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal)
|
25
25
|
Train::Platforms::Detect::Specifications::OS.load
|
26
26
|
Train::Platforms::Detect::Specifications::Api.load
|
27
27
|
|
@@ -64,12 +64,12 @@ class Train::Plugins::Transport
|
|
64
64
|
# Enable caching types for Train. Currently we support
|
65
65
|
# :api_call, :file and :command types
|
66
66
|
def enable_cache(type)
|
67
|
-
|
67
|
+
raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
|
68
68
|
@cache_enabled[type.to_sym] = true
|
69
69
|
end
|
70
70
|
|
71
71
|
def disable_cache(type)
|
72
|
-
|
72
|
+
raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
|
73
73
|
@cache_enabled[type.to_sym] = false
|
74
74
|
clear_cache(type.to_sym)
|
75
75
|
end
|
@@ -81,13 +81,13 @@ class Train::Plugins::Transport
|
|
81
81
|
|
82
82
|
def to_json
|
83
83
|
{
|
84
|
-
|
84
|
+
"files" => Hash[@cache[:file].map { |x, y| [x, y.to_json] }],
|
85
85
|
}
|
86
86
|
end
|
87
87
|
|
88
88
|
def load_json(j)
|
89
|
-
require
|
90
|
-
j[
|
89
|
+
require "train/transports/mock"
|
90
|
+
j["files"].each do |path, jf|
|
91
91
|
@cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf)
|
92
92
|
end
|
93
93
|
end
|
@@ -137,7 +137,7 @@ class Train::Plugins::Transport
|
|
137
137
|
#
|
138
138
|
# @return [LoginCommand] array of command line tokens
|
139
139
|
def login_command
|
140
|
-
|
140
|
+
raise NotImplementedError, "#{self.class} does not implement #login_command()"
|
141
141
|
end
|
142
142
|
|
143
143
|
# Block and return only when the remote host is prepared and ready to
|
@@ -161,7 +161,7 @@ class Train::Plugins::Transport
|
|
161
161
|
#
|
162
162
|
# @return [CommandResult] contains the result of running the command
|
163
163
|
def run_command_via_connection(_command, &_data_handler)
|
164
|
-
|
164
|
+
raise NotImplementedError, "#{self.class} does not implement #run_command_via_connection()"
|
165
165
|
end
|
166
166
|
|
167
167
|
# Interact with files on the target. Read, write, and get metadata
|
@@ -170,7 +170,7 @@ class Train::Plugins::Transport
|
|
170
170
|
# @param [String] path which is being inspected
|
171
171
|
# @return [FileCommon] file object that allows for interaction
|
172
172
|
def file_via_connection(_path, *_args)
|
173
|
-
|
173
|
+
raise NotImplementedError, "#{self.class} does not implement #file_via_connection(...)"
|
174
174
|
end
|
175
175
|
|
176
176
|
def clear_cache(type)
|
@@ -3,17 +3,17 @@
|
|
3
3
|
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
4
|
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
6
|
+
require "logger"
|
7
|
+
require "train/errors"
|
8
|
+
require "train/extras"
|
9
|
+
require "train/options"
|
10
10
|
|
11
11
|
class Train::Plugins
|
12
12
|
class Transport
|
13
13
|
include Train::Extras
|
14
14
|
Train::Options.attach(self)
|
15
15
|
|
16
|
-
require
|
16
|
+
require "train/plugins/base_connection"
|
17
17
|
|
18
18
|
# Initialize a new Transport object
|
19
19
|
#
|
@@ -21,7 +21,7 @@ class Train::Plugins
|
|
21
21
|
# @return [Transport] the transport object
|
22
22
|
def initialize(options = {})
|
23
23
|
@options = merge_options({}, options || {})
|
24
|
-
@logger = @options[:logger] || Logger.new(
|
24
|
+
@logger = @options[:logger] || Logger.new($stdout, level: :fatal)
|
25
25
|
end
|
26
26
|
|
27
27
|
# Create a connection to the target. Options may be provided
|
@@ -30,7 +30,7 @@ class Train::Plugins
|
|
30
30
|
# @param [Hash] _options = nil provide optional configuration params
|
31
31
|
# @return [Connection] the connection for this configuration
|
32
32
|
def connection(_options = nil)
|
33
|
-
|
33
|
+
raise Train::ClientError, "#{self.class} does not implement #connection()"
|
34
34
|
end
|
35
35
|
|
36
36
|
# Register the inheriting class with as a train plugin using the
|
@@ -1,27 +1,27 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
3
|
+
require "train/plugins"
|
4
|
+
require "ms_rest_azure"
|
5
|
+
require "azure_mgmt_resources"
|
6
|
+
require "azure_graph_rbac"
|
7
|
+
require "azure_mgmt_key_vault"
|
8
|
+
require "socket"
|
9
|
+
require "timeout"
|
10
|
+
require "train/transports/helpers/azure/file_credentials"
|
11
|
+
require "train/transports/clients/azure/graph_rbac"
|
12
|
+
require "train/transports/clients/azure/vault"
|
13
13
|
|
14
14
|
module Train::Transports
|
15
15
|
class Azure < Train.plugin(1)
|
16
|
-
name
|
17
|
-
option :tenant_id, default: ENV[
|
18
|
-
option :client_id, default: ENV[
|
19
|
-
option :client_secret, default: ENV[
|
20
|
-
option :subscription_id, default: ENV[
|
21
|
-
option :msi_port, default: ENV[
|
16
|
+
name "azure"
|
17
|
+
option :tenant_id, default: ENV["AZURE_TENANT_ID"]
|
18
|
+
option :client_id, default: ENV["AZURE_CLIENT_ID"]
|
19
|
+
option :client_secret, default: ENV["AZURE_CLIENT_SECRET"]
|
20
|
+
option :subscription_id, default: ENV["AZURE_SUBSCRIPTION_ID"]
|
21
|
+
option :msi_port, default: ENV["AZURE_MSI_PORT"] || "50342"
|
22
22
|
|
23
23
|
# This can provide the client id and secret
|
24
|
-
option :credentials_file, default: ENV[
|
24
|
+
option :credentials_file, default: ENV["AZURE_CRED_FILE"]
|
25
25
|
|
26
26
|
def connection(_ = nil)
|
27
27
|
@connection ||= Connection.new(@options)
|
@@ -30,7 +30,7 @@ module Train::Transports
|
|
30
30
|
class Connection < BaseConnection
|
31
31
|
attr_reader :options
|
32
32
|
|
33
|
-
DEFAULT_FILE = ::File.join(Dir.home,
|
33
|
+
DEFAULT_FILE = ::File.join(Dir.home, ".azure", "credentials")
|
34
34
|
|
35
35
|
def initialize(options)
|
36
36
|
@apis = {}
|
@@ -51,14 +51,14 @@ module Train::Transports
|
|
51
51
|
@options[:msi_port] = @options[:msi_port].to_i unless @options[:msi_port].nil?
|
52
52
|
|
53
53
|
# additional platform details
|
54
|
-
release = Gem.loaded_specs[
|
54
|
+
release = Gem.loaded_specs["azure_mgmt_resources"].version
|
55
55
|
@platform_details = { release: "azure_mgmt_resources-v#{release}" }
|
56
56
|
|
57
57
|
connect
|
58
58
|
end
|
59
59
|
|
60
60
|
def platform
|
61
|
-
force_platform!(
|
61
|
+
force_platform!("azure", @platform_details)
|
62
62
|
end
|
63
63
|
|
64
64
|
def azure_client(klass = ::Azure::Resources::Profiles::Latest::Mgmt::Client, opts = {})
|
@@ -85,13 +85,13 @@ module Train::Transports
|
|
85
85
|
def connect
|
86
86
|
if msi_auth?
|
87
87
|
# this needs set for azure cloud to authenticate
|
88
|
-
ENV[
|
88
|
+
ENV["MSI_VM"] = "true"
|
89
89
|
provider = ::MsRestAzure::MSITokenProvider.new(@options[:msi_port])
|
90
90
|
else
|
91
91
|
provider = ::MsRestAzure::ApplicationTokenProvider.new(
|
92
92
|
@options[:tenant_id],
|
93
93
|
@options[:client_id],
|
94
|
-
@options[:client_secret]
|
94
|
+
@options[:client_secret]
|
95
95
|
)
|
96
96
|
end
|
97
97
|
|
@@ -166,7 +166,7 @@ module Train::Transports
|
|
166
166
|
def port_open?(port, seconds = 3)
|
167
167
|
Timeout.timeout(seconds) do
|
168
168
|
begin
|
169
|
-
TCPSocket.new(
|
169
|
+
TCPSocket.new("localhost", port).close
|
170
170
|
true
|
171
171
|
rescue SystemCallError
|
172
172
|
false
|
@@ -24,8 +24,8 @@ class Train::Transports::SSH
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def unique_identifier
|
27
|
-
result = run_command_via_connection(
|
28
|
-
result.stdout.split(
|
27
|
+
result = run_command_via_connection("show version | include Processor")
|
28
|
+
result.stdout.split(" ")[-1]
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
@@ -45,26 +45,26 @@ class Train::Transports::SSH
|
|
45
45
|
if @enable_password
|
46
46
|
# This verifies we are not in privileged exec mode before running the
|
47
47
|
# enable command. Otherwise, the password will be in history.
|
48
|
-
if run_command_via_connection(
|
48
|
+
if run_command_via_connection("show privilege").stdout.split[-1] != "15"
|
49
49
|
# Extra newlines to get back to prompt if incorrect password is used
|
50
50
|
run_command_via_connection("enable\n#{@enable_password}\n\n\n")
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
54
|
# Prevent `--MORE--` by removing terminal length limit
|
55
|
-
run_command_via_connection(
|
55
|
+
run_command_via_connection("terminal length 0")
|
56
56
|
|
57
57
|
@session
|
58
58
|
end
|
59
59
|
|
60
60
|
def run_command_via_connection(cmd, &_data_handler)
|
61
61
|
# Ensure buffer is empty before sending data
|
62
|
-
@buf =
|
62
|
+
@buf = ""
|
63
63
|
|
64
64
|
logger.debug("[SSH] Running `#{cmd}` on #{self}")
|
65
65
|
session.send_data(cmd + "\r\n")
|
66
66
|
|
67
|
-
logger.debug(
|
67
|
+
logger.debug("[SSH] waiting for prompt")
|
68
68
|
until @buf =~ @prompt
|
69
69
|
if @buf =~ /Bad (secrets|password)|Access denied/
|
70
70
|
raise BadEnablePassword
|
@@ -74,16 +74,16 @@ class Train::Transports::SSH
|
|
74
74
|
|
75
75
|
# Save the buffer and clear it for the next command
|
76
76
|
output = @buf.dup
|
77
|
-
@buf =
|
77
|
+
@buf = ""
|
78
78
|
|
79
79
|
format_result(format_output(output, cmd))
|
80
80
|
end
|
81
81
|
|
82
82
|
ERROR_MATCHERS = [
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
83
|
+
"Bad IP address",
|
84
|
+
"Incomplete command",
|
85
|
+
"Invalid input detected",
|
86
|
+
"Unrecognized host",
|
87
87
|
].freeze
|
88
88
|
|
89
89
|
# IOS commands do not have an exit code so we must compare the command
|
@@ -92,9 +92,9 @@ class Train::Transports::SSH
|
|
92
92
|
# result.
|
93
93
|
def format_result(result)
|
94
94
|
if ERROR_MATCHERS.none? { |e| result.include?(e) }
|
95
|
-
CommandResult.new(result,
|
95
|
+
CommandResult.new(result, "", 0)
|
96
96
|
else
|
97
|
-
CommandResult.new(
|
97
|
+
CommandResult.new("", result, 1)
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
@@ -107,10 +107,10 @@ class Train::Transports::SSH
|
|
107
107
|
trailing_line_endings = /(\r\n)+$/
|
108
108
|
|
109
109
|
output
|
110
|
-
.sub(leading_prompt,
|
111
|
-
.sub(command_string,
|
112
|
-
.gsub(trailing_prompt,
|
113
|
-
.gsub(trailing_line_endings,
|
110
|
+
.sub(leading_prompt, "")
|
111
|
+
.sub(command_string, "")
|
112
|
+
.gsub(trailing_prompt, "")
|
113
|
+
.gsub(trailing_line_endings, "")
|
114
114
|
end
|
115
115
|
|
116
116
|
# Create an SSH channel that writes to @buf when data is received
|
@@ -121,9 +121,9 @@ class Train::Transports::SSH
|
|
121
121
|
@buf += data
|
122
122
|
end
|
123
123
|
|
124
|
-
ch.send_channel_request(
|
125
|
-
raise
|
126
|
-
logger.debug(
|
124
|
+
ch.send_channel_request("shell") do |_, success|
|
125
|
+
raise "Failed to open SSH shell" unless success
|
126
|
+
logger.debug("[SSH] shell opened")
|
127
127
|
end
|
128
128
|
end
|
129
129
|
end
|