xolo-admin 1.0.0 → 1.0.1
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/README.md +40 -3
- data/bin/xadm +1 -1
- data/data/client/xolo +1160 -0
- data/lib/optimist_with_insert_blanks.rb +1216 -0
- data/lib/xolo/admin/configuration.rb +16 -0
- data/lib/xolo/admin/connection.rb +14 -3
- data/lib/xolo/admin/interactive.rb +3 -1
- data/lib/xolo/admin/processing.rb +2 -6
- data/lib/xolo/admin/title.rb +2 -0
- data/lib/xolo/admin/validate.rb +3 -3
- data/lib/xolo/admin/version.rb +2 -0
- data/lib/xolo/core/base_classes/configuration.rb +238 -0
- data/lib/xolo/core/base_classes/server_object.rb +112 -0
- data/lib/xolo/core/base_classes/title.rb +648 -0
- data/lib/xolo/core/base_classes/version.rb +601 -0
- data/lib/xolo/core/constants.rb +81 -0
- data/lib/xolo/core/exceptions.rb +52 -0
- data/lib/xolo/core/json_wrappers.rb +43 -0
- data/lib/xolo/core/loading.rb +59 -0
- data/lib/xolo/core/output.rb +292 -0
- data/lib/xolo/core/version.rb +21 -0
- data/lib/xolo/core.rb +46 -0
- metadata +21 -10
|
@@ -94,6 +94,22 @@ module Xolo
|
|
|
94
94
|
ENDDESC
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
+
# @!attribute pw
|
|
98
|
+
# @return [String]
|
|
99
|
+
ssl_verify: {
|
|
100
|
+
label: 'Verify SSL Cert',
|
|
101
|
+
type: :boolean,
|
|
102
|
+
invalid_msg: '',
|
|
103
|
+
validate: :validate_boolean,
|
|
104
|
+
default: true,
|
|
105
|
+
desc: <<~ENDDESC
|
|
106
|
+
If your Xolo server is using a self-signed SSL certificate,
|
|
107
|
+
set this to false, to disable SSL verification.
|
|
108
|
+
|
|
109
|
+
Defaults to true, which is recommended for production servers.
|
|
110
|
+
ENDDESC
|
|
111
|
+
},
|
|
112
|
+
|
|
97
113
|
# @!attribute pw
|
|
98
114
|
# @return [String]
|
|
99
115
|
no_gui: {
|
|
@@ -79,6 +79,17 @@ module Xolo
|
|
|
79
79
|
raise Xolo::AuthenticationError, 'Invalid username or password'
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# @return [Hash] the SSL options for Faraday connections
|
|
83
|
+
##################
|
|
84
|
+
def ssl_opts
|
|
85
|
+
# true if nil or true, false if false
|
|
86
|
+
verify = config.ssl_verify.nil? || config.ssl_verify
|
|
87
|
+
|
|
88
|
+
{
|
|
89
|
+
verify: verify
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
82
93
|
# @pararm host [String] an alternate hostname to use, defaults to config.hostname
|
|
83
94
|
#
|
|
84
95
|
# @return [URI] The server base URL
|
|
@@ -107,7 +118,7 @@ module Xolo
|
|
|
107
118
|
@server_cnx = nil if host
|
|
108
119
|
return @server_cnx if @server_cnx
|
|
109
120
|
|
|
110
|
-
@server_cnx = Faraday.new(server_url(host: host)) do |cnx|
|
|
121
|
+
@server_cnx = Faraday.new(server_url(host: host), ssl: ssl_opts) do |cnx|
|
|
111
122
|
cnx.options[:timeout] = TIMEOUT
|
|
112
123
|
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
113
124
|
|
|
@@ -145,7 +156,7 @@ module Xolo
|
|
|
145
156
|
|
|
146
157
|
req_opts = { on_data: streaming_proc }
|
|
147
158
|
|
|
148
|
-
@streaming_cnx = Faraday.new(server_url(host: host), request: req_opts) do |cnx|
|
|
159
|
+
@streaming_cnx = Faraday.new(server_url(host: host), request: req_opts, ssl: ssl_opts) do |cnx|
|
|
149
160
|
cnx.options[:timeout] = TIMEOUT
|
|
150
161
|
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
151
162
|
cnx.use Xolo::Admin::CookieJar
|
|
@@ -168,7 +179,7 @@ module Xolo
|
|
|
168
179
|
@upload_cnx = nil if host
|
|
169
180
|
return @upload_cnx if @upload_cnx
|
|
170
181
|
|
|
171
|
-
@upload_cnx = Faraday.new(server_url(host: host)) do |cnx|
|
|
182
|
+
@upload_cnx = Faraday.new(server_url(host: host), ssl: ssl_opts) do |cnx|
|
|
172
183
|
cnx.options[:timeout] = TIMEOUT
|
|
173
184
|
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
174
185
|
|
|
@@ -163,7 +163,7 @@ module Xolo
|
|
|
163
163
|
def ssvc_na
|
|
164
164
|
tgt_all = walkthru_cmd_opts[:release_groups]&.include?(Xolo::TARGET_ALL)
|
|
165
165
|
|
|
166
|
-
"N/A if
|
|
166
|
+
"N/A if Release Group is '#{Xolo::TARGET_ALL}'" if tgt_all
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# @return [String, nil] If a string, a reason why the given menu item is not available now.
|
|
@@ -515,6 +515,8 @@ module Xolo
|
|
|
515
515
|
convert = send deets[:readline]
|
|
516
516
|
convert << Xolo::NONE unless deets[:required]
|
|
517
517
|
convert << Xolo::X
|
|
518
|
+
# if we're doing release groups, make sure the list includes 'all',
|
|
519
|
+
convert << Xolo::TARGET_ALL if deets[:label] == Xolo::Admin::Title::ATTRIBUTES[:release_groups][:label]
|
|
518
520
|
validate = nil
|
|
519
521
|
end
|
|
520
522
|
true
|
|
@@ -1079,7 +1079,7 @@ module Xolo
|
|
|
1079
1079
|
end
|
|
1080
1080
|
|
|
1081
1081
|
# run the cleanup
|
|
1082
|
-
# get the /test route to do whatever testing it does
|
|
1082
|
+
# get the /test route to do whatever testing it does.
|
|
1083
1083
|
# during testing - this will return all kinds of things.
|
|
1084
1084
|
#
|
|
1085
1085
|
# @return [void]
|
|
@@ -1104,11 +1104,7 @@ module Xolo
|
|
|
1104
1104
|
display_progress resp[:progress_stream_url_path]
|
|
1105
1105
|
end
|
|
1106
1106
|
|
|
1107
|
-
|
|
1108
|
-
# 1.2 gb
|
|
1109
|
-
large_file = '/dist/caspershare/Packages-DEACTIVATED/SecUpd2020-006HighSierra.pkg'
|
|
1110
|
-
|
|
1111
|
-
pkg_to_upload = Pathname.new large_file
|
|
1107
|
+
pkg_to_upload = Pathname.new '/path/to/some/file.pkg'
|
|
1112
1108
|
puts "Uploading Test File #{pkg_to_upload.size} bytes... "
|
|
1113
1109
|
upload_test_file(pkg_to_upload)
|
|
1114
1110
|
|
data/lib/xolo/admin/title.rb
CHANGED
data/lib/xolo/admin/validate.rb
CHANGED
|
@@ -196,7 +196,7 @@ module Xolo
|
|
|
196
196
|
if cli_cmd.command == Xolo::Admin::Options::ADD_TITLE_CMD
|
|
197
197
|
err =
|
|
198
198
|
if title_exists
|
|
199
|
-
'already exists in Xolo
|
|
199
|
+
"title '#{val}' already exists in Xolo"
|
|
200
200
|
elsif val !~ /\A[a-z0-9-][a-z0-9-]+\z/
|
|
201
201
|
TITLE_ATTRS[:title][:invalid_msg]
|
|
202
202
|
else
|
|
@@ -207,7 +207,7 @@ module Xolo
|
|
|
207
207
|
elsif Xolo::Admin::Options::MUST_EXIST_COMMANDS.include?(cli_cmd.command)
|
|
208
208
|
return val if title_exists
|
|
209
209
|
|
|
210
|
-
err = "doesn't exist in Xolo"
|
|
210
|
+
err = "title '#{val}' doesn't exist in Xolo"
|
|
211
211
|
|
|
212
212
|
# any other command
|
|
213
213
|
else
|
|
@@ -975,7 +975,7 @@ module Xolo
|
|
|
975
975
|
if walkthru?
|
|
976
976
|
"Cannot be in Self Service when Target Group is '#{Xolo::TARGET_ALL}'"
|
|
977
977
|
else
|
|
978
|
-
"--self-service cannot be used when --
|
|
978
|
+
"--self-service cannot be used when --release-groups contains '#{Xolo::TARGET_ALL}'"
|
|
979
979
|
end
|
|
980
980
|
raise_consistency_error msg
|
|
981
981
|
end
|
data/lib/xolo/admin/version.rb
CHANGED
|
@@ -87,6 +87,8 @@ module Xolo
|
|
|
87
87
|
def self.fetch(title, version, cnx)
|
|
88
88
|
resp = cnx.get server_route(title, version)
|
|
89
89
|
new resp.body
|
|
90
|
+
rescue Faraday::ResourceNotFound
|
|
91
|
+
raise Xolo::NoSuchItemError, "No such version '#{version}'"
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
# Deploy a version to desired computers and groups via MDM
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
module Xolo
|
|
10
|
+
|
|
11
|
+
module Core
|
|
12
|
+
|
|
13
|
+
module BaseClasses
|
|
14
|
+
|
|
15
|
+
# A class for working with pre-defined settings & preferences for Xolo
|
|
16
|
+
#
|
|
17
|
+
# This is the base class for Xolo::Server::Configuration and Xolo::Admin::Configuration
|
|
18
|
+
#
|
|
19
|
+
# The subclasses must 'include Singleton', making them use the Singleton Pattern, meaning
|
|
20
|
+
# only one instance can exist at a time.
|
|
21
|
+
#
|
|
22
|
+
# This parent class provides methods for loading and saving YAML files with configuration
|
|
23
|
+
# and prefs settings in the Server or Admin context.
|
|
24
|
+
#
|
|
25
|
+
# The YAML files store a Hash of keys and values relevent to the context
|
|
26
|
+
# they are used in.
|
|
27
|
+
#
|
|
28
|
+
# Subclasses must define these constants and related methods:
|
|
29
|
+
#
|
|
30
|
+
# Constant CONF_FILENAME [String]
|
|
31
|
+
# The filename (not the full path) of the config yaml file to read and write.
|
|
32
|
+
#
|
|
33
|
+
# Instance method #conf_file [Pathname]
|
|
34
|
+
# The full expanded absolute path to the config yaml file.
|
|
35
|
+
#
|
|
36
|
+
# Constant KEYS [Hash{Symbol: Hash}]
|
|
37
|
+
# The keys of this Hash are the keys that may be found in the yaml file.
|
|
38
|
+
# The Hash values here define the value in the YAML file, with these keys:
|
|
39
|
+
#
|
|
40
|
+
# required: [Boolean] Is this key/value required in the YAML file? The server
|
|
41
|
+
# will not start if a required key is missing.
|
|
42
|
+
#
|
|
43
|
+
# type: [Symbol] The data-type of the value, one of the types supported by
|
|
44
|
+
# Optimist: :boolean, :string, :integer, :float, :io, :date
|
|
45
|
+
# See https://github.com/ManageIQ/optimist/wiki/Option-Types
|
|
46
|
+
#
|
|
47
|
+
# default: [Object] If this key doesn't exist in the YAML file, this is the
|
|
48
|
+
# value used by Xolo.
|
|
49
|
+
#
|
|
50
|
+
# desc: [String] A full description of what this value is, how it is used,
|
|
51
|
+
# possible values, etc. This is presented in help and/or walkthru.
|
|
52
|
+
#
|
|
53
|
+
# load_method: [Symbol] The name of a method in the Configuration Instance
|
|
54
|
+
# to which the YAML value will be passed to convert it into the real value
|
|
55
|
+
# to be used. For example, some YAML values might contain a command to be
|
|
56
|
+
# executed, a pathname to be read, or a raw value to be used. These
|
|
57
|
+
# values should be passed to the :data_from_command_file_or_string method
|
|
58
|
+
# which will return the value to actually be used (the stdout of a command,
|
|
59
|
+
# the contents of a file, or a raw value)
|
|
60
|
+
#
|
|
61
|
+
# private: [Boolean] If true, the value is never presented in logs or normal
|
|
62
|
+
# output. it is replaced with <private>. Use this for sensitive secrets.
|
|
63
|
+
#
|
|
64
|
+
#
|
|
65
|
+
#
|
|
66
|
+
class Configuration
|
|
67
|
+
|
|
68
|
+
# Mixins
|
|
69
|
+
#####################################
|
|
70
|
+
#####################################
|
|
71
|
+
|
|
72
|
+
# Constants
|
|
73
|
+
#####################################
|
|
74
|
+
#####################################
|
|
75
|
+
|
|
76
|
+
PIPE = '|'
|
|
77
|
+
|
|
78
|
+
PRIVATE = '<private>'
|
|
79
|
+
|
|
80
|
+
# Class Methods
|
|
81
|
+
#####################################
|
|
82
|
+
#####################################
|
|
83
|
+
|
|
84
|
+
def self.inherited(child_class)
|
|
85
|
+
Xolo.verbose_inherit child_class, self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Attributes
|
|
89
|
+
#####################################
|
|
90
|
+
#####################################
|
|
91
|
+
|
|
92
|
+
# @return [Hash{Symbol: Object}] The data as read directly from the YAML file
|
|
93
|
+
attr_reader :raw_data
|
|
94
|
+
|
|
95
|
+
# @return
|
|
96
|
+
|
|
97
|
+
# Constructor
|
|
98
|
+
#####################################
|
|
99
|
+
#####################################
|
|
100
|
+
|
|
101
|
+
# # Initialize!
|
|
102
|
+
def initialize
|
|
103
|
+
keys.each_key { |attr| self.class.attr_accessor attr }
|
|
104
|
+
load_from_file
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Public Instance Methods
|
|
108
|
+
#####################################
|
|
109
|
+
#####################################
|
|
110
|
+
|
|
111
|
+
###############
|
|
112
|
+
def to_h
|
|
113
|
+
data = {}
|
|
114
|
+
keys.each_key do |key|
|
|
115
|
+
data[key] = send(key)
|
|
116
|
+
end
|
|
117
|
+
data
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
###############
|
|
121
|
+
def to_h_private
|
|
122
|
+
data = to_h
|
|
123
|
+
keys.each do |key, deets|
|
|
124
|
+
data[key] = PRIVATE if deets[:private]
|
|
125
|
+
end
|
|
126
|
+
data
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Save new data (or raw_data) to the config file. We don't save the
|
|
130
|
+
# actual instance variables, as they may be expanded from
|
|
131
|
+
# commands or file paths, and its the commands or file paths
|
|
132
|
+
# we want to save.
|
|
133
|
+
#
|
|
134
|
+
# @param data [Hash{Symbol: Object}] The data to save to the config file
|
|
135
|
+
#
|
|
136
|
+
# @return [void]
|
|
137
|
+
###############
|
|
138
|
+
def save_to_file(data: nil)
|
|
139
|
+
data ||= raw_data
|
|
140
|
+
conf_file.parent.mkpath unless conf_file.parent.directory?
|
|
141
|
+
conf_file.pix_save YAML.dump(data)
|
|
142
|
+
conf_file.chmod 0o600
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# If the given string starts with a pipe (|) then
|
|
146
|
+
# remove the pipe and execute the remainder, returning
|
|
147
|
+
# its stdout.
|
|
148
|
+
#
|
|
149
|
+
# If the given string is a path to an executable file path, return
|
|
150
|
+
# its stdout.
|
|
151
|
+
#
|
|
152
|
+
# If the given string is a path to a readable file path, return
|
|
153
|
+
# its contents.
|
|
154
|
+
#
|
|
155
|
+
# Otherwise, the string is the desired data, so just return it.
|
|
156
|
+
#
|
|
157
|
+
# @param str [String] a command, file path, or string
|
|
158
|
+
#
|
|
159
|
+
# @param enforce_secure_mode [Boolean] If true, and the str is
|
|
160
|
+
# a path to a file, make sure that the file mode is 700 or 600.
|
|
161
|
+
#
|
|
162
|
+
# @return [String] The std output of the command, file contents, or string
|
|
163
|
+
#
|
|
164
|
+
###############
|
|
165
|
+
def data_from_command_file_or_string(str, enforce_secure_mode: false)
|
|
166
|
+
# if it starts with a pipe, it's a command and args
|
|
167
|
+
# remove the pipe and execute the rest, returning stdout
|
|
168
|
+
return `#{str.delete_prefix(PIPE)}`.chomp if str.start_with? PIPE
|
|
169
|
+
|
|
170
|
+
path = Pathname.new(str)
|
|
171
|
+
return str unless path.file?
|
|
172
|
+
|
|
173
|
+
# this will return, e.g. "600" "775" etc as strings
|
|
174
|
+
mode = format '%o', (path.stat.mode & 0o777)
|
|
175
|
+
|
|
176
|
+
if path.executable?
|
|
177
|
+
if enforce_secure_mode && !mode.end_with?('00')
|
|
178
|
+
raise Xolo::Core::Exceptions::PermissionError,
|
|
179
|
+
"Executable file #{str} must be mode 0700, 0500, or 0100"
|
|
180
|
+
end
|
|
181
|
+
`#{path.to_s.shellescape}`.chomp
|
|
182
|
+
|
|
183
|
+
elsif path.readable?
|
|
184
|
+
if enforce_secure_mode && mode != '600'
|
|
185
|
+
raise Xolo::Core::Exceptions::PermissionError,
|
|
186
|
+
"Readable file #{str} must be mode 0600"
|
|
187
|
+
end
|
|
188
|
+
path.read.chomp
|
|
189
|
+
|
|
190
|
+
else
|
|
191
|
+
str
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Private Instance Methods
|
|
196
|
+
#####################################
|
|
197
|
+
#####################################
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Simpler access to the KEYS constant in subclasses
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash]
|
|
203
|
+
#
|
|
204
|
+
###############
|
|
205
|
+
def keys
|
|
206
|
+
self.class::KEYS
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Load in the values from the config file
|
|
210
|
+
#
|
|
211
|
+
# @return [void]
|
|
212
|
+
#
|
|
213
|
+
###############
|
|
214
|
+
###############
|
|
215
|
+
def load_from_file
|
|
216
|
+
conf_file.parent.mkpath unless conf_file.parent.directory?
|
|
217
|
+
|
|
218
|
+
unless conf_file.readable?
|
|
219
|
+
@raw_data = {}
|
|
220
|
+
return
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
@raw_data = YAML.load conf_file.read
|
|
224
|
+
@raw_data.each do |k, v|
|
|
225
|
+
next unless keys[k]
|
|
226
|
+
|
|
227
|
+
v = send(keys[k][:load_method], v) if keys[k][:load_method]
|
|
228
|
+
send "#{k}=", v
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
end # class Configuration
|
|
233
|
+
|
|
234
|
+
end # BaseClasses
|
|
235
|
+
|
|
236
|
+
end # Core
|
|
237
|
+
|
|
238
|
+
end # Xolo
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Core
|
|
14
|
+
|
|
15
|
+
module BaseClasses
|
|
16
|
+
|
|
17
|
+
# The base class for dealing with Titles and Versions/Patches in the
|
|
18
|
+
# Xolo Server, Admin, and Client modules.
|
|
19
|
+
#
|
|
20
|
+
# The base class for "xolo objects stored on the xolo server", i.e.
|
|
21
|
+
# Titles and Versions/Patches - whether they are being used on the server,
|
|
22
|
+
# in xadm, or in the client.
|
|
23
|
+
#
|
|
24
|
+
# This class holds stuff common to all no matter where or how they are used.
|
|
25
|
+
#
|
|
26
|
+
# See also {Xolo::Core::BaseClasses::Title} and {Xolo::Core::BaseClasses::Version}
|
|
27
|
+
#############################
|
|
28
|
+
class ServerObject
|
|
29
|
+
|
|
30
|
+
# Mixins
|
|
31
|
+
#############################
|
|
32
|
+
#############################
|
|
33
|
+
|
|
34
|
+
extend Xolo::Core::JSONWrappers
|
|
35
|
+
|
|
36
|
+
include Xolo::Core::JSONWrappers
|
|
37
|
+
|
|
38
|
+
# Constants
|
|
39
|
+
#############################
|
|
40
|
+
#############################
|
|
41
|
+
|
|
42
|
+
# Attributes
|
|
43
|
+
#############################
|
|
44
|
+
#############################
|
|
45
|
+
|
|
46
|
+
# Constructor
|
|
47
|
+
######################
|
|
48
|
+
######################
|
|
49
|
+
def initialize(data_hash)
|
|
50
|
+
# log_debug "Instantiating a #{self.class}..."
|
|
51
|
+
|
|
52
|
+
self.class::ATTRIBUTES.each do |attr, deets|
|
|
53
|
+
val = data_hash[attr]
|
|
54
|
+
|
|
55
|
+
# log_debug "Initializing, setting ATTR '#{attr}' => '#{val}' (#{val.class})"
|
|
56
|
+
|
|
57
|
+
# anything not nil, esp empty arrays, needs to be set
|
|
58
|
+
next if val.nil?
|
|
59
|
+
|
|
60
|
+
# convert timestamps to Time objects if needed,
|
|
61
|
+
# All the other values shouldn't need converting
|
|
62
|
+
# when taking in JSON or xadm opts.
|
|
63
|
+
val = Time.parse(val.to_s) if deets[:type] == :time && !val.is_a?(Time)
|
|
64
|
+
|
|
65
|
+
# call the setter
|
|
66
|
+
send "#{attr}=", val
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Instance Methods
|
|
71
|
+
######################
|
|
72
|
+
######################
|
|
73
|
+
|
|
74
|
+
# Convert to a Hash for sending between xadm and the Xolo Server,
|
|
75
|
+
# or installing on clients.
|
|
76
|
+
#
|
|
77
|
+
# Only the values defined in ATTRIBUTES are sent, because all other
|
|
78
|
+
# other attributes are meant only for the local context, i.e.
|
|
79
|
+
# on the server, via xadm, or via 'xolo'.
|
|
80
|
+
#
|
|
81
|
+
# @return [String] The attributes of this title as JSON
|
|
82
|
+
#####################
|
|
83
|
+
def to_h
|
|
84
|
+
hash = {}
|
|
85
|
+
self.class::ATTRIBUTES.each do |attr, deets|
|
|
86
|
+
hash[attr] = send attr
|
|
87
|
+
|
|
88
|
+
# ensure multi values are arrays, even if they are empty
|
|
89
|
+
hash[attr] = [hash[attr]].compact if deets[:multi] && !hash[attr].is_a?(Array)
|
|
90
|
+
end
|
|
91
|
+
hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert to a JSON object for sending between xadm and the Xolo Server
|
|
95
|
+
# or storage on the server.
|
|
96
|
+
#
|
|
97
|
+
# Always make it 'pretty', i.e. human readable, since it often
|
|
98
|
+
# gets stored in files that humans will look at
|
|
99
|
+
#
|
|
100
|
+
# @return [String] The attributes of this title as JSON
|
|
101
|
+
#####################
|
|
102
|
+
def to_json(*_args)
|
|
103
|
+
JSON.pretty_generate to_h
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end # class Title
|
|
107
|
+
|
|
108
|
+
end # module BaseClasses
|
|
109
|
+
|
|
110
|
+
end # module Core
|
|
111
|
+
|
|
112
|
+
end # module Xolo
|