ruby-jss 0.6.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.

Potentially problematic release.


This version of ruby-jss might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +7 -0
  3. data/CHANGES.md +112 -0
  4. data/LICENSE.txt +174 -0
  5. data/README.md +426 -0
  6. data/THANKS.md +6 -0
  7. data/bin/cgrouper +485 -0
  8. data/bin/subnet-update +400 -0
  9. data/lib/jss-api.rb +2 -0
  10. data/lib/jss.rb +190 -0
  11. data/lib/jss/api_connection.rb +410 -0
  12. data/lib/jss/api_object.rb +616 -0
  13. data/lib/jss/api_object/advanced_search.rb +389 -0
  14. data/lib/jss/api_object/advanced_search/advanced_computer_search.rb +95 -0
  15. data/lib/jss/api_object/advanced_search/advanced_mobile_device_search.rb +96 -0
  16. data/lib/jss/api_object/advanced_search/advanced_user_search.rb +95 -0
  17. data/lib/jss/api_object/building.rb +92 -0
  18. data/lib/jss/api_object/category.rb +147 -0
  19. data/lib/jss/api_object/computer.rb +852 -0
  20. data/lib/jss/api_object/creatable.rb +98 -0
  21. data/lib/jss/api_object/criteriable.rb +189 -0
  22. data/lib/jss/api_object/criteriable/criteria.rb +231 -0
  23. data/lib/jss/api_object/criteriable/criterion.rb +228 -0
  24. data/lib/jss/api_object/department.rb +93 -0
  25. data/lib/jss/api_object/distribution_point.rb +560 -0
  26. data/lib/jss/api_object/extendable.rb +221 -0
  27. data/lib/jss/api_object/extension_attribute.rb +466 -0
  28. data/lib/jss/api_object/extension_attribute/computer_extension_attribute.rb +362 -0
  29. data/lib/jss/api_object/extension_attribute/mobile_device_extension_attribute.rb +189 -0
  30. data/lib/jss/api_object/extension_attribute/user_extension_attribute.rb +117 -0
  31. data/lib/jss/api_object/group.rb +380 -0
  32. data/lib/jss/api_object/group/computer_group.rb +124 -0
  33. data/lib/jss/api_object/group/mobile_device_group.rb +139 -0
  34. data/lib/jss/api_object/group/user_group.rb +139 -0
  35. data/lib/jss/api_object/ldap_server.rb +535 -0
  36. data/lib/jss/api_object/locatable.rb +286 -0
  37. data/lib/jss/api_object/matchable.rb +97 -0
  38. data/lib/jss/api_object/mobile_device.rb +556 -0
  39. data/lib/jss/api_object/netboot_server.rb +148 -0
  40. data/lib/jss/api_object/network_segment.rb +414 -0
  41. data/lib/jss/api_object/osx_configuration_profile.rb +262 -0
  42. data/lib/jss/api_object/package.rb +839 -0
  43. data/lib/jss/api_object/peripheral.rb +335 -0
  44. data/lib/jss/api_object/peripheral_type.rb +295 -0
  45. data/lib/jss/api_object/policy.rb +898 -0
  46. data/lib/jss/api_object/purchasable.rb +316 -0
  47. data/lib/jss/api_object/removable_macaddr.rb +98 -0
  48. data/lib/jss/api_object/scopable.rb +136 -0
  49. data/lib/jss/api_object/scopable/scope.rb +621 -0
  50. data/lib/jss/api_object/script.rb +631 -0
  51. data/lib/jss/api_object/self_servable.rb +356 -0
  52. data/lib/jss/api_object/site.rb +93 -0
  53. data/lib/jss/api_object/software_update_server.rb +109 -0
  54. data/lib/jss/api_object/updatable.rb +117 -0
  55. data/lib/jss/api_object/uploadable.rb +138 -0
  56. data/lib/jss/api_object/user.rb +272 -0
  57. data/lib/jss/client.rb +504 -0
  58. data/lib/jss/compatibility.rb +66 -0
  59. data/lib/jss/composer.rb +185 -0
  60. data/lib/jss/configuration.rb +306 -0
  61. data/lib/jss/db_connection.rb +298 -0
  62. data/lib/jss/exceptions.rb +95 -0
  63. data/lib/jss/ruby_extensions.rb +35 -0
  64. data/lib/jss/ruby_extensions/filetest.rb +43 -0
  65. data/lib/jss/ruby_extensions/hash.rb +79 -0
  66. data/lib/jss/ruby_extensions/ipaddr.rb +91 -0
  67. data/lib/jss/ruby_extensions/pathname.rb +77 -0
  68. data/lib/jss/ruby_extensions/string.rb +59 -0
  69. data/lib/jss/ruby_extensions/time.rb +63 -0
  70. data/lib/jss/server.rb +108 -0
  71. data/lib/jss/utility.rb +478 -0
  72. data/lib/jss/version.rb +31 -0
  73. metadata +187 -0
@@ -0,0 +1,478 @@
1
+ ### Copyright 2016 Pixar
2
+ ###
3
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
4
+ ### with the following modification; you may not use this file except in
5
+ ### compliance with the Apache License and the following modification to it:
6
+ ### Section 6. Trademarks. is deleted and replaced with:
7
+ ###
8
+ ### 6. Trademarks. This License does not grant permission to use the trade
9
+ ### names, trademarks, service marks, or product names of the Licensor
10
+ ### and its affiliates, except as required to comply with Section 4(c) of
11
+ ### the License and to reproduce the content of the NOTICE file.
12
+ ###
13
+ ### You may obtain a copy of the Apache License at
14
+ ###
15
+ ### http://www.apache.org/licenses/LICENSE-2.0
16
+ ###
17
+ ### Unless required by applicable law or agreed to in writing, software
18
+ ### distributed under the Apache License with the above modification is
19
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20
+ ### KIND, either express or implied. See the Apache License for the specific
21
+ ### language governing permissions and limitations under the Apache License.
22
+ ###
23
+ ###
24
+
25
+ ###
26
+ module JSS
27
+
28
+ ### A collection of useful utility methods. Mostly for
29
+ ### converting values between formats, parsing data, and
30
+ ### user interaction.
31
+
32
+ ### Converts an OS Version into an Array of higher OS versions.
33
+ ###
34
+ ### It's unlikely that this library will still be in use as-is by the release of OS X 10.19.15.
35
+ ### Hopefully well before then JAMF will implement a "minimum OS" in the JSS itself.
36
+ ###
37
+ ### @param min_os [String] the mimimum OS version to expand, e.g. ">=10.6.7" or "10.6.7"
38
+ ###
39
+ ### @return [Array] Nearly all potential OS versions from the minimum to 10.19.x.
40
+ ###
41
+ ### @example
42
+ ### JSS.expand_min_os ">=10.6.7" # => returns this array
43
+ ### # ["10.6.7",
44
+ ### # "10.6.8",
45
+ ### # "10.6.9",
46
+ ### # "10.6.10",
47
+ ### # "10.6.11",
48
+ ### # "10.6.12",
49
+ ### # "10.6.13",
50
+ ### # "10.6.14",
51
+ ### # "10.6.15",
52
+ ### # "10.7.x",
53
+ ### # "10.8.x",
54
+ ### # "10.9.x",
55
+ ### # "10.10.x",
56
+ ### # "10.11.x",
57
+ ### # "10.12.x",
58
+ ### # "10.13.x",
59
+ ### # "10.14.x",
60
+ ### # "10.15.x",
61
+ ### # "10.16.x",
62
+ ### # "10.17.x",
63
+ ### # "10.18.x",
64
+ ### # "10.19.x"]
65
+ ###
66
+ ###
67
+ def self.expand_min_os (min_os)
68
+
69
+ min_os = min_os.delete ">="
70
+
71
+ ### split the version into major, minor and maintenance release numbers
72
+ (maj,min,maint) = min_os.split(".")
73
+ maint = "x" if maint.nil? or maint == "0"
74
+
75
+ ### if the maint release number is an "x" just start the list of OK OS's with it
76
+ if maint == "x"
77
+ ok_oses = [maj + "." + min.to_s + ".x"]
78
+
79
+ ### otherwise, start with it and explicitly add all maint releases up to 15
80
+ ### (and hope apple doesn't do more than 15 maint releases for an OS)
81
+ else
82
+ ok_oses = []
83
+ (maint.to_i..15).each do |m|
84
+ ok_oses << maj + "." + min +"." + m.to_s
85
+ end # each m
86
+ end
87
+
88
+ ### now account for all OS X versions starting with 10.
89
+ ### up to at least 10.19.x
90
+ ((min.to_i + 1)..19).each do |v|
91
+ ok_oses << maj + "." + v.to_s + ".x"
92
+ end # each v
93
+ return ok_oses
94
+ end
95
+
96
+ ### Scripts and packages can have processor limitations.
97
+ ### This method tests a given processor, against a requirement
98
+ ### to see if the requirement is met.
99
+ ###
100
+ ### @param requirement[String] The processor requirement.
101
+ ### either 'ppc', 'x86', or some variation on "none", nil, or empty
102
+ ###
103
+ ### @param processor[String] the processor to check, defaults to
104
+ ### the processor of the current machine. Any flavor of intel
105
+ ## is (i486, i386, x86-64, etc) is treated as "x86"
106
+ ###
107
+ ### @return [Boolean] can this pkg be installed with the processor
108
+ ### given?
109
+ ###
110
+ def self.processor_ok? (requirement, processor = nil)
111
+
112
+ return true if requirement.to_s.empty? or requirement =~ /none/i
113
+ processor ||= `/usr/bin/uname -p`
114
+ return requirement == (processor.to_s.include?("86") ? "x86" : "ppc")
115
+ end
116
+
117
+ ### Scripts and packages can have OS limitations.
118
+ ### This method tests a given OS, against a requirement list
119
+ ### to see if the requirement is met.
120
+ ###
121
+ ### @param requirement[String] The os requirement list, a comma-seprated string
122
+ ### or array of strings of allows OSes. e.g. 10.7, 10.8.5 or 10.9.x
123
+ ###
124
+ ### @param processor[String] the os to check, defaults to
125
+ ### the os of the current machine.
126
+ ###
127
+ ### @return [Boolean] can this pkg be installed with the processor
128
+ ### given?
129
+ ###
130
+ def self.os_ok? (requirement, os_to_check = nil)
131
+ return true if requirement.to_s.empty? or requirement.to_s =~ /none/i
132
+ requirement = JSS.to_s_and_a(requirement)[:arrayform]
133
+
134
+ os_to_check ||= `/usr/bin/sw_vers -productVersion`.chomp
135
+
136
+ # convert the requirement array into an array of regexps.
137
+ # examples:
138
+ # "10.8.5" becomes /^10\.8\.5$/
139
+ # "10.8" becomes /^10.8(.0)?$/
140
+ # "10.8.x" /^10\.8\.?\d*$/
141
+ req_regexps = requirement.map do |r|
142
+ if r.end_with?('.x')
143
+ /^#{r.chomp('.x').gsub('.','\.')}\.?\d*$/
144
+
145
+ elsif r =~ /^\d+\.\d+$/
146
+ /^#{r.gsub('.','\.')}(.0)?$/
147
+
148
+ else
149
+ /^#{r.gsub('.','\.')}$/
150
+ end
151
+ end
152
+
153
+ req_regexps.each{|re| return true if os_to_check =~ re }
154
+ return false
155
+ end
156
+
157
+
158
+ ### Given a list of data as a comma-separated string, or an Array of strings,
159
+ ### return a Hash with both versions.
160
+ ###
161
+ ### Some parts of the JSS require lists as comma-separated strings, while
162
+ ### often those data are easier work with as arrays. This method is a handy way
163
+ ### to get either form when given either form.
164
+ ###
165
+ ### @param somedata [String, Array] the data to parse, of either class,
166
+ ###
167
+ ### @return [Hash{:stringform => String, :arrayform => Array}] the data as both comma-separated String and Array
168
+ ###
169
+ ### @example
170
+ ### JSS.to_s_and_a "foo, bar, baz" # Hash => {:stringform => "foo, bar, baz", :arrayform => ["foo", "bar", "baz"]}
171
+ ###
172
+ ### JSS.to_s_and_a ["foo", "bar", "baz"] # Hash => {:stringform => "foo, bar, baz", :arrayform => ["foo", "bar", "baz"]}
173
+ ###
174
+ def self.to_s_and_a (somedata)
175
+ case somedata
176
+ when nil
177
+ valstr = ""
178
+ valarr = []
179
+ when String
180
+ valstr = somedata
181
+ valarr = somedata.split(/,\s*/)
182
+ when Array
183
+ valstr = somedata.join ", "
184
+ valarr = somedata
185
+ else
186
+ raise JSS::InvalidDataError, "Input must be a comma-separated String or an Array of Strings"
187
+ end # case
188
+ return {:stringform => valstr, :arrayform => valarr}
189
+ end # to_s_and_a
190
+
191
+ ### Parse a plist into a Ruby data structure.
192
+ ### This enhances Plist::parse_xml taking file paths, as well as XML Strings
193
+ ### and reading the files regardless of binary/XML format.
194
+ ###
195
+ ### @param plist[Pathname, String] the plist XML, or the path to a plist file
196
+ ###
197
+ ### @return [Object] the parsed plist as a ruby hash,array, etc.
198
+ ###
199
+ def self.parse_plist (plist)
200
+
201
+ # did we get a string of xml, or a string pathname?
202
+ case plist
203
+ when String
204
+ if plist.include? "</plist>"
205
+ return Plist.parse_xml plist
206
+ else
207
+ plist = Pathname.new plist
208
+ end
209
+ when Pathname
210
+ true
211
+ else
212
+ raise ArgumentError, "Argument must be a path (as a Pathname or String) or a String of XML"
213
+ end # case plist
214
+
215
+ # if we're here, its a Pathname
216
+ raise JSS::MissingDataError, "No such file: #{plist}" unless plist.file?
217
+
218
+ return Plist.parse_xml `/usr/libexec/PlistBuddy -x -c print #{Shellwords.escape(plist.to_s)}`
219
+
220
+ end # parse_plist
221
+
222
+
223
+ ### Converts anything that responds to #to_s to a Time, or nil
224
+ ###
225
+ ### Return nil if the item is nil, 0 or an empty String.
226
+ ###
227
+ ### Otherwise the item converted to a string, and parsed with DateTime.parse.
228
+ ### It is then examined to see if it has a UTC offset. If not, the local offset
229
+ ### is applied, then the DateTime is converted to a Time.
230
+ ###
231
+ ### @param a_datetime [#to_s] The thing to convert to a time.
232
+ ###
233
+ ### @return [Time, nil] nil is returned if a_datetime is nil, 0 or an empty String.
234
+ ###
235
+ def self.parse_time(a_datetime)
236
+ return nil if NIL_DATES.include? a_datetime
237
+
238
+ the_dt = DateTime.parse(a_datetime.to_s)
239
+
240
+ ### The microseconds in DateTimes are stored as a fraction of a day.
241
+ ### Convert them to an integer of microseconds
242
+ usec = (the_dt.sec_fraction * 60 * 60 * 24 * (10**6)).to_i
243
+
244
+ ### if the UTC offset of the datetime is zero, make a new one with the correct local offset
245
+ ### (which might also be zero if we happen to be in GMT)
246
+ if the_dt.offset == 0
247
+ the_dt = DateTime.new(the_dt.year, the_dt.month, the_dt.day, the_dt.hour, the_dt.min, the_dt.sec, JSS::TIME_ZONE_OFFSET)
248
+ end
249
+ # now convert it to a Time and return it
250
+ Time.at the_dt.strftime('%s').to_i, usec
251
+
252
+ end #parse_time
253
+
254
+ ### Deprecated - to be eventually removed in favor of
255
+ ### the more-appropriately named JSS::parse_time
256
+ ###
257
+ ### @see JSS::parse_time
258
+ ###
259
+ def self.parse_datetime(a_datetime) ; self.parse_time(a_datetime) ; end
260
+
261
+ ### Converts JSS epoch (unix epoch + milliseconds) to a Ruby Time object
262
+ ###
263
+ ### @param epoch[String, Integer, nil]
264
+ ###
265
+ ### @return [Time, nil] nil is returned if epoch is nil, 0 or an empty String.
266
+ ###
267
+ def self.epoch_to_time(epoch)
268
+ return nil if NIL_DATES.include? epoch
269
+ Time.at(epoch.to_i / 1000.0)
270
+ end #parse_date
271
+
272
+ ### Given a string of xml element text, escape any characters that would make XML unhappy.
273
+ ### * & => &amp;
274
+ ### * " => &quot;
275
+ ### * < => &lt;
276
+ ### * > => &gt;
277
+ ### * ' => &apos;
278
+ ###
279
+ ### @param string [String] the string to make xml-compliant.
280
+ ###
281
+ ### @return [String] the xml-compliant string
282
+ ###
283
+ def self.escape_xml(string)
284
+ string.gsub(/&/, '&amp;').gsub(/\"/, '&quot;').gsub(/>/, '&gt;').gsub(/</, '&lt;').gsub(/'/, '&apos;')
285
+ end
286
+
287
+ ### Given an element name and an array of content, generate an Array of
288
+ ### REXML::Element objects with that name, and matching content.
289
+ ### The array of REXML elements would render thus:
290
+ ### <foo>bar</foo>
291
+ ### <foo>morefoo</foo>
292
+ ###
293
+ ### @param element [#to_s] an element_name like :foo
294
+ ###
295
+ ### @param list [Array<#to_s>] an Array of element content such as ["bar", :morefoo]
296
+ ###
297
+ ### @return [Array<REXML::Element>]
298
+ ###
299
+ def self.array_to_rexml_array(element,list)
300
+ raise JSS::InvalidDataError, "Arg. must be an Array." unless list.kind_of? Array
301
+ element = element.to_s
302
+ list.map do |v|
303
+ e = REXML::Element.new(element)
304
+ e.text = v
305
+ e
306
+ end
307
+ end
308
+
309
+ ### Given a simple Hash, convert it to an array of REXML Elements such that each
310
+ ### key becomes an element, and its value becomes the text content of
311
+ ### that element
312
+ ###
313
+ ### @example
314
+ ### my_hash = {:foo => "bar", :baz => :morefoo}
315
+ ### xml = JSS.hash_to_rexml_array(my_hash)
316
+ ### xml.each{|x| puts x }
317
+ ###
318
+ ### <foo>bar</foo>
319
+ ### <baz>morefoo</baz>
320
+ ###
321
+ ### @param hash [Hash{#to_s => #to_s}] the Hash to convert
322
+ ###
323
+ ### @return [Array<REXML::Element>] the Array of REXML elements.
324
+ ###
325
+ def self.hash_to_rexml_array(hash)
326
+ raise InvalidDataError, "Arg. must be a Hash." unless hash.kind_of? Hash
327
+ ary = []
328
+ hash.each_pair do |k,v|
329
+ el = REXML::Element.new k.to_s
330
+ el.text = v
331
+ ary << el
332
+ end
333
+ ary
334
+ end
335
+
336
+ ### Given an Array of Hashes with :id and/or :name keys, return
337
+ ### a single REXML element with a sub-element for each item,
338
+ ### each of which contains a :name or :id element.
339
+ ###
340
+ ### @param list_element [#to_s] the name of the XML element that contains the list.
341
+ ### e.g. :computers
342
+ ###
343
+ ### @param item_element [#to_s] the name of each XML element in the list,
344
+ ### e.g. :computer
345
+ ###
346
+ ### @param item_list [Array<Hash>] an Array of Hashes each with a :name or :id key.
347
+ ###
348
+ ### @param content [Symbol] which hash key should be used as the content of if list item? Defaults to :name
349
+ ###
350
+ ### @return [REXML::Element] the item list as REXML
351
+ ###
352
+ ### @example
353
+ ### comps = [{:id=>2,:name=>'kimchi'},{:id=>5,:name=>'mantis'}]
354
+ ### xml = JSS.item_list_to_rexml_list(:computers, :computer , comps, :name)
355
+ ### puts xml
356
+ ### # output manually formatted for clarity. No newlines in the real xml string
357
+ ### <computers>
358
+ ### <computer>
359
+ ### <name>kimchi</name>
360
+ ### </computer>
361
+ ### <computer>
362
+ ### <name>mantis</name>
363
+ ### </computer>
364
+ ### </computers>
365
+ ###
366
+ ### # if content is :id, then, eg. <name>kimchi</name> would be <id>2</id>
367
+ ###
368
+ def self.item_list_to_rexml_list(list_element, item_element , item_list, content = :name)
369
+ xml_list = REXML::Element.new list_element.to_s
370
+ item_list.each do |i|
371
+ xml_list.add_element(item_element.to_s).add_element(content.to_s).text = i[content]
372
+ end
373
+ xml_list
374
+ end
375
+
376
+ ### Parse a JSS Version number into something comparable
377
+ ###
378
+ ### Unfortunately, the JSS version numbering is inconsistant and improper at the moment.
379
+ ### Version 9.32 should be version 9.3.2, so that it
380
+ ### will be recognizable as being less than 9.4
381
+ ###
382
+ ### To work around this until JAMF standardizes version numbering,
383
+ ### we will assume any digits before the first dot is the major version
384
+ ### and the first digit after the first dot is the minor version
385
+ ### and anything else, including other dots, is the revision
386
+ ###
387
+ ### If that revision starts with a dot, it is removed.
388
+ ### so 9.32 becomes major-9, minor-3, rev-2
389
+ ### and 9.32.3764 becomes major-9, minor-3, rev-2.3764
390
+ ### and 9.3.2.3764 becomes major-9, minor-3, rev-2.3764
391
+ ###
392
+ ### This method of parsing will break if the minor revision
393
+ ### ever gets above 9.
394
+ ###
395
+ ### Returns a hash with these keys:
396
+ ### * :major => the major version, Integer
397
+ ### * :minor => the minor version, Integor
398
+ ### * :revision => the revision, String
399
+ ### * :version => a Gem::Version object built from the above keys, which is easily compared to others.
400
+ ###
401
+ ### @param version[String] a JSS version number from the API
402
+ ###
403
+ ### @return [Hash{Symbol => String, Gem::Version}] the parsed version data.
404
+ ###
405
+ def self.parse_jss_version(version)
406
+ spl = version.split('.')
407
+
408
+ case spl.count
409
+ when 1
410
+ major = spl[0].to_i
411
+ minor = 0
412
+ revision = '0'
413
+ when 2
414
+ major = spl[0].to_i
415
+ minor = spl[1][0,1].to_i
416
+ revision = spl[1][1..-1]
417
+ revision = '0' if revision.empty?
418
+ else
419
+ major = spl[0].to_i
420
+ minor = spl[1][0,1].to_i
421
+ revision = spl[1..-1].join('.')[1..-1]
422
+ revision = revision[1..-1] if revision.start_with? '.'
423
+ end
424
+
425
+ ###revision = revision[1..-1] if revision.start_with? '.'
426
+ { :major => major,
427
+ :minor => minor,
428
+ :revision => revision,
429
+ :version => Gem::Version.new("#{major}.#{minor}.#{revision}")
430
+ }
431
+ end
432
+
433
+ ### @return [Boolean] is this code running as root?
434
+ ###
435
+ def self.superuser?
436
+ Process.euid == 0
437
+ end
438
+
439
+ ### Retrive one or all lines from whatever was piped to standard input.
440
+ ###
441
+ ### Standard input is read completely the first time this method is called
442
+ ### and the lines are stored as an Array in the module var @@stdin_lines
443
+ ###
444
+ ### @param line[Integer] which line of stdin is being retrieved.
445
+ ### The default is zero (0) which returns all of stdin as a single string.
446
+ ###
447
+ ### @return [String, nil] the requested ling of stdin, or nil if it doesn't exist.
448
+ ###
449
+ def self.stdin(line = 0)
450
+ @@stdin_lines ||= ($stdin.tty? ? [] : $stdin.read.lines.map{|line| line.chomp("\n") })
451
+
452
+ return @@stdin_lines.join("\n") if line <= 0
453
+ idx = line - 1
454
+ return @@stdin_lines[idx]
455
+ end
456
+
457
+ ### Prompt for a password in a terminal.
458
+ ###
459
+ ### @param message [String] the prompt message to display
460
+ ###
461
+ ### @return [String] the text typed by the user
462
+ ###
463
+ def self.prompt_for_password(message)
464
+
465
+ begin
466
+ $stdin.reopen '/dev/tty' unless $stdin.tty?
467
+ $stderr.print "#{message} "
468
+ system "/bin/stty -echo"
469
+ pw = $stdin.gets.chomp("\n")
470
+ puts
471
+ ensure
472
+ system "/bin/stty echo"
473
+ end # begin
474
+ return pw
475
+ end
476
+
477
+
478
+ end # module