xolo-admin 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.
@@ -0,0 +1,432 @@
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 Admin
12
+
13
+ # Module for parsing and validating the xadm options from the commandline
14
+ module CommandLine
15
+
16
+ # Module Methods
17
+ ##########################
18
+ ##########################
19
+
20
+ # when this module is included
21
+ def self.included(includer)
22
+ Xolo.verbose_include includer, self
23
+ end
24
+
25
+ # Instance Methods
26
+ ##########################
27
+ ##########################
28
+
29
+ # Parse ARGV.
30
+ #
31
+ # First we use Optimist to parse the global opts and populate
32
+ # the OpenStruct Xolo::Admin::Options.global_opts
33
+ #
34
+ # Then we look at the next arguments on the command line
35
+ # (things without a - or -- ) and
36
+ # populate the OpenStruct Xolo::Admin::Options.cli_cmd
37
+ #
38
+ # Then we use Optimist again to look at any remaining
39
+ # options, which apply to the command, and populate
40
+ # the OpenStruct Xolo::Admin::Options.cli_cmd_opts
41
+ #
42
+ ################################################
43
+ def parse_cli
44
+ # if we ask for help at all, we never do walkthru
45
+ if ARGV.include?(Xolo::Admin::Options::HELP_OPT) || ARGV.include?(Xolo::Admin::Options::HELP_CMD)
46
+ ARGV.delete '-w'
47
+ ARGV.delete '--walkthru'
48
+ end
49
+
50
+ # save the global opts hash from optimist into our OpenStruct
51
+ parse_global_cli.each { |k, v| global_opts[k] = v }
52
+
53
+ # Now parse the rest of the command line, getting the
54
+ # command its its args into cli_cmd, and the opts for them
55
+ # into cli_cmd_opts
56
+ parse_command_cli
57
+ end
58
+
59
+ # Use Optimist to parse the global opts, stopping when it hits a known command
60
+ # This also generates the top level --help output
61
+ #
62
+ # @return [Hash] The global opts from the command line, parsed by optimist
63
+ ################################################
64
+ def parse_global_cli
65
+ # set this so its available inside the optimist options block
66
+ executable_file = Xolo::Admin::EXECUTABLE_FILENAME
67
+ usage_line = usage
68
+
69
+ Optimist.options do
70
+ banner 'Name:'
71
+ banner " #{executable_file}, A command-line tool for managing Software Titles and Versions in Xolo."
72
+
73
+ banner "\nUsage:"
74
+ banner " #{usage_line}"
75
+
76
+ banner "\nGlobal Options:"
77
+
78
+ # add a blank line between each of the cli options in the help output
79
+ # NOTE: chrisl added this to the optimist.rb included in this project.
80
+ insert_blanks
81
+
82
+ version Xolo::VERSION
83
+
84
+ # The global opts
85
+ ## manually set :version and :help here, or they appear at the bottom of the help
86
+ opt :version, 'Print version and exit'
87
+ opt :help, 'Show this help and exit'
88
+
89
+ # This actually sets the optimist global options and their help blurbs
90
+ #
91
+ Xolo::Admin::Options::GLOBAL_OPTIONS.each do |opt_key, deets|
92
+ type = deets[:type] ? :string : :boolean
93
+ opt opt_key, deets[:desc], short: deets[:cli], type: type
94
+ end
95
+ stop_on Xolo::Admin::Options::COMMANDS.keys
96
+
97
+ # everything below is just more help output
98
+
99
+ banner "\nCommands:"
100
+ Xolo::Admin::Options::COMMANDS.each do |cmd, deets|
101
+ banner format(' %-20s %s', cmd, deets[:desc])
102
+ end
103
+
104
+ banner "\nCommand Targets:"
105
+ banner Xolo::Admin::Options::DFT_CMD_TITLE_ARG_BANNER
106
+ banner Xolo::Admin::Options::DFT_CMD_VERSION_ARG_BANNER
107
+
108
+ banner "\nCommand Options:"
109
+ banner " Use '#{executable_file} help command' or '#{executable_file} command #{Xolo::Admin::Options::HELP_OPT}' to see command-specific help."
110
+
111
+ banner "\nExamples:"
112
+ banner " #{executable_file} add-title google-chrome <options...>"
113
+ banner " Add a new title 'google-chrome' to Xolo,"
114
+ banner ' specifying all options on the command line'
115
+
116
+ banner "\n #{executable_file} --walkthru add-title google-chrome"
117
+ banner " Add a new title 'google-chrome' to Xolo,"
118
+ banner ' providing options interactively'
119
+
120
+ banner "\n #{executable_file} edit-title google-chrome <options...>"
121
+ banner " Edit the existing title 'google-chrome',"
122
+ banner ' specifying all options on the command line'
123
+
124
+ banner "\n #{executable_file} delete-title google-chrome"
125
+ banner " Delete the existing title 'google-chrome' from Xolo,"
126
+ banner ' along with all of its versions.'
127
+
128
+ banner "\n #{executable_file} add-version google-chrome 95.144.21194 <options...>"
129
+ banner " Add a new version number 95.144.21194 to the title 'google-chrome'"
130
+ banner ' specifying all options on the command line.'
131
+ banner ' Options not provided are inherited from the previous version, if available.'
132
+
133
+ banner "\n #{executable_file} edit-version google-chrome 95.144.21194 <options...>"
134
+ banner " Edit version number 95.144.21194 of the title 'google-chrome'"
135
+ banner ' specifying all options on the command line'
136
+
137
+ banner "\n #{executable_file} delete-version google-chrome 95.144.21194"
138
+ banner " Delete version 95.144.21194 from the title 'google-chrome'"
139
+
140
+ banner "\n #{executable_file} search chrome"
141
+ banner " List all titles that contain the string 'chrome'"
142
+ banner ' and its available versions'
143
+
144
+ banner "\n #{executable_file} report google-chrome"
145
+ banner " Report computers with any version of title 'google-chrome' installed"
146
+
147
+ banner "\n #{executable_file} report google-chrome 95.144.21194"
148
+ banner " Report computers with version 95.144.21194 of title 'google-chrome' installed"
149
+
150
+ banner "\n #{executable_file} list-groups"
151
+ banner ' List all computer groups in Jamf Pro'
152
+ end # Optimist.options
153
+ end
154
+
155
+ # Once we run this, the global opts have been parsed
156
+ # and ARGV should contain our command, any args it takes,
157
+ # and then any options for that command and args.
158
+ #
159
+ # e.g. the command might be 'edit-version'
160
+ # and the args are a title, and a version to edit.
161
+ # Anything after that are the options for doing the editing
162
+ # (unless we are using --walkthru)
163
+ #
164
+ # @return [void]
165
+ ################################################
166
+ def parse_command_cli
167
+ # This gets the command (like config or add-title) and any args
168
+ # like a title and/or a version
169
+ # putting them into cli_cmd
170
+ parse_cmd_and_args # unless cli_cmd.command
171
+
172
+ # if we are using --walkthru, all remaining command options are ignored,
173
+ # so just return.
174
+ # The walkthru_cmd_opts will be populated by the interactive
175
+ # process
176
+ return if walkthru?
177
+
178
+ # parse_cmd_opts uses Optimist to get the --options that go
179
+ # with the command and its args.
180
+ # we loop thru them (its a hash) and save them into our
181
+ # cli_cmd_opts OpenStruct
182
+ optimist_hash = parse_cmd_opts
183
+ optimist_hash.each { |k, v| cli_cmd_opts[k] = v }
184
+
185
+ # Now merge in current_opt_values for anything not given on the cli
186
+ # This is how we inherit values, or apply defaults
187
+ current_opt_values.to_h.each do |k, v|
188
+ next if cli_cmd_opts["#{k}_given"]
189
+
190
+ cli_cmd_opts[k] = v unless v.pix_empty?
191
+ end
192
+
193
+ # Validate the options given on the commandline
194
+ validate_cli_cmd_opts
195
+
196
+ # now cli_cmd_opts contains all the data for
197
+ # processing whatever we're processing, so do the internal consistency checks
198
+ validate_internal_consistency cli_cmd_opts
199
+
200
+ # If we got here, everything in Xolo::Admin::Options.cli_cmd_opts is good to go
201
+ # and can be sent to the server for processing.
202
+ end # parse_command_cli
203
+
204
+ # Get the xadm command, and its args (title, maybe version, etc)
205
+ # from the Command Line.
206
+ # They get stored in the cli_cmd OpenStruct.
207
+ #
208
+ # We don't use optimist for this, we just examine the first items of
209
+ # ARGV until we hit one starting with a dash.
210
+ #
211
+ ##################################################################
212
+ def parse_cmd_and_args
213
+ # next item will be the command we are executing
214
+ cli_cmd.command = ARGV.shift
215
+
216
+ # if there is no command, treat it like `xadm --help`
217
+ return if reparse_global_cli_for_help?
218
+
219
+ # we have a command, validate it
220
+ validate_cli_command
221
+
222
+ # Some commands, like 'config', always do walkthru
223
+ global_opts.walkthru = true if cli_cmd.command == Xolo::Admin::Options::CONFIG_CMD
224
+
225
+ # if the command is 'help'
226
+ # then
227
+ # 'xadm [globalOpts] help' becomes 'xadm --help'
228
+ # and
229
+ # 'xadm [globalOpts] help command' becomes 'xadm [globalOpts] command --help'
230
+ #
231
+ # in those forms, Optimist will deal with displaying the help.
232
+ #
233
+ if cli_cmd.command == Xolo::Admin::Options::HELP_CMD
234
+
235
+ # 'xadm [globalOpts] help' becomes 'xadm --help' (via the reparse method)
236
+ cli_cmd.command = ARGV.shift
237
+ return if reparse_global_cli_for_help?
238
+
239
+ # we have a new command, for which we are getting help. Validate it
240
+ validate_cli_command
241
+
242
+ # 'xadm [globalOpts] help command' becomes 'xadm [globalOpts] command --help'
243
+ ARGV.unshift Xolo::Admin::Options::HELP_OPT
244
+ end
245
+ # if we are here and any part of ARGV is --help, nothing more to do.
246
+ return if ARGV.include?(Xolo::Admin::Options::HELP_OPT)
247
+
248
+ # if we are saving the client code, we don't need to log in
249
+ return if cli_cmd.command == Xolo::Admin::Options::SAVE_CLIENT_CODE_CMD
250
+
251
+ # log in now, cuz we need the server to validate the rest of the
252
+ # command line
253
+ #
254
+ # TODO: Be pickier about which commands actually need the server, and
255
+ # only log in for them.
256
+ #################
257
+ login
258
+
259
+ # What kind of command do we have, since we know it isn't 'help' if we
260
+ # are here.
261
+ if title_command?
262
+ # the next item is the title
263
+ cli_cmd.title = ARGV.shift
264
+ validate_cli_title
265
+
266
+ elsif version_command?
267
+ # the next item is the title and the one after that might be a version
268
+ cli_cmd.title = ARGV.shift
269
+ validate_cli_title
270
+
271
+ cli_cmd.version = ARGV.shift
272
+ validate_cli_version
273
+
274
+ elsif title_or_version_command?
275
+ # the next item is the title and the one after that might be a version
276
+ cli_cmd.title = ARGV.shift
277
+ validate_cli_title
278
+
279
+ cli_cmd.version = ARGV.shift unless ARGV.first.to_s.start_with? Xolo::DASH
280
+ validate_cli_version if cli_cmd.version
281
+
282
+ end # if
283
+ end
284
+
285
+ # Are we showing the full help? If so, re-parse the global opts
286
+ ##################################################################
287
+ def reparse_global_cli_for_help?
288
+ cmdstr = cli_cmd.command.to_s
289
+ return false unless cmdstr.empty? || cmdstr.start_with?(Xolo::DASH)
290
+
291
+ ARGV.unshift Xolo::Admin::Options::HELP_OPT
292
+ parse_global_cli
293
+ true
294
+ end
295
+
296
+ # Are we doing mandatory walkthru? If so, re-parse the global opts
297
+ ##################################################################
298
+ # def reparse_global_cli_for_mandatory_walkthru?
299
+ # return false if @reparsed_global_cli_for_mandatory_walkthru
300
+ # return false if ARGV.include? Xolo::Admin::Options::HELP_OPT
301
+ # return false unless cli_cmd.command == Xolo::Admin::Options::CONFIG_CMD
302
+
303
+ # ARGV.clear
304
+ # ARGV << '--walkthru'
305
+ # ARGV << '--debug' if global_opts.debug
306
+ # ARGV << '--auto-confirm' if global_opts.auto_confirm
307
+ # ARGV << Xolo::Admin::Options::CONFIG_CMD
308
+
309
+ # parse_cli
310
+ # @reparsed_global_cli_for_mandatory_walkthru = true
311
+ # true
312
+ # end
313
+
314
+ # Parse the options for the command.
315
+ # This returns a hash from Optimist
316
+ # @return [Hash] the optimist hash
317
+ ##################################################################
318
+ def parse_cmd_opts
319
+ cmd = cli_cmd.command
320
+ return if cmd == Xolo::Admin::Options::HELP_CMD || cmd.to_s.empty?
321
+
322
+ # set these for use inside the optimist options block
323
+ ###
324
+ executable_file = Xolo::Admin::EXECUTABLE_FILENAME
325
+ cmd_desc = Xolo::Admin::Options::COMMANDS.dig cmd, :desc
326
+ cmd_long_desc = Xolo::Admin::Options::COMMANDS.dig cmd, :long_desc
327
+ cmd_long_desc &&= format_multiline_indent(cmd_long_desc, indent: 2)
328
+ cmd_usage = Xolo::Admin::Options::COMMANDS.dig cmd, :usage
329
+ cmd_display = Xolo::Admin::Options::COMMANDS.dig cmd, :display
330
+ cmd_opts = Xolo::Admin::Options::COMMANDS.dig cmd, :opts
331
+
332
+ title_command?
333
+ vers_cmd = version_command?
334
+ title_or_vers_command = title_or_version_command?
335
+ add_command = add_command?
336
+ edit_command?
337
+ arg_banner = Xolo::Admin::Options::COMMANDS.dig(cmd, :arg_banner)
338
+ arg_banner ||= Xolo::Admin::Options::DFT_CMD_TITLE_ARG_BANNER
339
+
340
+ # The optimist parser and help generator
341
+ # for the command options
342
+ Optimist.options do
343
+ # NOTE: extra newlines are added to the front of strings, cuz
344
+ # optimist's 'banner' method chomps the ends.
345
+ banner 'Command:'
346
+ banner " #{cmd}, #{cmd_desc}"
347
+ if cmd_long_desc
348
+ banner "\nDescription:"
349
+ banner " #{cmd_long_desc}"
350
+ end
351
+
352
+ banner "\nUsage:"
353
+ usage = cmd_usage || "#{executable_file} [global options] #{cmd_display} [options]"
354
+ banner " #{usage}"
355
+
356
+ unless arg_banner == :none
357
+ banner "\nArguments:"
358
+ banner arg_banner
359
+ banner Xolo::Admin::Options::DFT_CMD_VERSION_ARG_BANNER if vers_cmd || title_or_vers_command
360
+ end
361
+
362
+ if cmd_opts
363
+ banner "\nOptions:"
364
+
365
+ # add a blank line between each of the cli options
366
+ # NOTE: chrisl added this to the optimist.rb included in this project.
367
+ insert_blanks
368
+
369
+ # create the optimist options for the command
370
+ cmd_opts.each do |opt_key, deets|
371
+ next unless deets[:cli]
372
+
373
+ # Required opts are only required when adding.
374
+ # when editing, they should already exist
375
+ required = deets[:required] && add_command
376
+
377
+ desc = deets[:desc]
378
+ desc = "#{desc}REQUIRED" if required
379
+
380
+ # booleans are CLI flags defaulting to false
381
+ # everything else is a string that we will convert as we validate later
382
+ type = deets[:type] == :boolean ? :boolean : :string
383
+
384
+ # here we actually set the optimist opt.
385
+ opt opt_key, desc, short: deets[:cli], type: type, required: required, multi: deets[:multi]
386
+ end # opts_to_use.each
387
+ end # if cmd_opts
388
+ end # Optimist.options
389
+ end
390
+
391
+ # @return [Boolean] does the command we're running deal with titles?
392
+ #######################################
393
+ def title_command?
394
+ Xolo::Admin::Options::COMMANDS[cli_cmd.command][:target] == :title
395
+ end
396
+
397
+ # @return [Boolean] does the command we're running deal with versions?
398
+ #######################################
399
+ def version_command?
400
+ Xolo::Admin::Options::COMMANDS[cli_cmd.command][:target] == :version
401
+ end
402
+
403
+ # @return [Boolean] does the command we're running deal with either titles or versions?
404
+ #######################################
405
+ def title_or_version_command?
406
+ Xolo::Admin::Options::COMMANDS[cli_cmd.command][:target] == :title_or_version
407
+ end
408
+
409
+ # @return [Boolean] does the command we're running not deal with titles or versions?
410
+ # e.g. 'config' or 'help'
411
+ #######################################
412
+ def no_target_command?
413
+ !Xolo::Admin::Options::COMMANDS[cli_cmd.command][:target]
414
+ end
415
+
416
+ # @return [Boolean] does the command we're running add a title or version to xolo?
417
+ #######################################
418
+ def add_command?
419
+ Xolo::Admin::Options::ADD_COMMANDS.include? cli_cmd.command
420
+ end
421
+
422
+ # @return [Boolean] does the command we're running add a title or version to xolo?
423
+ #######################################
424
+ def edit_command?
425
+ Xolo::Admin::Options::EDIT_COMMANDS.include? cli_cmd.command
426
+ end
427
+
428
+ end # module CommandLine
429
+
430
+ end # module Admin
431
+
432
+ end # module Xolo
@@ -0,0 +1,196 @@
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 Admin
14
+
15
+ # Personal prefs for users of 'xadm'
16
+ class Configuration < Xolo::Core::BaseClasses::Configuration
17
+
18
+ include Singleton
19
+
20
+ # Save to yaml file in ~/Library/Preferences/com.pixar.xolo.admin.prefs.yaml
21
+ #
22
+ # - hostname of xolo server
23
+ # - always port 443, for now
24
+ #
25
+ # Note - credentials for Xolo Server area stored in login keychain.
26
+ # code for that is in the Xolo::Admin::Credentials module.
27
+
28
+ # Constants
29
+ ##############################
30
+ ##############################
31
+ CONF_FILE_DIR = '~/Library/Preferences/'
32
+ CONF_FILENAME = 'com.pixar.xolo.admin.config.yaml'
33
+
34
+ CREDENTIALS_NEEDED = '<credentials needed>'
35
+ CREDENTIALS_IN_KEYCHAIN = '<stored in keychain>'
36
+ CREDENTIALS_STORED = '<stored>'
37
+
38
+ # See Xolo::Core::BaseClasses::Configuration for required values
39
+ # when used to access the config file.
40
+ #
41
+ # Also adds values used for CLI and walktru, as with the
42
+ # ATTRIBUTES of Xolo::Core::BaseClasses::Title and Xolo::Core::BaseClasses::Version
43
+ #
44
+ KEYS = {
45
+
46
+ # @!attribute hostname
47
+ # @return [String]
48
+ hostname: {
49
+ required: true,
50
+ label: 'Xolo Server Hostname',
51
+ type: :string,
52
+ validate: true,
53
+ invalid_msg: "Invalid hostname, can't connect, or not a Xolo server.",
54
+ desc: <<~ENDDESC
55
+ The hostname of the Xolo Server to interact with,
56
+ e.g. 'xolo.myschool.edu'
57
+ Enter 'x' to exit if no attempts are successful.
58
+ ENDDESC
59
+ },
60
+
61
+ # @!attribute admin
62
+ # @return [String]
63
+ admin: {
64
+ required: true,
65
+ label: 'Username',
66
+ type: :string,
67
+ validate: false,
68
+ desc: <<~ENDDESC
69
+ The Xolo admin username for connecting to the Xolo server.
70
+ The same that you would use to connect to Jamf Pro.
71
+ ENDDESC
72
+ },
73
+
74
+ # @!attribute pw
75
+ # @return [String]
76
+ pw: {
77
+ required: true,
78
+ label: 'Password',
79
+ type: :string,
80
+ validate: true,
81
+ walkthru_na: :pw_na,
82
+ secure_interactive_input: true,
83
+ invalid_msg: 'Incorrect username or password, or user not allowed.',
84
+ desc: <<~ENDDESC
85
+ The password for connecting to the Xolo server. The same that
86
+ you would use to connect to Jamf Pro.
87
+ It will be stored in your login keychain for use in your terminal or
88
+ other MacOS GUI applications, such as XCode.
89
+
90
+ If you are configuring a non-GUI environment, such as a CI workflow,
91
+ set 'Non-GUI mode' to true. See the 'Non-GUI mode' option below for details
92
+
93
+ Enter 'x' to exit if you are in an unknown password loop.
94
+ ENDDESC
95
+ },
96
+
97
+ # @!attribute pw
98
+ # @return [String]
99
+ no_gui: {
100
+ required: false,
101
+ label: 'Non-GUI mode',
102
+ type: :boolean,
103
+ validate: :validate_boolean,
104
+ walkthru_na: :pw_na,
105
+ secure_interactive_input: true,
106
+ desc: <<~ENDDESC
107
+ If you are configuring xadm for a non-GUI environment, such as a CI workflow,
108
+ set this to true. This will prevent xadm from trying to access the keychain.
109
+
110
+ The password value can then be set to:
111
+ - A command prefixed with '|' that will be executed to get the password from stdout.
112
+ This can have any CLI options and arguments you need to get the password.
113
+ This is useful when using a secret-storage system to manage secrets.
114
+
115
+ - A path to an executable file that returns the password to stdout.
116
+ No arguments are passed, the file is just executed. The file must have only
117
+ rwx permissions for the user running xadm, i.e. mode 0700.
118
+
119
+ - A path to a readable file containing the password, which must have only rw
120
+ permissions for the user running xadm, i.e. mode 0600.
121
+
122
+ - Or the password itself, which will be stored in the xadm config file
123
+
124
+ WARNING: Be careful when storing passwords in files.
125
+ ENDDESC
126
+ },
127
+
128
+ # @!attribute pw
129
+ # @return [String]
130
+ editor: {
131
+ label: 'Preferred editor',
132
+ type: :string,
133
+ validate: true,
134
+ invalid_msg: 'That editor does not exist, or is not executable.',
135
+ desc: <<~ENDDESC
136
+ The editor to use for interactively editing descriptions and other multi-line
137
+ text. Enter the full path to an editor, such as '/usr/bin/vim'. It must
138
+ take the name of a file to edit as an argument.
139
+
140
+ GUI editors are supported, such as /usr/local/bin/bbedit. They will be launched
141
+ as needed when editing multi-line text.
142
+ Note that you may need to provide a command line option to the editor to make
143
+ the cli process wait for the GUI editor to finish. For example, the -w option
144
+ for bbedit.
145
+
146
+ If no editor is set in your config, you will be asked to use one of a few
147
+ basic ones.
148
+ ENDDESC
149
+ }
150
+
151
+ }.freeze
152
+
153
+ # Class methods
154
+ ##############################
155
+ ##############################
156
+
157
+ # The KEYS that are available as CLI & walkthru options
158
+ # with the 'xadm config' command.
159
+ #
160
+ # @return [Hash{Symbol: Hash}]
161
+ #
162
+ ####################
163
+ def self.cli_opts
164
+ KEYS
165
+ end
166
+
167
+ # The help text for the 'xadm config' command.
168
+ # Needed because none of the options are available
169
+ # as CLI options.
170
+ #########################
171
+ def self.help_desc_text
172
+ text = +''
173
+ KEYS.each_value do |value|
174
+ text += <<~ENDDESC
175
+ #{value[:label]}:
176
+ #{value[:desc].lines.map { |l| " #{l}" }.join}
177
+ ENDDESC
178
+ end
179
+ text
180
+ end
181
+
182
+ # Public Instance methods
183
+ ##############################
184
+ ##############################
185
+
186
+ # @return [Pathname] The file that stores configuration values
187
+ #######################
188
+ def conf_file
189
+ @conf_file ||= Pathname.new("#{CONF_FILE_DIR}#{CONF_FILENAME}").expand_path
190
+ end
191
+
192
+ end # class Configuration
193
+
194
+ end # module Admin
195
+
196
+ end # module Xolo